feat(sessions): welcome screen (#3112)
* prototype - can send layout name for new session from session-manager * feat(sessions): ui for selecting layout for new session in the session-manager * fix: send available layouts to plugins * make tests compile * fix tests * improve ui * fix: respect built-in layouts * ui for built-in layouts * some cleanups * style(fmt): rustfmt * welcome screen ui * fix: make sure layout config is not shared between sessions * allow disconnecting other users from current session and killing other sessions * fix: respect default layout * add welcome screen layout * tests(plugins): new api methods * fix(session-manager): do not quit welcome screen on esc and break * fix(plugins): adjust permissions * style(fmt): rustfmt * style(fmt): fix warnings
This commit is contained in:
parent
286f7ccc28
commit
6b20a958f4
36 changed files with 1941 additions and 400 deletions
|
|
@ -280,6 +280,16 @@ impl ZellijPlugin for State {
|
|||
context,
|
||||
);
|
||||
},
|
||||
Key::Ctrl('5') => {
|
||||
switch_session(Some("my_new_session"));
|
||||
},
|
||||
Key::Ctrl('6') => disconnect_other_clients(),
|
||||
Key::Ctrl('7') => {
|
||||
switch_session_with_layout(
|
||||
Some("my_other_new_session"),
|
||||
LayoutInfo::BuiltIn("compact".to_owned()),
|
||||
);
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Event::CustomMessage(message, payload) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod new_session_info;
|
||||
mod resurrectable_sessions;
|
||||
mod session_list;
|
||||
mod ui;
|
||||
|
|
@ -5,34 +6,58 @@ use zellij_tile::prelude::*;
|
|||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use new_session_info::NewSessionInfo;
|
||||
use ui::{
|
||||
components::{
|
||||
render_controls_line, render_error, render_new_session_line, render_prompt,
|
||||
render_renaming_session_screen, render_resurrection_toggle, Colors,
|
||||
render_controls_line, render_error, render_new_session_block, render_prompt,
|
||||
render_renaming_session_screen, render_screen_toggle, Colors,
|
||||
},
|
||||
welcome_screen::{render_banner, render_welcome_boundaries},
|
||||
SessionUiInfo,
|
||||
};
|
||||
|
||||
use resurrectable_sessions::ResurrectableSessions;
|
||||
use session_list::SessionList;
|
||||
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
enum ActiveScreen {
|
||||
NewSession,
|
||||
AttachToSession,
|
||||
ResurrectSession,
|
||||
}
|
||||
|
||||
impl Default for ActiveScreen {
|
||||
fn default() -> Self {
|
||||
ActiveScreen::AttachToSession
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
session_name: Option<String>,
|
||||
sessions: SessionList,
|
||||
resurrectable_sessions: ResurrectableSessions,
|
||||
search_term: String,
|
||||
new_session_name: Option<String>,
|
||||
new_session_info: NewSessionInfo,
|
||||
renaming_session_name: Option<String>,
|
||||
error: Option<String>,
|
||||
browsing_resurrection_sessions: bool,
|
||||
active_screen: ActiveScreen,
|
||||
colors: Colors,
|
||||
is_welcome_screen: bool,
|
||||
show_kill_all_sessions_warning: bool,
|
||||
}
|
||||
|
||||
register_plugin!(State);
|
||||
|
||||
impl ZellijPlugin for State {
|
||||
fn load(&mut self, _configuration: BTreeMap<String, String>) {
|
||||
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||
self.is_welcome_screen = configuration
|
||||
.get("welcome_screen")
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false);
|
||||
if self.is_welcome_screen {
|
||||
self.active_screen = ActiveScreen::NewSession;
|
||||
}
|
||||
subscribe(&[
|
||||
EventType::ModeUpdate,
|
||||
EventType::SessionUpdate,
|
||||
|
|
@ -55,6 +80,12 @@ impl ZellijPlugin for State {
|
|||
should_render = true;
|
||||
},
|
||||
Event::SessionUpdate(session_infos, resurrectable_session_list) => {
|
||||
for session_info in &session_infos {
|
||||
if session_info.is_current_session {
|
||||
self.new_session_info
|
||||
.update_layout_list(session_info.available_layouts.clone());
|
||||
}
|
||||
}
|
||||
self.resurrectable_sessions
|
||||
.update(resurrectable_session_list);
|
||||
self.update_session_infos(session_infos);
|
||||
|
|
@ -66,36 +97,53 @@ impl ZellijPlugin for State {
|
|||
}
|
||||
|
||||
fn render(&mut self, rows: usize, cols: usize) {
|
||||
if self.browsing_resurrection_sessions {
|
||||
self.resurrectable_sessions.render(rows, cols);
|
||||
return;
|
||||
} else if let Some(new_session_name) = self.renaming_session_name.as_ref() {
|
||||
render_renaming_session_screen(&new_session_name, rows, cols);
|
||||
return;
|
||||
let (x, y, width, height) = self.main_menu_size(rows, cols);
|
||||
|
||||
if self.is_welcome_screen {
|
||||
render_banner(x, 0, rows.saturating_sub(height), width);
|
||||
}
|
||||
render_resurrection_toggle(cols, false);
|
||||
render_prompt(
|
||||
self.new_session_name.is_some(),
|
||||
&self.search_term,
|
||||
render_screen_toggle(self.active_screen, x, y, width.saturating_sub(2));
|
||||
|
||||
match self.active_screen {
|
||||
ActiveScreen::NewSession => {
|
||||
render_new_session_block(
|
||||
&self.new_session_info,
|
||||
self.colors,
|
||||
height,
|
||||
width,
|
||||
x,
|
||||
y + 2,
|
||||
);
|
||||
let room_for_list = rows.saturating_sub(5); // search line and controls
|
||||
self.sessions.update_rows(room_for_list);
|
||||
let list = self
|
||||
.sessions
|
||||
.render(room_for_list, cols.saturating_sub(7), self.colors); // 7 for various ui
|
||||
for line in list {
|
||||
println!("{}", line.render());
|
||||
}
|
||||
render_new_session_line(
|
||||
&self.new_session_name,
|
||||
self.sessions.is_searching,
|
||||
self.colors,
|
||||
);
|
||||
if let Some(error) = self.error.as_ref() {
|
||||
render_error(&error, rows, cols);
|
||||
},
|
||||
ActiveScreen::AttachToSession => {
|
||||
if let Some(new_session_name) = self.renaming_session_name.as_ref() {
|
||||
render_renaming_session_screen(&new_session_name, height, width, x, y + 2);
|
||||
} else if self.show_kill_all_sessions_warning {
|
||||
self.render_kill_all_sessions_warning(height, width, x, y);
|
||||
} else {
|
||||
render_controls_line(self.sessions.is_searching, rows, cols, self.colors);
|
||||
render_prompt(&self.search_term, self.colors, x, y + 2);
|
||||
let room_for_list = height.saturating_sub(6); // search line and controls;
|
||||
self.sessions.update_rows(room_for_list);
|
||||
let list =
|
||||
self.sessions
|
||||
.render(room_for_list, width.saturating_sub(7), self.colors); // 7 for various ui
|
||||
for (i, line) in list.iter().enumerate() {
|
||||
print!("\u{1b}[{};{}H{}", y + i + 5, x, line.render());
|
||||
}
|
||||
}
|
||||
},
|
||||
ActiveScreen::ResurrectSession => {
|
||||
self.resurrectable_sessions.render(height, width, x, y);
|
||||
},
|
||||
}
|
||||
if let Some(error) = self.error.as_ref() {
|
||||
render_error(&error, height, width, x, y);
|
||||
} else {
|
||||
render_controls_line(self.active_screen, width, self.colors, x + 1, rows);
|
||||
}
|
||||
if self.is_welcome_screen {
|
||||
render_welcome_boundaries(rows, cols); // explicitly done in the end to override some
|
||||
// stuff, see comment in function
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -109,40 +157,77 @@ impl State {
|
|||
self.error = None;
|
||||
return true;
|
||||
}
|
||||
match self.active_screen {
|
||||
ActiveScreen::NewSession => self.handle_new_session_key(key),
|
||||
ActiveScreen::AttachToSession => self.handle_attach_to_session(key),
|
||||
ActiveScreen::ResurrectSession => self.handle_resurrect_session_key(key),
|
||||
}
|
||||
}
|
||||
fn handle_new_session_key(&mut self, key: Key) -> bool {
|
||||
let mut should_render = false;
|
||||
if let Key::Right = key {
|
||||
if self.new_session_name.is_none() {
|
||||
self.sessions.result_expand();
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::Left = key {
|
||||
if self.new_session_name.is_none() {
|
||||
self.sessions.result_shrink();
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::Down = key {
|
||||
if self.browsing_resurrection_sessions {
|
||||
self.resurrectable_sessions.move_selection_down();
|
||||
} else if self.new_session_name.is_none() && self.renaming_session_name.is_none() {
|
||||
self.sessions.move_selection_down();
|
||||
}
|
||||
if let Key::Down = key {
|
||||
self.new_session_info.handle_key(key);
|
||||
should_render = true;
|
||||
} else if let Key::Up = key {
|
||||
if self.browsing_resurrection_sessions {
|
||||
self.resurrectable_sessions.move_selection_up();
|
||||
} else if self.new_session_name.is_none() && self.renaming_session_name.is_none() {
|
||||
self.sessions.move_selection_up();
|
||||
}
|
||||
self.new_session_info.handle_key(key);
|
||||
should_render = true;
|
||||
} else if let Key::Char(character) = key {
|
||||
if character == '\n' {
|
||||
self.handle_selection();
|
||||
} else if let Some(new_session_name) = self.new_session_name.as_mut() {
|
||||
} else {
|
||||
self.new_session_info.handle_key(key);
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::Backspace = key {
|
||||
self.new_session_info.handle_key(key);
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('w') = key {
|
||||
self.active_screen = ActiveScreen::NewSession;
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('c') = key {
|
||||
self.new_session_info.handle_key(key);
|
||||
should_render = true;
|
||||
} else if let Key::BackTab = key {
|
||||
self.toggle_active_screen();
|
||||
should_render = true;
|
||||
} else if let Key::Esc = key {
|
||||
self.new_session_info.handle_key(key);
|
||||
should_render = true;
|
||||
}
|
||||
should_render
|
||||
}
|
||||
fn handle_attach_to_session(&mut self, key: Key) -> bool {
|
||||
let mut should_render = false;
|
||||
if self.show_kill_all_sessions_warning {
|
||||
if let Key::Char('y') = key {
|
||||
let all_other_sessions = self.sessions.all_other_sessions();
|
||||
kill_sessions(&all_other_sessions);
|
||||
self.reset_selected_index();
|
||||
self.search_term.clear();
|
||||
self.sessions
|
||||
.update_search_term(&self.search_term, &self.colors);
|
||||
self.show_kill_all_sessions_warning = false
|
||||
} else if let Key::Char('n') | Key::Esc | Key::Ctrl('c') = key {
|
||||
self.show_kill_all_sessions_warning = false
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::Right = key {
|
||||
self.sessions.result_expand();
|
||||
should_render = true;
|
||||
} else if let Key::Left = key {
|
||||
self.sessions.result_shrink();
|
||||
should_render = true;
|
||||
} else if let Key::Down = key {
|
||||
self.sessions.move_selection_down();
|
||||
should_render = true;
|
||||
} else if let Key::Up = key {
|
||||
self.sessions.move_selection_up();
|
||||
should_render = true;
|
||||
} else if let Key::Char(character) = key {
|
||||
if character == '\n' {
|
||||
self.handle_selection();
|
||||
} else if let Some(new_session_name) = self.renaming_session_name.as_mut() {
|
||||
new_session_name.push(character);
|
||||
} else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
|
||||
renaming_session_name.push(character);
|
||||
} else if self.browsing_resurrection_sessions {
|
||||
self.resurrectable_sessions.handle_character(character);
|
||||
} else {
|
||||
self.search_term.push(character);
|
||||
self.sessions
|
||||
|
|
@ -150,20 +235,12 @@ impl State {
|
|||
}
|
||||
should_render = true;
|
||||
} else if let Key::Backspace = key {
|
||||
if let Some(new_session_name) = self.new_session_name.as_mut() {
|
||||
if let Some(new_session_name) = self.renaming_session_name.as_mut() {
|
||||
if new_session_name.is_empty() {
|
||||
self.new_session_name = None;
|
||||
self.renaming_session_name = None;
|
||||
} else {
|
||||
new_session_name.pop();
|
||||
}
|
||||
} else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
|
||||
if renaming_session_name.is_empty() {
|
||||
self.renaming_session_name = None;
|
||||
} else {
|
||||
renaming_session_name.pop();
|
||||
}
|
||||
} else if self.browsing_resurrection_sessions {
|
||||
self.resurrectable_sessions.handle_backspace();
|
||||
} else {
|
||||
self.search_term.pop();
|
||||
self.sessions
|
||||
|
|
@ -171,101 +248,110 @@ impl State {
|
|||
}
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('w') = key {
|
||||
if self.sessions.is_searching || self.browsing_resurrection_sessions {
|
||||
// no-op
|
||||
} else if self.new_session_name.is_some() {
|
||||
self.new_session_name = None;
|
||||
} else {
|
||||
self.new_session_name = Some(String::new());
|
||||
}
|
||||
self.active_screen = ActiveScreen::NewSession;
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('r') = key {
|
||||
if self.sessions.is_searching || self.browsing_resurrection_sessions {
|
||||
// no-op
|
||||
} else if self.renaming_session_name.is_some() {
|
||||
self.renaming_session_name = None;
|
||||
} else {
|
||||
self.renaming_session_name = Some(String::new());
|
||||
should_render = true;
|
||||
} else if let Key::Delete = key {
|
||||
if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
|
||||
kill_sessions(&[selected_session_name]);
|
||||
self.reset_selected_index();
|
||||
self.search_term.clear();
|
||||
self.sessions
|
||||
.update_search_term(&self.search_term, &self.colors);
|
||||
} else {
|
||||
self.show_error("Must select session before killing it.");
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('d') = key {
|
||||
let all_other_sessions = self.sessions.all_other_sessions();
|
||||
if all_other_sessions.is_empty() {
|
||||
self.show_error("No other sessions to kill. Quit to kill the current one.");
|
||||
} else {
|
||||
self.show_kill_all_sessions_warning = true;
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('x') = key {
|
||||
disconnect_other_clients();
|
||||
} else if let Key::Ctrl('c') = key {
|
||||
if let Some(new_session_name) = self.new_session_name.as_mut() {
|
||||
if new_session_name.is_empty() {
|
||||
self.new_session_name = None;
|
||||
} else {
|
||||
new_session_name.clear()
|
||||
}
|
||||
} else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
|
||||
if renaming_session_name.is_empty() {
|
||||
self.renaming_session_name = None;
|
||||
} else {
|
||||
renaming_session_name.clear()
|
||||
}
|
||||
} else if !self.search_term.is_empty() {
|
||||
if !self.search_term.is_empty() {
|
||||
self.search_term.clear();
|
||||
self.sessions
|
||||
.update_search_term(&self.search_term, &self.colors);
|
||||
self.reset_selected_index();
|
||||
} else {
|
||||
} else if !self.is_welcome_screen {
|
||||
self.reset_selected_index();
|
||||
hide_self();
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::BackTab = key {
|
||||
self.browsing_resurrection_sessions = !self.browsing_resurrection_sessions;
|
||||
self.toggle_active_screen();
|
||||
should_render = true;
|
||||
} else if let Key::Delete = key {
|
||||
if self.browsing_resurrection_sessions {
|
||||
self.resurrectable_sessions.delete_selected_session();
|
||||
should_render = true;
|
||||
}
|
||||
} else if let Key::Ctrl('d') = key {
|
||||
if self.browsing_resurrection_sessions {
|
||||
self.resurrectable_sessions
|
||||
.show_delete_all_sessions_warning();
|
||||
should_render = true;
|
||||
}
|
||||
} else if let Key::Esc = key {
|
||||
if self.renaming_session_name.is_some() {
|
||||
self.renaming_session_name = None;
|
||||
should_render = true;
|
||||
} else if self.new_session_name.is_some() {
|
||||
self.new_session_name = None;
|
||||
} else if !self.is_welcome_screen {
|
||||
hide_self();
|
||||
}
|
||||
}
|
||||
should_render
|
||||
}
|
||||
fn handle_resurrect_session_key(&mut self, key: Key) -> bool {
|
||||
let mut should_render = false;
|
||||
if let Key::Down = key {
|
||||
self.resurrectable_sessions.move_selection_down();
|
||||
should_render = true;
|
||||
} else if let Key::Up = key {
|
||||
self.resurrectable_sessions.move_selection_up();
|
||||
should_render = true;
|
||||
} else if let Key::Char(character) = key {
|
||||
if character == '\n' {
|
||||
self.handle_selection();
|
||||
} else {
|
||||
self.resurrectable_sessions.handle_character(character);
|
||||
}
|
||||
should_render = true;
|
||||
} else if let Key::Backspace = key {
|
||||
self.resurrectable_sessions.handle_backspace();
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('w') = key {
|
||||
self.active_screen = ActiveScreen::NewSession;
|
||||
should_render = true;
|
||||
} else if let Key::BackTab = key {
|
||||
self.toggle_active_screen();
|
||||
should_render = true;
|
||||
} else if let Key::Delete = key {
|
||||
self.resurrectable_sessions.delete_selected_session();
|
||||
should_render = true;
|
||||
} else if let Key::Ctrl('d') = key {
|
||||
self.resurrectable_sessions
|
||||
.show_delete_all_sessions_warning();
|
||||
should_render = true;
|
||||
} else if let Key::Esc = key {
|
||||
if !self.is_welcome_screen {
|
||||
hide_self();
|
||||
}
|
||||
}
|
||||
should_render
|
||||
}
|
||||
fn handle_selection(&mut self) {
|
||||
if self.browsing_resurrection_sessions {
|
||||
if let Some(session_name_to_resurrect) =
|
||||
self.resurrectable_sessions.get_selected_session_name()
|
||||
{
|
||||
switch_session(Some(&session_name_to_resurrect));
|
||||
}
|
||||
} else if let Some(new_session_name) = &self.new_session_name {
|
||||
if new_session_name.is_empty() {
|
||||
switch_session(None);
|
||||
} else if self.session_name.as_ref() == Some(new_session_name) {
|
||||
// noop - we're already here!
|
||||
self.new_session_name = None;
|
||||
} else {
|
||||
switch_session(Some(new_session_name));
|
||||
}
|
||||
} else if let Some(renaming_session_name) = &self.renaming_session_name.take() {
|
||||
match self.active_screen {
|
||||
ActiveScreen::NewSession => {
|
||||
self.new_session_info.handle_selection(&self.session_name);
|
||||
},
|
||||
ActiveScreen::AttachToSession => {
|
||||
if let Some(renaming_session_name) = &self.renaming_session_name.take() {
|
||||
if renaming_session_name.is_empty() {
|
||||
// TODO: implement these, then implement the error UI, then implement the renaming
|
||||
// session screen, then test it
|
||||
self.show_error("New name must not be empty.");
|
||||
return; // s that we don't hide self
|
||||
return; // so that we don't hide self
|
||||
} else if self.session_name.as_ref() == Some(renaming_session_name) {
|
||||
// noop - we're already called that!
|
||||
return; // s that we don't hide self
|
||||
return; // so that we don't hide self
|
||||
} else if self.sessions.has_session(&renaming_session_name) {
|
||||
self.show_error("A session by this name already exists.");
|
||||
return; // s that we don't hide self
|
||||
return; // so that we don't hide self
|
||||
} else if self
|
||||
.resurrectable_sessions
|
||||
.has_session(&renaming_session_name)
|
||||
|
|
@ -277,7 +363,8 @@ impl State {
|
|||
rename_session(&renaming_session_name);
|
||||
return; // s that we don't hide self
|
||||
}
|
||||
} else if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
|
||||
}
|
||||
if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
|
||||
let selected_tab = self.sessions.get_selected_tab_position();
|
||||
let selected_pane = self.sessions.get_selected_pane_id();
|
||||
let is_current_session = self.sessions.selected_is_current_session();
|
||||
|
|
@ -292,15 +379,34 @@ impl State {
|
|||
go_to_tab(tab_position as u32);
|
||||
}
|
||||
} else {
|
||||
switch_session_with_focus(&selected_session_name, selected_tab, selected_pane);
|
||||
switch_session_with_focus(
|
||||
&selected_session_name,
|
||||
selected_tab,
|
||||
selected_pane,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.reset_selected_index();
|
||||
self.new_session_name = None;
|
||||
self.search_term.clear();
|
||||
self.sessions
|
||||
.update_search_term(&self.search_term, &self.colors);
|
||||
hide_self();
|
||||
},
|
||||
ActiveScreen::ResurrectSession => {
|
||||
if let Some(session_name_to_resurrect) =
|
||||
self.resurrectable_sessions.get_selected_session_name()
|
||||
{
|
||||
switch_session(Some(&session_name_to_resurrect));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
fn toggle_active_screen(&mut self) {
|
||||
self.active_screen = match self.active_screen {
|
||||
ActiveScreen::NewSession => ActiveScreen::AttachToSession,
|
||||
ActiveScreen::AttachToSession => ActiveScreen::ResurrectSession,
|
||||
ActiveScreen::ResurrectSession => ActiveScreen::NewSession,
|
||||
};
|
||||
}
|
||||
fn show_error(&mut self, error_text: &str) {
|
||||
self.error = Some(error_text.to_owned());
|
||||
|
|
@ -329,4 +435,53 @@ impl State {
|
|||
}
|
||||
self.sessions.set_sessions(session_infos);
|
||||
}
|
||||
fn main_menu_size(&self, rows: usize, cols: usize) -> (usize, usize, usize, usize) {
|
||||
// x, y, width, height
|
||||
let width = if self.is_welcome_screen {
|
||||
std::cmp::min(cols, 101)
|
||||
} else {
|
||||
cols
|
||||
};
|
||||
let x = if self.is_welcome_screen {
|
||||
(cols.saturating_sub(width) as f64 / 2.0).floor() as usize + 2
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let y = if self.is_welcome_screen {
|
||||
(rows.saturating_sub(15) as f64 / 2.0).floor() as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let height = rows.saturating_sub(y);
|
||||
(x, y, width, height)
|
||||
}
|
||||
fn render_kill_all_sessions_warning(&self, rows: usize, columns: usize, x: usize, y: usize) {
|
||||
if rows == 0 || columns == 0 {
|
||||
return;
|
||||
}
|
||||
let session_count = self.sessions.all_other_sessions().len();
|
||||
let session_count_len = session_count.to_string().chars().count();
|
||||
let warning_description_text = format!("This will kill {} active sessions", session_count);
|
||||
let confirmation_text = "Are you sure? (y/n)";
|
||||
let warning_y_location = y + (rows / 2).saturating_sub(1);
|
||||
let confirmation_y_location = y + (rows / 2) + 1;
|
||||
let warning_x_location =
|
||||
x + columns.saturating_sub(warning_description_text.chars().count()) / 2;
|
||||
let confirmation_x_location =
|
||||
x + columns.saturating_sub(confirmation_text.chars().count()) / 2;
|
||||
print_text_with_coordinates(
|
||||
Text::new(warning_description_text).color_range(0, 15..16 + session_count_len),
|
||||
warning_x_location,
|
||||
warning_y_location,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
print_text_with_coordinates(
|
||||
Text::new(confirmation_text).color_indices(2, vec![15, 17]),
|
||||
confirmation_x_location,
|
||||
confirmation_y_location,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
265
default-plugins/session-manager/src/new_session_info.rs
Normal file
265
default-plugins/session-manager/src/new_session_info.rs
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use std::cmp::Ordering;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NewSessionInfo {
|
||||
name: String,
|
||||
layout_list: LayoutList,
|
||||
entering_new_session_info: EnteringState,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum EnteringState {
|
||||
EnteringName,
|
||||
EnteringLayoutSearch,
|
||||
}
|
||||
|
||||
impl Default for EnteringState {
|
||||
fn default() -> Self {
|
||||
EnteringState::EnteringName
|
||||
}
|
||||
}
|
||||
|
||||
impl NewSessionInfo {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
pub fn layout_search_term(&self) -> &str {
|
||||
&self.layout_list.layout_search_term
|
||||
}
|
||||
pub fn entering_new_session_info(&self) -> bool {
|
||||
true
|
||||
}
|
||||
pub fn entering_new_session_name(&self) -> bool {
|
||||
self.entering_new_session_info == EnteringState::EnteringName
|
||||
}
|
||||
pub fn entering_layout_search_term(&self) -> bool {
|
||||
self.entering_new_session_info == EnteringState::EnteringLayoutSearch
|
||||
}
|
||||
pub fn add_char(&mut self, character: char) {
|
||||
match self.entering_new_session_info {
|
||||
EnteringState::EnteringName => {
|
||||
self.name.push(character);
|
||||
},
|
||||
EnteringState::EnteringLayoutSearch => {
|
||||
self.layout_list.layout_search_term.push(character);
|
||||
self.update_layout_search_term();
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn handle_backspace(&mut self) {
|
||||
match self.entering_new_session_info {
|
||||
EnteringState::EnteringName => {
|
||||
self.name.pop();
|
||||
},
|
||||
EnteringState::EnteringLayoutSearch => {
|
||||
self.layout_list.layout_search_term.pop();
|
||||
self.update_layout_search_term();
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn handle_break(&mut self) {
|
||||
match self.entering_new_session_info {
|
||||
EnteringState::EnteringName => {
|
||||
self.name.clear();
|
||||
},
|
||||
EnteringState::EnteringLayoutSearch => {
|
||||
self.layout_list.layout_search_term.clear();
|
||||
self.entering_new_session_info = EnteringState::EnteringName;
|
||||
self.update_layout_search_term();
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn handle_key(&mut self, key: Key) {
|
||||
match key {
|
||||
Key::Backspace => {
|
||||
self.handle_backspace();
|
||||
},
|
||||
Key::Ctrl('c') | Key::Esc => {
|
||||
self.handle_break();
|
||||
},
|
||||
Key::Char(character) => {
|
||||
self.add_char(character);
|
||||
},
|
||||
Key::Up => {
|
||||
self.move_selection_up();
|
||||
},
|
||||
Key::Down => {
|
||||
self.move_selection_down();
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
pub fn handle_selection(&mut self, current_session_name: &Option<String>) {
|
||||
match self.entering_new_session_info {
|
||||
EnteringState::EnteringLayoutSearch => {
|
||||
let new_session_layout: Option<LayoutInfo> = self.selected_layout_info();
|
||||
let new_session_name = if self.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.name.as_str())
|
||||
};
|
||||
if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) {
|
||||
match new_session_layout {
|
||||
Some(new_session_layout) => {
|
||||
switch_session_with_layout(new_session_name, new_session_layout)
|
||||
},
|
||||
None => {
|
||||
switch_session(new_session_name);
|
||||
},
|
||||
}
|
||||
}
|
||||
self.name.clear();
|
||||
self.layout_list.clear_selection();
|
||||
hide_self();
|
||||
},
|
||||
EnteringState::EnteringName => {
|
||||
self.entering_new_session_info = EnteringState::EnteringLayoutSearch;
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn update_layout_list(&mut self, layout_info: Vec<LayoutInfo>) {
|
||||
self.layout_list.update_layout_list(layout_info);
|
||||
}
|
||||
pub fn layout_list(&self) -> Vec<(LayoutInfo, bool)> {
|
||||
// bool - is_selected
|
||||
self.layout_list
|
||||
.layout_list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index))
|
||||
.collect()
|
||||
}
|
||||
pub fn layouts_to_render(&self) -> Vec<(LayoutInfo, Vec<usize>, bool)> {
|
||||
// (layout_info,
|
||||
// search_indices,
|
||||
// is_selected)
|
||||
if self.is_searching() {
|
||||
self.layout_search_results()
|
||||
.into_iter()
|
||||
.map(|(layout_search_result, is_selected)| {
|
||||
(
|
||||
layout_search_result.layout_info,
|
||||
layout_search_result.indices,
|
||||
is_selected,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.layout_list()
|
||||
.into_iter()
|
||||
.map(|(layout_info, is_selected)| (layout_info, vec![], is_selected))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
pub fn layout_search_results(&self) -> Vec<(LayoutSearchResult, bool)> {
|
||||
// bool - is_selected
|
||||
self.layout_list
|
||||
.layout_search_results
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index))
|
||||
.collect()
|
||||
}
|
||||
pub fn is_searching(&self) -> bool {
|
||||
!self.layout_list.layout_search_term.is_empty()
|
||||
}
|
||||
pub fn layout_count(&self) -> usize {
|
||||
self.layout_list.layout_list.len()
|
||||
}
|
||||
pub fn selected_layout_info(&self) -> Option<LayoutInfo> {
|
||||
self.layout_list.selected_layout_info()
|
||||
}
|
||||
fn update_layout_search_term(&mut self) {
|
||||
if self.layout_list.layout_search_term.is_empty() {
|
||||
self.layout_list.clear_selection();
|
||||
self.layout_list.layout_search_results = vec![];
|
||||
} else {
|
||||
let mut matches = vec![];
|
||||
let matcher = SkimMatcherV2::default().use_cache(true);
|
||||
for layout_info in &self.layout_list.layout_list {
|
||||
if let Some((score, indices)) =
|
||||
matcher.fuzzy_indices(&layout_info.name(), &self.layout_list.layout_search_term)
|
||||
{
|
||||
matches.push(LayoutSearchResult {
|
||||
layout_info: layout_info.clone(),
|
||||
score,
|
||||
indices,
|
||||
});
|
||||
}
|
||||
}
|
||||
matches.sort_by(|a, b| b.score.cmp(&a.score));
|
||||
self.layout_list.layout_search_results = matches;
|
||||
self.layout_list.clear_selection();
|
||||
}
|
||||
}
|
||||
fn move_selection_up(&mut self) {
|
||||
self.layout_list.move_selection_up();
|
||||
}
|
||||
fn move_selection_down(&mut self) {
|
||||
self.layout_list.move_selection_down();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LayoutList {
|
||||
layout_list: Vec<LayoutInfo>,
|
||||
layout_search_results: Vec<LayoutSearchResult>,
|
||||
selected_layout_index: usize,
|
||||
layout_search_term: String,
|
||||
}
|
||||
|
||||
impl LayoutList {
|
||||
pub fn update_layout_list(&mut self, layout_list: Vec<LayoutInfo>) {
|
||||
let old_layout_length = self.layout_list.len();
|
||||
self.layout_list = layout_list;
|
||||
if old_layout_length != self.layout_list.len() {
|
||||
// honestly, this is just the UX choice that sucks the least...
|
||||
self.clear_selection();
|
||||
}
|
||||
}
|
||||
pub fn selected_layout_info(&self) -> Option<LayoutInfo> {
|
||||
if !self.layout_search_term.is_empty() {
|
||||
self.layout_search_results
|
||||
.get(self.selected_layout_index)
|
||||
.map(|l| l.layout_info.clone())
|
||||
} else {
|
||||
self.layout_list.get(self.selected_layout_index).cloned()
|
||||
}
|
||||
}
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.selected_layout_index = 0;
|
||||
}
|
||||
fn max_index(&self) -> usize {
|
||||
if self.layout_search_term.is_empty() {
|
||||
self.layout_list.len().saturating_sub(1)
|
||||
} else {
|
||||
self.layout_search_results.len().saturating_sub(1)
|
||||
}
|
||||
}
|
||||
fn move_selection_up(&mut self) {
|
||||
let max_index = self.max_index();
|
||||
if self.selected_layout_index > 0 {
|
||||
self.selected_layout_index -= 1;
|
||||
} else {
|
||||
self.selected_layout_index = max_index;
|
||||
}
|
||||
}
|
||||
fn move_selection_down(&mut self) {
|
||||
let max_index = self.max_index();
|
||||
if self.selected_layout_index < max_index {
|
||||
self.selected_layout_index += 1;
|
||||
} else {
|
||||
self.selected_layout_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LayoutSearchResult {
|
||||
pub layout_info: LayoutInfo,
|
||||
pub score: i64,
|
||||
pub indices: Vec<usize>,
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ use fuzzy_matcher::skim::SkimMatcherV2;
|
|||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use humantime::format_duration;
|
||||
|
||||
use crate::ui::components::render_resurrection_toggle;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use zellij_tile::shim::*;
|
||||
|
|
@ -24,23 +22,22 @@ impl ResurrectableSessions {
|
|||
list.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
self.all_resurrectable_sessions = list;
|
||||
}
|
||||
pub fn render(&self, rows: usize, columns: usize) {
|
||||
pub fn render(&self, rows: usize, columns: usize, x: usize, y: usize) {
|
||||
if self.delete_all_dead_sessions_warning {
|
||||
self.render_delete_all_sessions_warning(rows, columns);
|
||||
self.render_delete_all_sessions_warning(rows, columns, x, y);
|
||||
return;
|
||||
}
|
||||
render_resurrection_toggle(columns, true);
|
||||
let search_indication = Text::new(format!("> {}_", self.search_term)).color_range(1, ..);
|
||||
let table_rows = rows.saturating_sub(3);
|
||||
let search_indication =
|
||||
Text::new(format!("Search: {}_", self.search_term)).color_range(2, ..7);
|
||||
let table_rows = rows.saturating_sub(5); // search row, toggle row and some padding
|
||||
let table_columns = columns;
|
||||
let table = if self.is_searching {
|
||||
self.render_search_results(table_rows, columns)
|
||||
} else {
|
||||
self.render_all_entries(table_rows, columns)
|
||||
};
|
||||
print_text_with_coordinates(search_indication, 0, 0, None, None);
|
||||
print_table_with_coordinates(table, 0, 1, Some(table_columns), Some(table_rows));
|
||||
self.render_controls_line(rows);
|
||||
print_text_with_coordinates(search_indication, x.saturating_sub(1), y + 2, None, None);
|
||||
print_table_with_coordinates(table, x, y + 3, Some(table_columns), Some(table_rows));
|
||||
}
|
||||
fn render_search_results(&self, table_rows: usize, _table_columns: usize) -> Table {
|
||||
let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row
|
||||
|
|
@ -103,7 +100,7 @@ impl ResurrectableSessions {
|
|||
}
|
||||
table
|
||||
}
|
||||
fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize) {
|
||||
fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize, x: usize, y: usize) {
|
||||
if rows == 0 || columns == 0 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -112,11 +109,12 @@ impl ResurrectableSessions {
|
|||
let warning_description_text =
|
||||
format!("This will delete {} resurrectable sessions", session_count,);
|
||||
let confirmation_text = "Are you sure? (y/n)";
|
||||
let warning_y_location = (rows / 2).saturating_sub(1);
|
||||
let confirmation_y_location = (rows / 2) + 1;
|
||||
let warning_y_location = y + (rows / 2).saturating_sub(1);
|
||||
let confirmation_y_location = y + (rows / 2) + 1;
|
||||
let warning_x_location =
|
||||
columns.saturating_sub(warning_description_text.chars().count()) / 2;
|
||||
let confirmation_x_location = columns.saturating_sub(confirmation_text.chars().count()) / 2;
|
||||
x + columns.saturating_sub(warning_description_text.chars().count()) / 2;
|
||||
let confirmation_x_location =
|
||||
x + columns.saturating_sub(confirmation_text.chars().count()) / 2;
|
||||
print_text_with_coordinates(
|
||||
Text::new(warning_description_text).color_range(0, 17..18 + session_count_len),
|
||||
warning_x_location,
|
||||
|
|
@ -200,15 +198,6 @@ impl ResurrectableSessions {
|
|||
Text::new(" ")
|
||||
}
|
||||
}
|
||||
fn render_controls_line(&self, rows: usize) {
|
||||
let controls_line = Text::new(format!(
|
||||
"Help: <↓↑> - Navigate, <DEL> - Delete Session, <Ctrl d> - Delete all sessions"
|
||||
))
|
||||
.color_range(3, 6..10)
|
||||
.color_range(3, 23..29)
|
||||
.color_range(3, 47..56);
|
||||
print_text_with_coordinates(controls_line, 0, rows.saturating_sub(1), None, None);
|
||||
}
|
||||
pub fn move_selection_down(&mut self) {
|
||||
if self.is_searching {
|
||||
if let Some(selected_index) = self.selected_search_index.as_mut() {
|
||||
|
|
|
|||
|
|
@ -323,6 +323,18 @@ impl SessionList {
|
|||
.find(|s| s.name == old_name)
|
||||
.map(|s| s.name = new_name.to_owned());
|
||||
}
|
||||
pub fn all_other_sessions(&self) -> Vec<String> {
|
||||
self.session_ui_infos
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if !s.is_current_session {
|
||||
Some(s.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use unicode_width::UnicodeWidthStr;
|
|||
use zellij_tile::prelude::*;
|
||||
|
||||
use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo};
|
||||
use crate::{ActiveScreen, NewSessionInfo};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ListItem {
|
||||
|
|
@ -292,18 +293,45 @@ impl LineToRender {
|
|||
pub fn append(&mut self, to_append: &str) {
|
||||
self.line.push_str(to_append)
|
||||
}
|
||||
pub fn make_selected(&mut self) {
|
||||
pub fn make_selected_as_search(&mut self, add_arrows: bool) {
|
||||
self.is_selected = true;
|
||||
let arrows = if add_arrows {
|
||||
self.colors.magenta(" <↓↑> ")
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
match self.colors.palette.bg {
|
||||
PaletteColor::EightBit(byte) => {
|
||||
self.line = format!(
|
||||
"\u{1b}[48;5;{byte}m\u{1b}[K\r\u{1b}[48;5;{byte}m{}",
|
||||
"\u{1b}[48;5;{byte}m\u{1b}[K\u{1b}[48;5;{byte}m{arrows}{}",
|
||||
self.line
|
||||
);
|
||||
},
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
self.line = format!(
|
||||
"\u{1b}[48;2;{};{};{}m\u{1b}[K\r\u{1b}[48;2;{};{};{}m{}",
|
||||
"\u{1b}[48;2;{};{};{}m\u{1b}[K\u{1b}[48;2;{};{};{}m{arrows}{}",
|
||||
r, g, b, r, g, b, self.line
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn make_selected(&mut self, add_arrows: bool) {
|
||||
self.is_selected = true;
|
||||
let arrows = if add_arrows {
|
||||
self.colors.magenta("<←↓↑→>")
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
match self.colors.palette.bg {
|
||||
PaletteColor::EightBit(byte) => {
|
||||
self.line = format!(
|
||||
"\u{1b}[48;5;{byte}m\u{1b}[K\u{1b}[48;5;{byte}m{arrows}{}",
|
||||
self.line
|
||||
);
|
||||
},
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
self.line = format!(
|
||||
"\u{1b}[48;2;{};{};{}m\u{1b}[K\u{1b}[48;2;{};{};{}m{arrows}{}",
|
||||
r, g, b, r, g, b, self.line
|
||||
);
|
||||
},
|
||||
|
|
@ -475,151 +503,275 @@ pub fn minimize_lines(
|
|||
(start_index, anchor_index, end_index, line_count_to_remove)
|
||||
}
|
||||
|
||||
pub fn render_prompt(typing_session_name: bool, search_term: &str, colors: Colors) {
|
||||
if !typing_session_name {
|
||||
let prompt = colors.bold(&format!("> {}_", search_term));
|
||||
println!("\u{1b}[H{}\n", prompt);
|
||||
} else {
|
||||
println!("\n");
|
||||
}
|
||||
pub fn render_prompt(search_term: &str, colors: Colors, x: usize, y: usize) {
|
||||
let prompt = colors.green(&format!("Search:"));
|
||||
let search_term = colors.bold(&format!("{}_", search_term));
|
||||
println!("\u{1b}[{};{}H{} {}\n", y + 1, x, prompt, search_term);
|
||||
}
|
||||
|
||||
pub fn render_resurrection_toggle(cols: usize, resurrection_screen_is_active: bool) {
|
||||
pub fn render_screen_toggle(active_screen: ActiveScreen, x: usize, y: usize, max_cols: usize) {
|
||||
let key_indication_text = "<TAB>";
|
||||
let running_sessions_text = "Running";
|
||||
let exited_sessions_text = "Exited";
|
||||
let (new_session_text, running_sessions_text, exited_sessions_text) = if max_cols > 66 {
|
||||
("New Session", "Attach to Session", "Resurrect Session")
|
||||
} else {
|
||||
("New", "Attach", "Resurrect")
|
||||
};
|
||||
let key_indication_len = key_indication_text.chars().count() + 1;
|
||||
let first_ribbon_length = running_sessions_text.chars().count() + 4;
|
||||
let second_ribbon_length = exited_sessions_text.chars().count() + 4;
|
||||
let key_indication_x =
|
||||
cols.saturating_sub(key_indication_len + first_ribbon_length + second_ribbon_length);
|
||||
let first_ribbon_length = new_session_text.chars().count() + 4;
|
||||
let second_ribbon_length = running_sessions_text.chars().count() + 4;
|
||||
let third_ribbon_length = exited_sessions_text.chars().count() + 4;
|
||||
let total_len =
|
||||
key_indication_len + first_ribbon_length + second_ribbon_length + third_ribbon_length;
|
||||
let key_indication_x = x;
|
||||
let first_ribbon_x = key_indication_x + key_indication_len;
|
||||
let second_ribbon_x = first_ribbon_x + first_ribbon_length;
|
||||
let third_ribbon_x = second_ribbon_x + second_ribbon_length;
|
||||
let mut new_session_text = Text::new(new_session_text);
|
||||
let mut running_sessions_text = Text::new(running_sessions_text);
|
||||
let mut exited_sessions_text = Text::new(exited_sessions_text);
|
||||
match active_screen {
|
||||
ActiveScreen::NewSession => {
|
||||
new_session_text = new_session_text.selected();
|
||||
},
|
||||
ActiveScreen::AttachToSession => {
|
||||
running_sessions_text = running_sessions_text.selected();
|
||||
},
|
||||
ActiveScreen::ResurrectSession => {
|
||||
exited_sessions_text = exited_sessions_text.selected();
|
||||
},
|
||||
}
|
||||
print_text_with_coordinates(
|
||||
Text::new(key_indication_text).color_range(3, ..),
|
||||
key_indication_x,
|
||||
0,
|
||||
y,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if resurrection_screen_is_active {
|
||||
print_ribbon_with_coordinates(
|
||||
Text::new(running_sessions_text),
|
||||
first_ribbon_x,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
print_ribbon_with_coordinates(
|
||||
Text::new(exited_sessions_text).selected(),
|
||||
second_ribbon_x,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
print_ribbon_with_coordinates(new_session_text, first_ribbon_x, y, None, None);
|
||||
print_ribbon_with_coordinates(running_sessions_text, second_ribbon_x, y, None, None);
|
||||
print_ribbon_with_coordinates(exited_sessions_text, third_ribbon_x, y, None, None);
|
||||
}
|
||||
|
||||
pub fn render_new_session_block(
|
||||
new_session_info: &NewSessionInfo,
|
||||
colors: Colors,
|
||||
max_rows_of_new_session_block: usize,
|
||||
max_cols_of_new_session_block: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) {
|
||||
let enter = colors.magenta("<ENTER>");
|
||||
if new_session_info.entering_new_session_name() {
|
||||
let prompt = "New session name:";
|
||||
let long_instruction = "when done, blank for random";
|
||||
let new_session_name = new_session_info.name();
|
||||
if max_cols_of_new_session_block
|
||||
> prompt.width() + long_instruction.width() + new_session_name.width() + 15
|
||||
{
|
||||
println!(
|
||||
"\u{1b}[m{}{} {}_ ({} {})",
|
||||
format!("\u{1b}[{};{}H", y + 1, x + 1),
|
||||
colors.green(prompt),
|
||||
colors.orange(&new_session_name),
|
||||
enter,
|
||||
long_instruction,
|
||||
);
|
||||
} else {
|
||||
print_ribbon_with_coordinates(
|
||||
Text::new(running_sessions_text).selected(),
|
||||
first_ribbon_x,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
print_ribbon_with_coordinates(
|
||||
Text::new(exited_sessions_text),
|
||||
second_ribbon_x,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool, colors: Colors) {
|
||||
if is_searching {
|
||||
return;
|
||||
}
|
||||
let new_session_shortcut_text = "<Ctrl w>";
|
||||
let new_session_shortcut = colors.magenta(new_session_shortcut_text);
|
||||
let new_session = colors.bold("New session");
|
||||
let enter = colors.magenta("<ENTER>");
|
||||
match session_name {
|
||||
Some(session_name) => {
|
||||
println!(
|
||||
"\u{1b}[m > {}_ ({}, {} when done)",
|
||||
colors.orange(session_name),
|
||||
colors.bold("Type optional name"),
|
||||
enter
|
||||
"\u{1b}[m{}{} {}_ {}",
|
||||
format!("\u{1b}[{};{}H", y + 1, x + 1),
|
||||
colors.green(prompt),
|
||||
colors.orange(&new_session_name),
|
||||
enter,
|
||||
);
|
||||
}
|
||||
} else if new_session_info.entering_layout_search_term() {
|
||||
let new_session_name = if new_session_info.name().is_empty() {
|
||||
"<RANDOM>"
|
||||
} else {
|
||||
new_session_info.name()
|
||||
};
|
||||
let prompt = "New session name:";
|
||||
let long_instruction = "to correct";
|
||||
let esc = colors.magenta("<ESC>");
|
||||
if max_cols_of_new_session_block
|
||||
> prompt.width() + long_instruction.width() + new_session_name.width() + 15
|
||||
{
|
||||
println!(
|
||||
"\u{1b}[m{}{}: {} ({} to correct)",
|
||||
format!("\u{1b}[{};{}H", y + 1, x + 1),
|
||||
colors.green("New session name"),
|
||||
colors.orange(new_session_name),
|
||||
esc,
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"\u{1b}[m{}{}: {} {}",
|
||||
format!("\u{1b}[{};{}H", y + 1, x + 1),
|
||||
colors.green("New session name"),
|
||||
colors.orange(new_session_name),
|
||||
esc,
|
||||
);
|
||||
}
|
||||
render_layout_selection_list(
|
||||
new_session_info,
|
||||
max_rows_of_new_session_block.saturating_sub(1),
|
||||
max_cols_of_new_session_block,
|
||||
x,
|
||||
y + 1,
|
||||
);
|
||||
},
|
||||
None => {
|
||||
println!("\u{1b}[m > {new_session_shortcut} - {new_session}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_error(error_text: &str, rows: usize, columns: usize) {
|
||||
pub fn render_layout_selection_list(
|
||||
new_session_info: &NewSessionInfo,
|
||||
max_rows_of_new_session_block: usize,
|
||||
max_cols_of_new_session_block: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) {
|
||||
let layout_search_term = new_session_info.layout_search_term();
|
||||
let search_term_len = layout_search_term.width();
|
||||
let layout_indication_line = if max_cols_of_new_session_block > 73 + search_term_len {
|
||||
Text::new(format!(
|
||||
"New session layout: {}_ (Search and select from list, <ENTER> when done)",
|
||||
layout_search_term
|
||||
))
|
||||
.color_range(2, ..20 + search_term_len)
|
||||
.color_range(3, 20..20 + search_term_len)
|
||||
.color_range(3, 52 + search_term_len..59 + search_term_len)
|
||||
} else {
|
||||
Text::new(format!(
|
||||
"New session layout: {}_ <ENTER>",
|
||||
layout_search_term
|
||||
))
|
||||
.color_range(2, ..20 + search_term_len)
|
||||
.color_range(3, 20..20 + search_term_len)
|
||||
.color_range(3, 22 + search_term_len..)
|
||||
};
|
||||
print_text_with_coordinates(layout_indication_line, x, y + 1, None, None);
|
||||
println!();
|
||||
let mut table = Table::new();
|
||||
for (i, (layout_info, indices, is_selected)) in
|
||||
new_session_info.layouts_to_render().into_iter().enumerate()
|
||||
{
|
||||
let layout_name = layout_info.name();
|
||||
let layout_name_len = layout_name.width();
|
||||
let is_builtin = layout_info.is_builtin();
|
||||
if i > max_rows_of_new_session_block {
|
||||
break;
|
||||
} else {
|
||||
let mut layout_cell = if is_builtin {
|
||||
Text::new(format!("{} (built-in)", layout_name))
|
||||
.color_range(1, 0..layout_name_len)
|
||||
.color_range(0, layout_name_len + 1..)
|
||||
.color_indices(3, indices)
|
||||
} else {
|
||||
Text::new(format!("{}", layout_name))
|
||||
.color_range(1, ..)
|
||||
.color_indices(3, indices)
|
||||
};
|
||||
if is_selected {
|
||||
layout_cell = layout_cell.selected();
|
||||
}
|
||||
let arrow_cell = if is_selected {
|
||||
Text::new(format!("<↓↑>")).selected().color_range(3, ..)
|
||||
} else {
|
||||
Text::new(format!(" ")).color_range(3, ..)
|
||||
};
|
||||
table = table.add_styled_row(vec![arrow_cell, layout_cell]);
|
||||
}
|
||||
}
|
||||
print_table_with_coordinates(table, x, y + 3, None, None);
|
||||
}
|
||||
|
||||
pub fn render_error(error_text: &str, rows: usize, columns: usize, x: usize, y: usize) {
|
||||
print_text_with_coordinates(
|
||||
Text::new(format!("Error: {}", error_text)).color_range(3, ..),
|
||||
0,
|
||||
rows,
|
||||
x,
|
||||
y + rows,
|
||||
Some(columns),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render_renaming_session_screen(new_session_name: &str, rows: usize, columns: usize) {
|
||||
pub fn render_renaming_session_screen(
|
||||
new_session_name: &str,
|
||||
rows: usize,
|
||||
columns: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) {
|
||||
if rows == 0 || columns == 0 {
|
||||
return;
|
||||
}
|
||||
let prompt_text = "NEW NAME FOR CURRENT SESSION";
|
||||
let new_session_name = format!("{}_", new_session_name);
|
||||
let prompt_y_location = (rows / 2).saturating_sub(1);
|
||||
let session_name_y_location = (rows / 2) + 1;
|
||||
let prompt_x_location = columns.saturating_sub(prompt_text.chars().count()) / 2;
|
||||
let session_name_x_location = columns.saturating_sub(new_session_name.chars().count()) / 2;
|
||||
print_text_with_coordinates(
|
||||
Text::new(prompt_text).color_range(0, ..),
|
||||
prompt_x_location,
|
||||
prompt_y_location,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
print_text_with_coordinates(
|
||||
Text::new(new_session_name).color_range(3, ..),
|
||||
session_name_x_location,
|
||||
session_name_y_location,
|
||||
None,
|
||||
None,
|
||||
let text = Text::new(format!(
|
||||
"New name for current session: {}_ (<ENTER> when done)",
|
||||
new_session_name
|
||||
))
|
||||
.color_range(2, ..29)
|
||||
.color_range(
|
||||
3,
|
||||
33 + new_session_name.width()..40 + new_session_name.width(),
|
||||
);
|
||||
print_text_with_coordinates(text, x, y, None, None);
|
||||
}
|
||||
|
||||
pub fn render_controls_line(is_searching: bool, row: usize, max_cols: usize, colors: Colors) {
|
||||
let (arrows, navigate) = if is_searching {
|
||||
(colors.magenta("<↓↑>"), colors.bold("Navigate"))
|
||||
} else {
|
||||
(colors.magenta("<←↓↑→>"), colors.bold("Navigate and Expand"))
|
||||
};
|
||||
let rename = colors.magenta("<Ctrl r>");
|
||||
let rename_text = colors.bold("Rename session");
|
||||
let enter = colors.magenta("<ENTER>");
|
||||
let select = colors.bold("Switch to selected");
|
||||
let esc = colors.magenta("<ESC>");
|
||||
let to_hide = colors.bold("Hide");
|
||||
|
||||
if max_cols >= 104 {
|
||||
pub fn render_controls_line(
|
||||
active_screen: ActiveScreen,
|
||||
max_cols: usize,
|
||||
colors: Colors,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) {
|
||||
match active_screen {
|
||||
ActiveScreen::NewSession => {
|
||||
if max_cols >= 50 {
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}"
|
||||
"\u{1b}[m\u{1b}[{y};{x}H\u{1b}[1mHelp: Fill in the form to start a new session."
|
||||
);
|
||||
} else if max_cols >= 73 {
|
||||
}
|
||||
},
|
||||
ActiveScreen::AttachToSession => {
|
||||
let arrows = colors.magenta("<←↓↑→>");
|
||||
let navigate = colors.bold("Navigate");
|
||||
let select = colors.bold("Switch");
|
||||
let enter = colors.magenta("<ENTER>");
|
||||
let select = colors.bold("Attach");
|
||||
let rename = colors.magenta("<Ctrl r>");
|
||||
let rename_text = colors.bold("Rename");
|
||||
let disconnect = colors.magenta("<Ctrl x>");
|
||||
let disconnect_text = colors.bold("Disconnect others");
|
||||
let kill = colors.magenta("<Del>");
|
||||
let kill_text = colors.bold("Kill");
|
||||
let kill_all = colors.magenta("<Ctrl d>");
|
||||
let kill_all_text = colors.bold("Kill all");
|
||||
|
||||
if max_cols > 90 {
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}"
|
||||
"\u{1b}[m\u{1b}[{y};{x}HHelp: {rename} - {rename_text}, {disconnect} - {disconnect_text}, {kill} - {kill_text}, {kill_all} - {kill_all_text}"
|
||||
);
|
||||
} else if max_cols >= 28 {
|
||||
print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{rename}/{esc}");
|
||||
print!("\u{1b}[m\u{1b}[{y};{x}H{rename}/{disconnect}/{kill}/{kill_all}");
|
||||
}
|
||||
},
|
||||
ActiveScreen::ResurrectSession => {
|
||||
let arrows = colors.magenta("<↓↑>");
|
||||
let navigate = colors.bold("Navigate");
|
||||
let enter = colors.magenta("<ENTER>");
|
||||
let select = colors.bold("Resurrect");
|
||||
let del = colors.magenta("<DEL>");
|
||||
let del_text = colors.bold("Delete");
|
||||
let del_all = colors.magenta("<Ctrl d>");
|
||||
let del_all_text = colors.bold("Delete all");
|
||||
|
||||
if max_cols > 83 {
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{y};{x}HHelp: {arrows} - {navigate}, {enter} - {select}, {del} - {del_text}, {del_all} - {del_all_text}"
|
||||
);
|
||||
} else if max_cols >= 28 {
|
||||
print!("\u{1b}[m\u{1b}[{y};{x}H{arrows}/{enter}/{del}/{del_all}");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod components;
|
||||
pub mod welcome_screen;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
use crate::session_list::{SelectedIndex, SessionList};
|
||||
|
|
@ -29,7 +30,7 @@ macro_rules! render_assets {
|
|||
if $selected_index.is_some() && !$has_deeper_selected_assets {
|
||||
let mut selected_asset: LineToRender =
|
||||
selected_asset.as_line_to_render(current_index, $max_cols, $colors);
|
||||
selected_asset.make_selected();
|
||||
selected_asset.make_selected(true);
|
||||
selected_asset.add_truncated_results(truncated_result_count_above);
|
||||
if anchor_asset_index + 1 >= end_index {
|
||||
// no more results below, let's add the more indication if we need to
|
||||
|
|
@ -76,8 +77,10 @@ impl SessionList {
|
|||
if lines_to_render.len() + result.lines_to_render() <= max_rows {
|
||||
let mut result_lines = result.render(max_cols);
|
||||
if Some(i) == self.selected_search_index {
|
||||
let mut render_arrows = true;
|
||||
for line_to_render in result_lines.iter_mut() {
|
||||
line_to_render.make_selected();
|
||||
line_to_render.make_selected_as_search(render_arrows);
|
||||
render_arrows = false; // only render arrows on the first search result
|
||||
}
|
||||
}
|
||||
lines_to_render.append(&mut result_lines);
|
||||
|
|
|
|||
168
default-plugins/session-manager/src/ui/welcome_screen.rs
Normal file
168
default-plugins/session-manager/src/ui/welcome_screen.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
static BANNER: &str = "
|
||||
██╗ ██╗██╗ ███████╗██████╗ ██████╗ ███╗ ███╗ ███████╗███████╗██╗ ██╗ ██╗ ██╗██╗
|
||||
██║ ██║██║ ██╔════╝██╔══██╗██╔═══██╗████╗ ████║ ╚══███╔╝██╔════╝██║ ██║ ██║ ██║██║
|
||||
███████║██║ █████╗ ██████╔╝██║ ██║██╔████╔██║ ███╔╝ █████╗ ██║ ██║ ██║ ██║██║
|
||||
██╔══██║██║ ██╔══╝ ██╔══██╗██║ ██║██║╚██╔╝██║ ███╔╝ ██╔══╝ ██║ ██║ ██║██ ██║╚═╝
|
||||
██║ ██║██║ ██║ ██║ ██║╚██████╔╝██║ ╚═╝ ██║ ███████╗███████╗███████╗███████╗██║╚█████╔╝██╗
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚══════╝╚═╝ ╚════╝ ╚═╝
|
||||
";
|
||||
|
||||
static SMALL_BANNER: &str = "
|
||||
██╗ ██╗██╗ ██╗
|
||||
██║ ██║██║ ██║
|
||||
███████║██║ ██║
|
||||
██╔══██║██║ ╚═╝
|
||||
██║ ██║██║ ██╗
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝
|
||||
";
|
||||
|
||||
static MEDIUM_BANNER: &str = "
|
||||
██╗ ██╗██╗ ████████╗██╗ ██╗███████╗██████╗ ███████╗ ██╗
|
||||
██║ ██║██║ ╚══██╔══╝██║ ██║██╔════╝██╔══██╗██╔════╝ ██║
|
||||
███████║██║ ██║ ███████║█████╗ ██████╔╝█████╗ ██║
|
||||
██╔══██║██║ ██║ ██╔══██║██╔══╝ ██╔══██╗██╔══╝ ╚═╝
|
||||
██║ ██║██║ ██║ ██║ ██║███████╗██║ ██║███████╗ ██╗
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝
|
||||
";
|
||||
|
||||
pub fn render_banner(x: usize, y: usize, rows: usize, cols: usize) {
|
||||
if rows >= 8 {
|
||||
if cols > 100 {
|
||||
println!("\u{1b}[{}H", y + rows.saturating_sub(8) / 2);
|
||||
for line in BANNER.lines() {
|
||||
println!("\u{1b}[{}C{}", x.saturating_sub(1), line);
|
||||
}
|
||||
} else if cols > 63 {
|
||||
println!("\u{1b}[{}H", y + rows.saturating_sub(8) / 2);
|
||||
let x = (cols.saturating_sub(63) as f64 / 2.0) as usize;
|
||||
for line in MEDIUM_BANNER.lines() {
|
||||
println!("\u{1b}[{}C{}", x, line);
|
||||
}
|
||||
} else {
|
||||
println!("\u{1b}[{}H", y + rows.saturating_sub(8) / 2);
|
||||
let x = (cols.saturating_sub(18) as f64 / 2.0) as usize;
|
||||
for line in SMALL_BANNER.lines() {
|
||||
println!("\u{1b}[{}C{}", x, line);
|
||||
}
|
||||
}
|
||||
} else if rows > 2 {
|
||||
println!(
|
||||
"\u{1b}[{};{}H\u{1b}[1mHi from Zellij!",
|
||||
(y + rows / 2) + 1,
|
||||
(x + cols.saturating_sub(15) / 2).saturating_sub(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_welcome_boundaries(rows: usize, cols: usize) {
|
||||
let width_of_main_menu = std::cmp::min(cols, 101);
|
||||
let has_room_for_logos = cols.saturating_sub(width_of_main_menu) > 100;
|
||||
let left_boundary_x = (cols.saturating_sub(width_of_main_menu) as f64 / 2.0).floor() as usize;
|
||||
let right_boundary_x = left_boundary_x + width_of_main_menu;
|
||||
let y_starting_point = rows.saturating_sub(15) / 2;
|
||||
let middle_row =
|
||||
(y_starting_point + rows.saturating_sub(y_starting_point) / 2).saturating_sub(1);
|
||||
for i in y_starting_point..rows {
|
||||
if i == middle_row {
|
||||
if has_room_for_logos {
|
||||
print!("\u{1b}[{};{}H┤", i + 1, left_boundary_x + 1);
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{};{}H├\u{1b}[K",
|
||||
i + 1,
|
||||
right_boundary_x + 1
|
||||
);
|
||||
print!("\u{1b}[{};{}H", i + 1, left_boundary_x.saturating_sub(9));
|
||||
for _ in 0..10 {
|
||||
print!("─");
|
||||
}
|
||||
print!("\u{1b}[{};{}H", i + 1, right_boundary_x + 2);
|
||||
for _ in 0..10 {
|
||||
print!("─");
|
||||
}
|
||||
} else {
|
||||
print!("\u{1b}[{};{}H│", i + 1, left_boundary_x + 1);
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{};{}H│\u{1b}[K",
|
||||
i + 1,
|
||||
right_boundary_x + 1
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if i == y_starting_point {
|
||||
print!("\u{1b}[{};{}H┌", i + 1, left_boundary_x + 1);
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{};{}H┐\u{1b}[K",
|
||||
i + 1,
|
||||
right_boundary_x + 1
|
||||
);
|
||||
} else if i == rows.saturating_sub(1) {
|
||||
print!("\u{1b}[{};{}H└", i + 1, left_boundary_x + 1);
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{};{}H┘\u{1b}[K",
|
||||
i + 1,
|
||||
right_boundary_x + 1
|
||||
);
|
||||
} else {
|
||||
print!("\u{1b}[{};{}H│", i + 1, left_boundary_x + 1);
|
||||
print!(
|
||||
"\u{1b}[m\u{1b}[{};{}H│\u{1b}[K",
|
||||
i + 1,
|
||||
right_boundary_x + 1
|
||||
); // this includes some
|
||||
// ANSI magic to delete
|
||||
// everything after this
|
||||
// boundary in order to
|
||||
// fix some rendering
|
||||
// bugs in the legacy
|
||||
// components of this
|
||||
// plugin
|
||||
}
|
||||
}
|
||||
}
|
||||
if rows.saturating_sub(y_starting_point) > 25 && has_room_for_logos {
|
||||
for (i, line) in LOGO.lines().enumerate() {
|
||||
print!(
|
||||
"\u{1b}[{};{}H{}",
|
||||
middle_row.saturating_sub(12) + i,
|
||||
0,
|
||||
line
|
||||
);
|
||||
}
|
||||
for (i, line) in LOGO.lines().enumerate() {
|
||||
print!(
|
||||
"\u{1b}[{};{}H{}",
|
||||
middle_row.saturating_sub(12) + i,
|
||||
cols.saturating_sub(47),
|
||||
line
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
static LOGO: &str = r#" [38;2;0;0;0m
|
||||
[38;2;0;0;0m
|
||||
[38;2;0;0;0m [38;2;160;186;139m_[38;2;142;164;125my[38;2;148;171;129m$ [38;2;79;88;77my[38;2;151;175;132m@[38;2;131;151;117mg[38;2;163;189;141m_
|
||||
[38;2;0;0;0m [38;2;114;130;103my[38;2;156;181;136ma[38;2;159;184;138m@[38;2;163;189;141m@@@[38;2;73;82;73mL[38;2;103;117;95m4[38;2;163;189;141m@@@@[38;2;160;185;138mg[38;2;123;141;110my[38;2;110;125;100m_
|
||||
[38;2;0;0;0m [38;2;82;92;79mu[38;2;163;189;141m@@@@@@[38;2;160;185;138mF [38;2;85;96;82m"[38;2;143;165;126m@[38;2;163;189;141m@@@@@@[38;2;148;171;129m@[38;2;145;167;127my[38;2;162;188;140m_
|
||||
[38;2;0;0;0m [38;2;190;97;107m_[38;2;165;84;95ma[38;2;174;88;99m@[38;2;190;97;107m@[38;2;117;59;73m, [38;2;159;184;138m@[38;2;163;189;141m@[38;2;160;186;139m@[38;2;147;170;129mP[38;2;163;189;141m~[38;2;66;73;67m` [38;2;126;159;190m_[38;2;118;149;179m_ [38;2;106;121;97m~[38;2;120;138;108mT[38;2;135;156;120m@[38;2;163;189;141m@@@@@@[38;2;162;188;140m@[38;2;132;152;117mg
|
||||
[38;2;0;0;0m [38;2;130;66;79m_[38;2;144;73;86my[38;2;187;95;105mg[38;2;189;96;106m@[38;2;190;97;107m@@@@[38;2;174;89;99m$ [38;2;118;136;107m"[38;2;128;147;114m~ [38;2;89;110;138m_[38;2;97;120;149my[38;2;124;156;187mg[38;2;125;158;189m@[38;2;126;159;190m@@[38;2;123;156;186m@[38;2;112;140;170mg[38;2;84;103;131my [38;2;120;138;108m`[38;2;162;188;140m~[38;2;128;148;115mP[38;2;146;168;128mR[38;2;135;155;119m@[38;2;139;160;122mF[38;2;158;183;137m~[38;2;233;202;138m_[38;2;196;170;119my[38;2;234;203;139mg[38;2;217;188;130mg[38;2;180;155;111my[38;2;166;143;104m_
|
||||
[38;2;0;0;0m [38;2;168;85;97my[38;2;174;89;99m$[38;2;190;97;107m@@@@@@[38;2;184;94;104mP[38;2;139;70;83mF [38;2;125;158;189m_[38;2;112;141;171my[38;2;116;146;176m$[38;2;126;159;190m@@@@@@@@@@[38;2;125;158;189mg[38;2;100;125;153my[38;2;101;127;155m_ [38;2;206;179;125m4[38;2;232;202;138m@[38;2;234;203;139m@@@@@[38;2;212;183;127m@[38;2;206;178;124my[38;2;145;124;93m_
|
||||
[38;2;0;0;0m [38;2;189;96;106mg[38;2;190;97;107m@@@@[38;2;178;91;101m@[38;2;168;85;97mF[38;2;187;95;105m~ [38;2;65;79;104m_[38;2;91;113;141my[38;2;120;151;182mg[38;2;124;157;188m@[38;2;126;159;190m@@@@@@@@@@@@@@@@[38;2;119;149;180m@[38;2;101;126;155mg[38;2;126;159;190m_ [38;2;200;173;121m~[38;2;180;156;111mF[38;2;217;188;130m@[38;2;234;203;139m@@@@[38;2;196;169;119m@
|
||||
[38;2;0;0;0m [38;2;183;93;104m$[38;2;190;97;107m@@@[38;2;189;97;107mF [38;2;103;128;157my[38;2;126;159;190mg@@@@@@@@@@@@@@@@@@@@@@@@[38;2;122;154;185mg[38;2;75;92;118my [38;2;206;178;124m9[38;2;234;203;139m@[38;2;199;172;121m@[38;2;193;167;118mF[38;2;106;90;73m"
|
||||
[38;2;0;0;0m [38;2;182;93;103m$[38;2;190;97;107m@@@[38;2;130;66;79m$ [38;2;79;97;124m4[38;2;126;159;190m@@@@@[38;2;112;140;170m@[38;2;126;159;190m~[38;2;104;130;159m@[38;2;126;159;190m@@@@@@@@@@@@@@@@@@@@ [38;2;82;68;61m"[38;2;154;132;98m_[38;2;183;157;112my[38;2;226;196;135mg[38;2;208;180;126m$
|
||||
[38;2;0;0;0m [38;2;178;90;101m$[38;2;190;97;107m@@@[38;2;131;66;79m$ [38;2;80;98;125m4[38;2;126;159;190m@@@@[38;2;96;120;148m@ [38;2;108;136;165m~[38;2;105;132;161m@[38;2;126;159;190m@@@@@@@@@@@@@@@@@@ [38;2;168;144;105mg[38;2;234;203;139m@@@[38;2;233;202;138m@
|
||||
[38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;131;66;80m$ [38;2;80;98;125m4[38;2;126;159;190m@@@@@[38;2;118;148;178m@[38;2;91;113;141my [38;2;109;136;166m~[38;2;107;134;163m@[38;2;126;159;190m@@@@@@@@@@@@@@@@ [38;2;182;157;112m$[38;2;234;203;139m@@[38;2;233;202;138m@[38;2;224;194;134mF
|
||||
[38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;132;67;80m$ [38;2;80;98;125m4[38;2;126;159;190m@@@@@@@[38;2;116;146;176m@[38;2;79;98;124my [38;2;109;137;166m~[38;2;112;141;171m@[38;2;126;159;190m@@@@@@@@@@@@@@ [38;2;140;120;90m`[38;2;189;163;116m~[38;2;234;203;139m_[38;2;161;138;101my[38;2;182;157;112m_
|
||||
[38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;134;68;81m$ [38;2;80;99;125m4[38;2;126;159;190m@@@@@@@[38;2;114;143;173mP[38;2;113;142;172m` [38;2;56;67;92m_[38;2;98;122;151mg[38;2;125;158;189m@[38;2;126;159;190m@@@@@@@@@@@@@@ [38;2;148;127;95my[38;2;234;203;139m@@@[38;2;213;184;128m@
|
||||
[38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;139;70;83m$ [38;2;80;99;126m4[38;2;126;159;190m@@@@@[38;2;117;147;177mP[38;2;119;150;180m` [38;2;52;62;86m_[38;2;109;137;166my[38;2;125;158;189m@[38;2;126;159;190m@@@@@@@@@@@@@@@@ [38;2;163;140;102m4[38;2;234;203;139m@@@[38;2;216;187;129m@
|
||||
[38;2;0;0;0m [38;2;174;88;99m$[38;2;190;97;107m@@@[38;2;143;73;85m$ [38;2;80;99;126m4[38;2;126;159;190m@@@@[38;2;104;129;158m$[38;2;54;64;89m_ [38;2;108;136;165my[38;2;125;158;189m@[38;2;126;159;190m@@@[38;2;110;139;168m@[38;2;126;159;190m [38;2;114;144;174m$[38;2;126;159;190m@@@@ [38;2;136;116;88m4[38;2;234;203;139m@@@[38;2;218;189;131m@
|
||||
[38;2;0;0;0m [38;2;174;88;99m$[38;2;190;97;107m@@@[38;2;145;73;86m$ [38;2;81;99;126m4[38;2;126;159;190m@@@@@@[38;2;113;142;172mg[38;2;124;157;188m@[38;2;126;159;190m@@@@@[38;2;125;158;188m@[38;2;107;135;164m@[38;2;107;134;163m@@@@@@@[38;2;108;136;165m@[38;2;121;153;184m@[38;2;126;159;190m@@@@ [38;2;198;171;120m~[38;2;208;180;125m@[38;2;234;203;139m@[38;2;220;191;132m@
|
||||
[38;2;0;0;0m [38;2;174;89;99m$[38;2;190;97;107m@@[38;2;184;93;104mP [38;2;91;113;141m7[38;2;109;137;166mR[38;2;126;159;190m@@@@@@@@@@@@@@@@@@@@@@@[38;2;123;155;186m@[38;2;111;139;169mP[38;2;84;104;131m~ [38;2;117;134;106m_[38;2;129;149;115m@[38;2;140;162;124mg[38;2;83;93;80m_[38;2;174;150;108m7[38;2;222;192;133m@
|
||||
[38;2;0;0;0m [38;2;145;73;86m~[38;2;168;85;97m~[38;2;140;162;124m_[38;2;125;144;112my[38;2;158;183;137mg[38;2;157;182;137m@[38;2;146;169;128mg[38;2;111;127;101my[38;2;61;67;64m_ [38;2;115;145;175m~[38;2;91;113;141m5[38;2;121;152;182m@[38;2;126;159;190m@@@@@@@@@@@@@@@@[38;2;108;136;165m@[38;2;105;131;160mF[38;2;98;122;151m~ [38;2;145;167;127m_[38;2;130;150;116my[38;2;162;188;140mg[38;2;163;189;141m@@@@[38;2;137;158;121my
|
||||
[38;2;0;0;0m [38;2;140;161;123mR[38;2;163;189;141m@@@@@@[38;2;113;129;102m@ [38;2;82;101;128m~[38;2;126;159;190m~[38;2;101;126;154m@[38;2;126;159;190m@@@@@@@@@[38;2;125;158;189m@[38;2;118;148;178mP[38;2;126;159;190m~[38;2;84;103;130m` [38;2;112;127;102my[38;2;153;177;133ma[38;2;154;179;134m@[38;2;163;189;141m@@@@@@[38;2;132;152;117m@[38;2;117;134;106mF
|
||||
[38;2;0;0;0m [38;2;147;170;129m~[38;2;116;132;105m5[38;2;153;177;133m@[38;2;163;189;141m@@@ [38;2;117;134;106m4[38;2;160;185;138m@[38;2;144;166;126mg[38;2;107;122;98my [38;2;62;74;99m`[38;2;122;154;184m~[38;2;109;137;167m4[38;2;124;157;188m@[38;2;126;159;190m@@[38;2;114;143;173m@[38;2;109;137;166mF[38;2;115;145;175m~ [38;2;122;140;110my[38;2;155;179;135m@[38;2;163;189;141m@@@@@[38;2;160;186;139m@[38;2;146;169;128mP[38;2;163;189;141m~[38;2;61;67;63m`
|
||||
[38;2;0;0;0m [38;2;162;188;140m`[38;2;163;189;141m~[38;2;143;165;126mP [38;2;99;112;92m4[38;2;163;189;141m@@@@[38;2;162;187;140mg[38;2;127;145;113my[38;2;120;137;108m_ [38;2;105;131;160m`[38;2;104;130;159m` [38;2;133;154;118m_[38;2;130;149;116my[38;2;126;145;113mr [38;2;137;158;121ma[38;2;163;189;141m@@@@[38;2;144;167;127m@[38;2;139;160;122mF[38;2;138;159;122m~
|
||||
[38;2;0;0;0m [38;2;116;132;105m5[38;2;154;178;134m@[38;2;163;189;141m@@@@@[38;2;152;176;133m@[38;2;163;189;141mgg[38;2;147;169;128m@[38;2;161;187;140m@[38;2;148;171;129m~[38;2;103;117;95m_[38;2;162;188;140mg[38;2;163;189;141m@@[38;2;122;140;110m@[38;2;163;189;141m~[38;2;140;161;123m`
|
||||
[38;2;0;0;0m [38;2;95;108;89m~[38;2;163;189;141m~[38;2;128;147;114m@[38;2;163;189;141m@@@@@[38;2;145;168;127m@[38;2;82;92;79m^[38;2;129;149;115my[38;2;155;180;135m@[38;2;145;168;127mF[38;2;160;185;139m~
|
||||
[38;2;0;0;0m [38;2;71;78;71m`[38;2;157;181;136m~[38;2;140;162;124m4[38;2;149;173;131m@[38;2;151;175;132mF [38;2;108;124;99m~
|
||||
[38;2;0;0;0m
|
||||
[38;2;0;0;0m "#;
|
||||
|
|
@ -18,16 +18,17 @@ use zellij_client::{
|
|||
use zellij_server::{os_input_output::get_server_os_input, start_server as start_server_impl};
|
||||
use zellij_utils::{
|
||||
cli::{CliArgs, Command, SessionCommand, Sessions},
|
||||
data::ConnectToSession,
|
||||
data::{ConnectToSession, LayoutInfo},
|
||||
envs,
|
||||
input::{
|
||||
actions::Action,
|
||||
config::{Config, ConfigError},
|
||||
layout::Layout,
|
||||
options::Options,
|
||||
},
|
||||
miette::{Report, Result},
|
||||
nix,
|
||||
setup::Setup,
|
||||
setup::{find_default_config_dir, get_layout_dir, Setup},
|
||||
};
|
||||
|
||||
pub(crate) use crate::sessions::list_sessions;
|
||||
|
|
@ -383,7 +384,8 @@ fn attach_with_session_name(
|
|||
pub(crate) fn start_client(opts: CliArgs) {
|
||||
// look for old YAML config/layout/theme files and convert them to KDL
|
||||
convert_old_yaml_files(&opts);
|
||||
let (config, layout, config_options) = match Setup::from_cli_args(&opts) {
|
||||
let (config, layout, config_options, config_without_layout, config_options_without_layout) =
|
||||
match Setup::from_cli_args(&opts) {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
if let ConfigError::KdlError(error) = e {
|
||||
|
|
@ -399,8 +401,8 @@ pub(crate) fn start_client(opts: CliArgs) {
|
|||
let os_input = get_os_input(get_client_os_input);
|
||||
loop {
|
||||
let os_input = os_input.clone();
|
||||
let config = config.clone();
|
||||
let layout = layout.clone();
|
||||
let mut config = config.clone();
|
||||
let mut layout = layout.clone();
|
||||
let mut config_options = config_options.clone();
|
||||
let mut opts = opts.clone();
|
||||
let mut is_a_reconnect = false;
|
||||
|
|
@ -423,6 +425,43 @@ pub(crate) fn start_client(opts: CliArgs) {
|
|||
opts.session = None;
|
||||
config_options.attach_to_session = None;
|
||||
}
|
||||
|
||||
if let Some(reconnect_layout) = &reconnect_to_session.layout {
|
||||
let layout_dir = config.options.layout_dir.clone().or_else(|| {
|
||||
get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir))
|
||||
});
|
||||
let new_session_layout = match reconnect_layout {
|
||||
LayoutInfo::BuiltIn(layout_name) => Layout::from_default_assets(
|
||||
&PathBuf::from(layout_name),
|
||||
layout_dir.clone(),
|
||||
config_without_layout.clone(),
|
||||
),
|
||||
LayoutInfo::File(layout_name) => Layout::from_path_or_default(
|
||||
Some(&PathBuf::from(layout_name)),
|
||||
layout_dir.clone(),
|
||||
config_without_layout.clone(),
|
||||
),
|
||||
};
|
||||
match new_session_layout {
|
||||
Ok(new_session_layout) => {
|
||||
// here we make sure to override both the layout and the config, but we do
|
||||
// this with an instance of the config before it was merged with the
|
||||
// layout configuration of the previous iteration of the loop, since we do
|
||||
// not want it to mix with the config of this session
|
||||
let (new_layout, new_layout_config) = new_session_layout;
|
||||
layout = new_layout;
|
||||
let mut new_config = config_without_layout.clone();
|
||||
let _ = new_config.merge(new_layout_config.clone());
|
||||
config = new_config;
|
||||
config_options =
|
||||
config_options_without_layout.merge(new_layout_config.options);
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse new session layout: {:?}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
is_a_reconnect = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ pub(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> {
|
|||
session_layout_cache_file_name(&folder_name.display().to_string());
|
||||
let raw_layout = match std::fs::read_to_string(&layout_file_name) {
|
||||
Ok(raw_layout) => raw_layout,
|
||||
Err(e) => {
|
||||
log::error!("Failed to read resurrection layout file: {:?}", e);
|
||||
Err(_e) => {
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
|
@ -61,13 +60,7 @@ pub(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> {
|
|||
.and_then(|metadata| metadata.created())
|
||||
{
|
||||
Ok(created) => Some(created),
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to read created stamp of resurrection file: {:?}",
|
||||
e
|
||||
);
|
||||
None
|
||||
},
|
||||
Err(_e) => None,
|
||||
};
|
||||
let layout = match Layout::from_kdl(
|
||||
&raw_layout,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ use zellij_utils::{
|
|||
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
|
||||
data::{ConnectToSession, Event, PluginCapabilities},
|
||||
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
|
||||
home::get_default_data_dir,
|
||||
home::{default_layout_dir, get_default_data_dir},
|
||||
input::{
|
||||
command::{RunCommand, TerminalAction},
|
||||
get_mode_info,
|
||||
|
|
@ -93,6 +93,7 @@ pub enum ServerInstruction {
|
|||
pipe_id: String,
|
||||
client_id: ClientId,
|
||||
},
|
||||
DisconnectAllClientsExcept(ClientId),
|
||||
}
|
||||
|
||||
impl From<&ServerInstruction> for ServerContext {
|
||||
|
|
@ -117,6 +118,9 @@ impl From<&ServerInstruction> for ServerContext {
|
|||
ServerInstruction::AssociatePipeWithClient { .. } => {
|
||||
ServerContext::AssociatePipeWithClient
|
||||
},
|
||||
ServerInstruction::DisconnectAllClientsExcept(..) => {
|
||||
ServerContext::DisconnectAllClientsExcept
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,6 +137,7 @@ pub(crate) struct SessionMetaData {
|
|||
pub client_attributes: ClientAttributes,
|
||||
pub default_shell: Option<TerminalAction>,
|
||||
pub layout: Box<Layout>,
|
||||
pub config_options: Box<Options>,
|
||||
screen_thread: Option<thread::JoinHandle<()>>,
|
||||
pty_thread: Option<thread::JoinHandle<()>>,
|
||||
plugin_thread: Option<thread::JoinHandle<()>>,
|
||||
|
|
@ -650,6 +655,21 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
}
|
||||
break;
|
||||
},
|
||||
ServerInstruction::DisconnectAllClientsExcept(client_id) => {
|
||||
let client_ids: Vec<ClientId> = session_state
|
||||
.read()
|
||||
.unwrap()
|
||||
.client_ids()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|c| c != &client_id)
|
||||
.collect();
|
||||
for client_id in client_ids {
|
||||
let _ = os_input
|
||||
.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal));
|
||||
remove_client!(client_id, os_input, session_state);
|
||||
}
|
||||
},
|
||||
ServerInstruction::DetachSession(client_ids) => {
|
||||
for client_id in client_ids {
|
||||
let _ = os_input
|
||||
|
|
@ -749,7 +769,16 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
session_state
|
||||
);
|
||||
},
|
||||
ServerInstruction::SwitchSession(connect_to_session, client_id) => {
|
||||
ServerInstruction::SwitchSession(mut connect_to_session, client_id) => {
|
||||
let layout_dir = session_data
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.and_then(|c| c.config_options.layout_dir.clone())
|
||||
.or_else(|| default_layout_dir());
|
||||
if let Some(layout_dir) = layout_dir {
|
||||
connect_to_session.apply_layout_dir(&layout_dir);
|
||||
}
|
||||
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
|
||||
session_data
|
||||
.write()
|
||||
|
|
@ -906,6 +935,7 @@ fn init_session(
|
|||
let client_attributes_clone = client_attributes.clone();
|
||||
let debug = opts.debug;
|
||||
let layout = layout.clone();
|
||||
let config_options = config_options.clone();
|
||||
move || {
|
||||
screen_thread_main(
|
||||
screen_bus,
|
||||
|
|
@ -1006,6 +1036,7 @@ fn init_session(
|
|||
default_shell,
|
||||
client_attributes,
|
||||
layout,
|
||||
config_options: config_options.clone(),
|
||||
screen_thread: Some(screen_thread),
|
||||
pty_thread: Some(pty_thread),
|
||||
plugin_thread: Some(plugin_thread),
|
||||
|
|
|
|||
|
|
@ -5970,3 +5970,243 @@ pub fn pipe_message_to_plugin_plugin_command() {
|
|||
});
|
||||
assert_snapshot!(format!("{:#?}", plugin_bytes_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn switch_session_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let cache_path = plugin_host_folder.join("permissions_test.kdl");
|
||||
let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
|
||||
create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
configuration: Default::default(),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::Exit,
|
||||
screen_receiver,
|
||||
1,
|
||||
&PermissionType::ChangeApplicationState,
|
||||
cache_path,
|
||||
plugin_thread_sender,
|
||||
client_id
|
||||
);
|
||||
let received_server_instruction = Arc::new(Mutex::new(vec![]));
|
||||
let server_thread = log_actions_in_thread!(
|
||||
received_server_instruction,
|
||||
ServerInstruction::SwitchSession,
|
||||
server_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
false,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
None,
|
||||
client_id,
|
||||
size,
|
||||
None,
|
||||
false,
|
||||
));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('5')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
teardown();
|
||||
server_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
let switch_session_event = received_server_instruction
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|i| {
|
||||
if let ServerInstruction::SwitchSession(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", switch_session_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn switch_session_with_layout_plugin_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let cache_path = plugin_host_folder.join("permissions_test.kdl");
|
||||
let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
|
||||
create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
configuration: Default::default(),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::Exit,
|
||||
screen_receiver,
|
||||
1,
|
||||
&PermissionType::ChangeApplicationState,
|
||||
cache_path,
|
||||
plugin_thread_sender,
|
||||
client_id
|
||||
);
|
||||
let received_server_instruction = Arc::new(Mutex::new(vec![]));
|
||||
let server_thread = log_actions_in_thread!(
|
||||
received_server_instruction,
|
||||
ServerInstruction::SwitchSession,
|
||||
server_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
false,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
None,
|
||||
client_id,
|
||||
size,
|
||||
None,
|
||||
false,
|
||||
));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('7')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
teardown();
|
||||
server_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
let switch_session_event = received_server_instruction
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|i| {
|
||||
if let ServerInstruction::SwitchSession(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", switch_session_event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn disconnect_other_clients_plugins_command() {
|
||||
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||
// destructor removes the directory
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let cache_path = plugin_host_folder.join("permissions_test.kdl");
|
||||
let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
|
||||
create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
|
||||
let plugin_should_float = Some(false);
|
||||
let plugin_title = Some("test_plugin".to_owned());
|
||||
let run_plugin = RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
configuration: Default::default(),
|
||||
};
|
||||
let tab_index = 1;
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
|
||||
received_screen_instructions,
|
||||
ScreenInstruction::Exit,
|
||||
screen_receiver,
|
||||
1,
|
||||
&PermissionType::ChangeApplicationState,
|
||||
cache_path,
|
||||
plugin_thread_sender,
|
||||
client_id
|
||||
);
|
||||
let received_server_instruction = Arc::new(Mutex::new(vec![]));
|
||||
let server_thread = log_actions_in_thread!(
|
||||
received_server_instruction,
|
||||
ServerInstruction::DisconnectAllClientsExcept,
|
||||
server_receiver,
|
||||
1
|
||||
);
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
plugin_should_float,
|
||||
false,
|
||||
plugin_title,
|
||||
run_plugin,
|
||||
tab_index,
|
||||
None,
|
||||
client_id,
|
||||
size,
|
||||
None,
|
||||
false,
|
||||
));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
Event::Key(Key::Ctrl('6')), // this triggers the enent in the fixture plugin
|
||||
)]));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
teardown();
|
||||
server_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||
let switch_session_event = received_server_instruction
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|i| {
|
||||
if let ServerInstruction::DisconnectAllClientsExcept(..) = i {
|
||||
Some(i.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
assert_snapshot!(format!("{:#?}", switch_session_event));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 6131
|
||||
expression: "format!(\"{:#?}\", switch_session_event)"
|
||||
---
|
||||
Some(
|
||||
DisconnectAllClientsExcept(
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 6051
|
||||
expression: "format!(\"{:#?}\", switch_session_event)"
|
||||
---
|
||||
Some(
|
||||
SwitchSession(
|
||||
ConnectToSession {
|
||||
name: Some(
|
||||
"my_new_session",
|
||||
),
|
||||
tab_position: None,
|
||||
pane_id: None,
|
||||
layout: None,
|
||||
},
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 6131
|
||||
expression: "format!(\"{:#?}\", switch_session_event)"
|
||||
---
|
||||
Some(
|
||||
SwitchSession(
|
||||
ConnectToSession {
|
||||
name: Some(
|
||||
"my_other_new_session",
|
||||
),
|
||||
tab_position: None,
|
||||
pane_id: None,
|
||||
layout: Some(
|
||||
BuiltIn(
|
||||
"compact",
|
||||
),
|
||||
),
|
||||
},
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
|
@ -18,10 +18,14 @@ use std::{
|
|||
use wasmer::{imports, AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Imports};
|
||||
use wasmer_wasi::WasiEnv;
|
||||
use zellij_utils::data::{
|
||||
CommandType, ConnectToSession, HttpVerb, MessageToPlugin, PermissionStatus, PermissionType,
|
||||
PluginPermission,
|
||||
CommandType, ConnectToSession, HttpVerb, LayoutInfo, MessageToPlugin, PermissionStatus,
|
||||
PermissionType, PluginPermission,
|
||||
};
|
||||
use zellij_utils::input::permission::PermissionCache;
|
||||
use zellij_utils::{
|
||||
interprocess::local_socket::LocalSocketStream,
|
||||
ipc::{ClientToServerMsg, IpcSenderWithContext},
|
||||
};
|
||||
|
||||
use url::Url;
|
||||
|
||||
|
|
@ -225,6 +229,7 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
|
|||
connect_to_session.name,
|
||||
connect_to_session.tab_position,
|
||||
connect_to_session.pane_id,
|
||||
connect_to_session.layout,
|
||||
)?,
|
||||
PluginCommand::DeleteDeadSession(session_name) => {
|
||||
delete_dead_session(session_name)?
|
||||
|
|
@ -252,6 +257,8 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
|
|||
cli_pipe_output(env, pipe_name, output)?
|
||||
},
|
||||
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
|
||||
PluginCommand::DisconnectOtherClients => disconnect_other_clients(env),
|
||||
PluginCommand::KillSessions(session_list) => kill_sessions(session_list),
|
||||
},
|
||||
(PermissionStatus::Denied, permission) => {
|
||||
log::error!(
|
||||
|
|
@ -900,6 +907,7 @@ fn switch_session(
|
|||
session_name: Option<String>,
|
||||
tab_position: Option<usize>,
|
||||
pane_id: Option<(u32, bool)>,
|
||||
layout: Option<LayoutInfo>,
|
||||
) -> Result<()> {
|
||||
// pane_id is (id, is_plugin)
|
||||
let err_context = || format!("Failed to switch session");
|
||||
|
|
@ -909,6 +917,7 @@ fn switch_session(
|
|||
name: session_name,
|
||||
tab_position,
|
||||
pane_id,
|
||||
layout,
|
||||
};
|
||||
env.plugin_env
|
||||
.senders
|
||||
|
|
@ -1278,6 +1287,30 @@ fn rename_session(env: &ForeignFunctionEnv, new_session_name: String) {
|
|||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn disconnect_other_clients(env: &ForeignFunctionEnv) {
|
||||
let _ = env
|
||||
.plugin_env
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::DisconnectAllClientsExcept(
|
||||
env.plugin_env.client_id,
|
||||
))
|
||||
.context("failed to send disconnect other clients instruction");
|
||||
}
|
||||
|
||||
fn kill_sessions(session_names: Vec<String>) {
|
||||
for session_name in session_names {
|
||||
let path = &*ZELLIJ_SOCK_DIR.join(&session_name);
|
||||
match LocalSocketStream::connect(path) {
|
||||
Ok(stream) => {
|
||||
let _ = IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession);
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to kill session {}: {:?}", session_name, e);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Custom panic handler for plugins.
|
||||
//
|
||||
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the
|
||||
|
|
@ -1406,7 +1439,9 @@ fn check_command_permission(
|
|||
| PluginCommand::DeleteDeadSession(..)
|
||||
| PluginCommand::DeleteAllDeadSessions
|
||||
| PluginCommand::RenameSession(..)
|
||||
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
|
||||
| PluginCommand::RenameTab(..)
|
||||
| PluginCommand::DisconnectOtherClients
|
||||
| PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState,
|
||||
PluginCommand::UnblockCliPipeInput(..)
|
||||
| PluginCommand::BlockCliPipeInput(..)
|
||||
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,
|
||||
|
|
|
|||
|
|
@ -577,6 +577,8 @@ pub(crate) struct Screen {
|
|||
default_shell: Option<PathBuf>,
|
||||
styled_underlines: bool,
|
||||
arrow_fonts: bool,
|
||||
layout_dir: Option<PathBuf>,
|
||||
default_layout_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
|
|
@ -592,12 +594,14 @@ impl Screen {
|
|||
copy_options: CopyOptions,
|
||||
debug: bool,
|
||||
default_layout: Box<Layout>,
|
||||
default_layout_name: Option<String>,
|
||||
default_shell: Option<PathBuf>,
|
||||
session_serialization: bool,
|
||||
serialize_pane_viewport: bool,
|
||||
scrollback_lines_to_serialize: Option<usize>,
|
||||
styled_underlines: bool,
|
||||
arrow_fonts: bool,
|
||||
layout_dir: Option<PathBuf>,
|
||||
) -> Self {
|
||||
let session_name = mode_info.session_name.clone().unwrap_or_default();
|
||||
let session_info = SessionInfo::new(session_name.clone());
|
||||
|
|
@ -629,6 +633,7 @@ impl Screen {
|
|||
session_name,
|
||||
session_infos_on_machine,
|
||||
default_layout,
|
||||
default_layout_name,
|
||||
default_shell,
|
||||
session_serialization,
|
||||
serialize_pane_viewport,
|
||||
|
|
@ -636,6 +641,7 @@ impl Screen {
|
|||
styled_underlines,
|
||||
arrow_fonts,
|
||||
resurrectable_sessions,
|
||||
layout_dir,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1412,12 +1418,21 @@ impl Screen {
|
|||
// generate own session info
|
||||
let pane_manifest = self.generate_and_report_pane_state()?;
|
||||
let tab_infos = self.generate_and_report_tab_state()?;
|
||||
// in the context of unit/integration tests, we don't need to list available layouts
|
||||
// because this is mostly about HD access - it does however throw off the timing in the
|
||||
// tests and causes them to flake, which is why we skip it here
|
||||
#[cfg(not(test))]
|
||||
let available_layouts =
|
||||
Layout::list_available_layouts(self.layout_dir.clone(), &self.default_layout_name);
|
||||
#[cfg(test)]
|
||||
let available_layouts = vec![];
|
||||
let session_info = SessionInfo {
|
||||
name: self.session_name.clone(),
|
||||
tabs: tab_infos,
|
||||
panes: pane_manifest,
|
||||
connected_clients: self.active_tab_indices.keys().len(),
|
||||
is_current_session: true,
|
||||
available_layouts,
|
||||
};
|
||||
self.bus
|
||||
.senders
|
||||
|
|
@ -2101,7 +2116,11 @@ pub(crate) fn screen_thread_main(
|
|||
let serialize_pane_viewport = config_options.serialize_pane_viewport.unwrap_or(false);
|
||||
let scrollback_lines_to_serialize = config_options.scrollback_lines_to_serialize;
|
||||
let session_is_mirrored = config_options.mirror_session.unwrap_or(false);
|
||||
let layout_dir = config_options.layout_dir;
|
||||
let default_shell = config_options.default_shell;
|
||||
let default_layout_name = config_options
|
||||
.default_layout
|
||||
.map(|l| format!("{}", l.display()));
|
||||
let copy_options = CopyOptions::new(
|
||||
config_options.copy_command,
|
||||
config_options.copy_clipboard.unwrap_or_default(),
|
||||
|
|
@ -2128,12 +2147,14 @@ pub(crate) fn screen_thread_main(
|
|||
copy_options,
|
||||
debug,
|
||||
default_layout,
|
||||
default_layout_name,
|
||||
default_shell,
|
||||
session_serialization,
|
||||
serialize_pane_viewport,
|
||||
scrollback_lines_to_serialize,
|
||||
styled_underlines,
|
||||
arrow_fonts,
|
||||
layout_dir,
|
||||
);
|
||||
|
||||
let mut pending_tab_ids: HashSet<usize> = HashSet::new();
|
||||
|
|
|
|||
|
|
@ -847,7 +847,7 @@ impl Tab {
|
|||
pub fn rename_session(&mut self, new_session_name: String) -> Result<()> {
|
||||
{
|
||||
let mode_infos = &mut self.mode_info.borrow_mut();
|
||||
for (_client_id, mut mode_info) in mode_infos.iter_mut() {
|
||||
for (_client_id, mode_info) in mode_infos.iter_mut() {
|
||||
mode_info.session_name = Some(new_session_name.clone());
|
||||
}
|
||||
self.default_mode_info.session_name = Some(new_session_name);
|
||||
|
|
|
|||
|
|
@ -241,10 +241,12 @@ fn create_new_screen(size: Size) -> Screen {
|
|||
let session_is_mirrored = true;
|
||||
let copy_options = CopyOptions::default();
|
||||
let default_layout = Box::new(Layout::default());
|
||||
let default_layout_name = None;
|
||||
let default_shell = None;
|
||||
let session_serialization = true;
|
||||
let serialize_pane_viewport = false;
|
||||
let scrollback_lines_to_serialize = None;
|
||||
let layout_dir = None;
|
||||
|
||||
let debug = false;
|
||||
let styled_underlines = true;
|
||||
|
|
@ -260,12 +262,14 @@ fn create_new_screen(size: Size) -> Screen {
|
|||
copy_options,
|
||||
debug,
|
||||
default_layout,
|
||||
default_layout_name,
|
||||
default_shell,
|
||||
session_serialization,
|
||||
serialize_pane_viewport,
|
||||
scrollback_lines_to_serialize,
|
||||
styled_underlines,
|
||||
arrow_fonts,
|
||||
layout_dir,
|
||||
);
|
||||
screen
|
||||
}
|
||||
|
|
@ -425,6 +429,7 @@ impl MockScreen {
|
|||
plugin_thread: None,
|
||||
pty_writer_thread: None,
|
||||
background_jobs_thread: None,
|
||||
config_options: Default::default(),
|
||||
layout,
|
||||
}
|
||||
}
|
||||
|
|
@ -481,6 +486,7 @@ impl MockScreen {
|
|||
plugin_thread: None,
|
||||
pty_writer_thread: None,
|
||||
background_jobs_thread: None,
|
||||
config_options: Default::default(),
|
||||
layout,
|
||||
};
|
||||
|
||||
|
|
@ -2869,17 +2875,17 @@ pub fn screen_can_break_floating_pane_to_a_new_tab() {
|
|||
1,
|
||||
1,
|
||||
));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
// move back to make sure the other pane is in the previous tab
|
||||
let _ = mock_screen
|
||||
.to_screen
|
||||
.send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
// move forward to make sure the broken pane is in the previous tab
|
||||
let _ = mock_screen
|
||||
.to_screen
|
||||
.send(ScreenInstruction::MoveFocusRightOrNextTab(1));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
|
||||
mock_screen.teardown(vec![server_thread, screen_thread]);
|
||||
|
||||
|
|
|
|||
|
|
@ -653,6 +653,18 @@ pub fn switch_session(name: Option<&str>) {
|
|||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Switch to a session with the given name, create one if no name is given
|
||||
pub fn switch_session_with_layout(name: Option<&str>, layout: LayoutInfo) {
|
||||
let plugin_command = PluginCommand::SwitchSession(ConnectToSession {
|
||||
name: name.map(|n| n.to_string()),
|
||||
layout: Some(layout),
|
||||
..Default::default()
|
||||
});
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Switch to a session with the given name, focusing either the provided pane_id or the provided
|
||||
/// tab position (in that order)
|
||||
pub fn switch_session_with_focus(
|
||||
|
|
@ -664,6 +676,7 @@ pub fn switch_session_with_focus(
|
|||
name: Some(name.to_owned()),
|
||||
tab_position,
|
||||
pane_id,
|
||||
..Default::default()
|
||||
});
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
|
|
@ -726,6 +739,26 @@ pub fn pipe_message_to_plugin(message_to_plugin: MessageToPlugin) {
|
|||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Disconnect all other clients from the current session
|
||||
pub fn disconnect_other_clients() {
|
||||
let plugin_command = PluginCommand::DisconnectOtherClients;
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Kill all Zellij sessions in the list
|
||||
pub fn kill_sessions<S: AsRef<str>>(session_names: &[S])
|
||||
where
|
||||
S: ToString,
|
||||
{
|
||||
let plugin_command =
|
||||
PluginCommand::KillSessions(session_names.into_iter().map(|s| s.to_string()).collect());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
|
||||
#[allow(unused)]
|
||||
|
|
|
|||
8
zellij-utils/assets/layouts/welcome.kdl
Normal file
8
zellij-utils/assets/layouts/welcome.kdl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
layout {
|
||||
pane borderless=true {
|
||||
plugin location="zellij:session-manager" {
|
||||
welcome_screen true
|
||||
}
|
||||
}
|
||||
}
|
||||
session_serialization false // this will apply only to the initial welcome screen layout, and is intended to prevent lots of garbage sessions left around
|
||||
|
|
@ -172,6 +172,16 @@ pub struct SessionManifest {
|
|||
pub connected_clients: u32,
|
||||
#[prost(bool, tag = "5")]
|
||||
pub is_current_session: bool,
|
||||
#[prost(message, repeated, tag = "6")]
|
||||
pub available_layouts: ::prost::alloc::vec::Vec<LayoutInfo>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct LayoutInfo {
|
||||
#[prost(string, tag = "1")]
|
||||
pub name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub source: ::prost::alloc::string::String,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pub struct PluginCommand {
|
|||
pub name: i32,
|
||||
#[prost(
|
||||
oneof = "plugin_command::Payload",
|
||||
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50"
|
||||
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60"
|
||||
)]
|
||||
pub payload: ::core::option::Option<plugin_command::Payload>,
|
||||
}
|
||||
|
|
@ -112,10 +112,18 @@ pub mod plugin_command {
|
|||
CliPipeOutputPayload(super::CliPipeOutputPayload),
|
||||
#[prost(message, tag = "50")]
|
||||
MessageToPluginPayload(super::MessageToPluginPayload),
|
||||
#[prost(message, tag = "60")]
|
||||
KillSessionsPayload(super::KillSessionsPayload),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct KillSessionsPayload {
|
||||
#[prost(string, repeated, tag = "1")]
|
||||
pub session_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct CliPipeOutputPayload {
|
||||
#[prost(string, tag = "1")]
|
||||
pub pipe_name: ::prost::alloc::string::String,
|
||||
|
|
@ -171,6 +179,8 @@ pub struct SwitchSessionPayload {
|
|||
pub pane_id: ::core::option::Option<u32>,
|
||||
#[prost(bool, optional, tag = "4")]
|
||||
pub pane_id_is_plugin: ::core::option::Option<bool>,
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub layout: ::core::option::Option<super::event::LayoutInfo>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
|
|
@ -376,6 +386,8 @@ pub enum CommandName {
|
|||
BlockCliPipeInput = 77,
|
||||
CliPipeOutput = 78,
|
||||
MessageToPlugin = 79,
|
||||
DisconnectOtherClients = 80,
|
||||
KillSessions = 81,
|
||||
}
|
||||
impl CommandName {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
|
|
@ -464,6 +476,8 @@ impl CommandName {
|
|||
CommandName::BlockCliPipeInput => "BlockCliPipeInput",
|
||||
CommandName::CliPipeOutput => "CliPipeOutput",
|
||||
CommandName::MessageToPlugin => "MessageToPlugin",
|
||||
CommandName::DisconnectOtherClients => "DisconnectOtherClients",
|
||||
CommandName::KillSessions => "KillSessions",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
|
|
@ -549,6 +563,8 @@ impl CommandName {
|
|||
"BlockCliPipeInput" => Some(Self::BlockCliPipeInput),
|
||||
"CliPipeOutput" => Some(Self::CliPipeOutput),
|
||||
"MessageToPlugin" => Some(Self::MessageToPlugin),
|
||||
"DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
|
||||
"KillSessions" => Some(Self::KillSessions),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -765,6 +765,28 @@ pub struct SessionInfo {
|
|||
pub panes: PaneManifest,
|
||||
pub connected_clients: usize,
|
||||
pub is_current_session: bool,
|
||||
pub available_layouts: Vec<LayoutInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum LayoutInfo {
|
||||
BuiltIn(String),
|
||||
File(String),
|
||||
}
|
||||
|
||||
impl LayoutInfo {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
LayoutInfo::BuiltIn(name) => &name,
|
||||
LayoutInfo::File(name) => &name,
|
||||
}
|
||||
}
|
||||
pub fn is_builtin(&self) -> bool {
|
||||
match self {
|
||||
LayoutInfo::BuiltIn(_name) => true,
|
||||
LayoutInfo::File(_name) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
|
@ -1032,12 +1054,12 @@ impl MessageToPlugin {
|
|||
self
|
||||
}
|
||||
pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self {
|
||||
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
new_plugin_args.should_float = Some(should_float);
|
||||
self
|
||||
}
|
||||
pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> Self {
|
||||
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
new_plugin_args.pane_id_to_replace = Some(pane_id);
|
||||
self
|
||||
}
|
||||
|
|
@ -1045,17 +1067,17 @@ impl MessageToPlugin {
|
|||
mut self,
|
||||
pane_title: impl Into<String>,
|
||||
) -> Self {
|
||||
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
new_plugin_args.pane_title = Some(pane_title.into());
|
||||
self
|
||||
}
|
||||
pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> Self {
|
||||
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
new_plugin_args.cwd = Some(cwd);
|
||||
self
|
||||
}
|
||||
pub fn new_plugin_instance_should_skip_cache(mut self) -> Self {
|
||||
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
|
||||
new_plugin_args.skip_cache = true;
|
||||
self
|
||||
}
|
||||
|
|
@ -1066,6 +1088,17 @@ pub struct ConnectToSession {
|
|||
pub name: Option<String>,
|
||||
pub tab_position: Option<usize>,
|
||||
pub pane_id: Option<(u32, bool)>, // (id, is_plugin)
|
||||
pub layout: Option<LayoutInfo>,
|
||||
}
|
||||
|
||||
impl ConnectToSession {
|
||||
pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) {
|
||||
if let Some(LayoutInfo::File(file_path)) = self.layout.as_mut() {
|
||||
*file_path = Path::join(layout_dir, &file_path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
@ -1228,4 +1261,6 @@ pub enum PluginCommand {
|
|||
BlockCliPipeInput(String), // String => pipe name
|
||||
CliPipeOutput(String, String), // String => pipe name, String => output
|
||||
MessageToPlugin(MessageToPlugin),
|
||||
DisconnectOtherClients,
|
||||
KillSessions(Vec<String>), // one or more session names
|
||||
}
|
||||
|
|
|
|||
|
|
@ -442,6 +442,7 @@ pub enum ServerContext {
|
|||
UnblockCliPipeInput,
|
||||
CliPipeOutput,
|
||||
AssociatePipeWithClient,
|
||||
DisconnectAllClientsExcept,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -222,6 +222,15 @@ impl Config {
|
|||
Err(e) => Err(ConfigError::IoPath(e, path.into())),
|
||||
}
|
||||
}
|
||||
pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> {
|
||||
self.options = self.options.merge(other.options);
|
||||
self.keybinds.merge(other.keybinds.clone());
|
||||
self.themes = self.themes.merge(other.themes);
|
||||
self.plugins = self.plugins.merge(other.plugins);
|
||||
self.ui = self.ui.merge(other.ui);
|
||||
self.env = self.env.merge(other.env);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -65,6 +65,17 @@ impl Keybinds {
|
|||
}
|
||||
ret
|
||||
}
|
||||
pub fn merge(&mut self, mut other: Keybinds) {
|
||||
for (other_input_mode, mut other_input_mode_keybinds) in other.0.drain() {
|
||||
let input_mode_keybinds = self
|
||||
.0
|
||||
.entry(other_input_mode)
|
||||
.or_insert_with(|| Default::default());
|
||||
for (other_action, other_action_keybinds) in other_input_mode_keybinds.drain() {
|
||||
input_mode_keybinds.insert(other_action, other_action_keybinds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The unit test location.
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
// If plugins should be able to depend on the layout system
|
||||
// then [`zellij-utils`] could be a proper place.
|
||||
use crate::{
|
||||
data::Direction,
|
||||
home::find_default_config_dir,
|
||||
data::{Direction, LayoutInfo},
|
||||
home::{default_layout_dir, find_default_config_dir},
|
||||
input::{
|
||||
command::RunCommand,
|
||||
config::{Config, ConfigError},
|
||||
|
|
@ -19,6 +19,7 @@ use crate::{
|
|||
setup::{self},
|
||||
};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
|
|
@ -819,6 +820,62 @@ impl Default for LayoutParts {
|
|||
}
|
||||
|
||||
impl Layout {
|
||||
// the first layout will either be the default one
|
||||
pub fn list_available_layouts(
|
||||
layout_dir: Option<PathBuf>,
|
||||
default_layout_name: &Option<String>,
|
||||
) -> Vec<LayoutInfo> {
|
||||
let mut available_layouts = layout_dir
|
||||
.clone()
|
||||
.or_else(|| default_layout_dir())
|
||||
.and_then(|layout_dir| match std::fs::read_dir(layout_dir) {
|
||||
Ok(layout_files) => Some(layout_files),
|
||||
Err(e) => {
|
||||
log::error!("Failed to read layout dir: {:?}", e);
|
||||
None
|
||||
},
|
||||
})
|
||||
.map(|layout_files| {
|
||||
let mut available_layouts = vec![];
|
||||
for file in layout_files {
|
||||
if let Ok(file) = file {
|
||||
if Layout::from_path_or_default_without_config(
|
||||
Some(&file.path()),
|
||||
layout_dir.clone(),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if let Some(file_name) = file.path().file_stem() {
|
||||
available_layouts
|
||||
.push(LayoutInfo::File(file_name.to_string_lossy().to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
available_layouts
|
||||
})
|
||||
.unwrap_or_else(Default::default);
|
||||
let default_layout_name = default_layout_name
|
||||
.as_ref()
|
||||
.map(|d| d.as_str())
|
||||
.unwrap_or("default");
|
||||
available_layouts.push(LayoutInfo::BuiltIn("default".to_owned()));
|
||||
available_layouts.push(LayoutInfo::BuiltIn("strider".to_owned()));
|
||||
available_layouts.push(LayoutInfo::BuiltIn("disable-status-bar".to_owned()));
|
||||
available_layouts.push(LayoutInfo::BuiltIn("compact".to_owned()));
|
||||
available_layouts.sort_by(|a, b| {
|
||||
let a_name = a.name();
|
||||
let b_name = b.name();
|
||||
if a_name == default_layout_name {
|
||||
return Ordering::Less;
|
||||
} else if b_name == default_layout_name {
|
||||
return Ordering::Greater;
|
||||
} else {
|
||||
a_name.cmp(&b_name)
|
||||
}
|
||||
});
|
||||
available_layouts
|
||||
}
|
||||
pub fn stringified_from_path_or_default(
|
||||
layout_path: Option<&PathBuf>,
|
||||
layout_dir: Option<PathBuf>,
|
||||
|
|
@ -861,6 +918,40 @@ impl Layout {
|
|||
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
|
||||
Ok((layout, config))
|
||||
}
|
||||
pub fn from_path_or_default_without_config(
|
||||
layout_path: Option<&PathBuf>,
|
||||
layout_dir: Option<PathBuf>,
|
||||
) -> Result<Layout, ConfigError> {
|
||||
let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
|
||||
Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
|
||||
let layout = Layout::from_kdl(
|
||||
&raw_layout,
|
||||
path_to_raw_layout,
|
||||
raw_swap_layouts
|
||||
.as_ref()
|
||||
.map(|(r, f)| (r.as_str(), f.as_str())),
|
||||
None,
|
||||
)?;
|
||||
Ok(layout)
|
||||
}
|
||||
pub fn from_default_assets(
|
||||
layout_name: &Path,
|
||||
_layout_dir: Option<PathBuf>,
|
||||
config: Config,
|
||||
) -> Result<(Layout, Config), ConfigError> {
|
||||
let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
|
||||
Layout::stringified_from_default_assets(layout_name)?;
|
||||
let layout = Layout::from_kdl(
|
||||
&raw_layout,
|
||||
path_to_raw_layout,
|
||||
raw_swap_layouts
|
||||
.as_ref()
|
||||
.map(|(r, f)| (r.as_str(), f.as_str())),
|
||||
None,
|
||||
)?;
|
||||
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
|
||||
Ok((layout, config))
|
||||
}
|
||||
pub fn from_str(
|
||||
raw: &str,
|
||||
path_to_raw_layout: String,
|
||||
|
|
@ -951,6 +1042,11 @@ impl Layout {
|
|||
Self::stringified_compact_swap_from_assets()?,
|
||||
)),
|
||||
)),
|
||||
Some("welcome") => Ok((
|
||||
"Welcome screen layout".into(),
|
||||
Self::stringified_welcome_from_assets()?,
|
||||
None,
|
||||
)),
|
||||
None | Some(_) => Err(ConfigError::IoPath(
|
||||
std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
|
||||
path.into(),
|
||||
|
|
@ -982,6 +1078,10 @@ impl Layout {
|
|||
Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?)
|
||||
}
|
||||
|
||||
pub fn stringified_welcome_from_assets() -> Result<String, ConfigError> {
|
||||
Ok(String::from_utf8(setup::WELCOME_LAYOUT.to_vec())?)
|
||||
}
|
||||
|
||||
pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
|
||||
self.template.clone().unwrap_or_default()
|
||||
}
|
||||
|
|
@ -1024,24 +1124,10 @@ impl Layout {
|
|||
swap_layout_path.as_os_str().to_string_lossy().into(),
|
||||
swap_kdl_layout,
|
||||
)),
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to read swap layout file: {}. Error: {:?}",
|
||||
swap_layout_path.as_os_str().to_string_lossy(),
|
||||
e
|
||||
);
|
||||
None
|
||||
},
|
||||
Err(_e) => None,
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to read swap layout file: {}. Error: {:?}",
|
||||
swap_layout_path.as_os_str().to_string_lossy(),
|
||||
e
|
||||
);
|
||||
None
|
||||
},
|
||||
Err(_e) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
mod kdl_layout_parser;
|
||||
use crate::data::{
|
||||
Direction, InputMode, Key, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType,
|
||||
Resize, SessionInfo, TabInfo,
|
||||
Direction, InputMode, Key, LayoutInfo, Palette, PaletteColor, PaneInfo, PaneManifest,
|
||||
PermissionType, Resize, SessionInfo, TabInfo,
|
||||
};
|
||||
use crate::envs::EnvironmentVariables;
|
||||
use crate::home::{find_default_config_dir, get_layout_dir};
|
||||
|
|
@ -1986,6 +1986,31 @@ impl SessionInfo {
|
|||
.and_then(|p| p.children())
|
||||
.map(|p| PaneManifest::decode_from_kdl(p))
|
||||
.ok_or("Failed to parse panes")?;
|
||||
let available_layouts: Vec<LayoutInfo> = kdl_document
|
||||
.get("available_layouts")
|
||||
.and_then(|p| p.children())
|
||||
.map(|e| {
|
||||
e.nodes()
|
||||
.iter()
|
||||
.filter_map(|n| {
|
||||
let layout_name = n.name().value().to_owned();
|
||||
let layout_source = n
|
||||
.entries()
|
||||
.iter()
|
||||
.find(|e| e.name().map(|n| n.value()) == Some("source"))
|
||||
.and_then(|e| e.value().as_string());
|
||||
match layout_source {
|
||||
Some(layout_source) => match layout_source {
|
||||
"built-in" => Some(LayoutInfo::BuiltIn(layout_name)),
|
||||
"file" => Some(LayoutInfo::File(layout_name)),
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.ok_or("Failed to parse available_layouts")?;
|
||||
let is_current_session = name == current_session_name;
|
||||
Ok(SessionInfo {
|
||||
name,
|
||||
|
|
@ -1993,6 +2018,7 @@ impl SessionInfo {
|
|||
panes,
|
||||
connected_clients,
|
||||
is_current_session,
|
||||
available_layouts,
|
||||
})
|
||||
}
|
||||
pub fn to_string(&self) -> String {
|
||||
|
|
@ -2017,10 +2043,25 @@ impl SessionInfo {
|
|||
let mut panes = KdlNode::new("panes");
|
||||
panes.set_children(self.panes.encode_to_kdl());
|
||||
|
||||
let mut available_layouts = KdlNode::new("available_layouts");
|
||||
let mut available_layouts_children = KdlDocument::new();
|
||||
for layout_info in &self.available_layouts {
|
||||
let (layout_name, layout_source) = match layout_info {
|
||||
LayoutInfo::File(name) => (name.clone(), "file"),
|
||||
LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
|
||||
};
|
||||
let mut layout_node = KdlNode::new(format!("{}", layout_name));
|
||||
let layout_source = KdlEntry::new_prop("source", layout_source);
|
||||
layout_node.entries_mut().push(layout_source);
|
||||
available_layouts_children.nodes_mut().push(layout_node);
|
||||
}
|
||||
available_layouts.set_children(available_layouts_children);
|
||||
|
||||
kdl_document.nodes_mut().push(name);
|
||||
kdl_document.nodes_mut().push(tabs);
|
||||
kdl_document.nodes_mut().push(panes);
|
||||
kdl_document.nodes_mut().push(connected_clients);
|
||||
kdl_document.nodes_mut().push(available_layouts);
|
||||
kdl_document.fmt();
|
||||
kdl_document.to_string()
|
||||
}
|
||||
|
|
@ -2506,6 +2547,11 @@ fn serialize_and_deserialize_session_info_with_data() {
|
|||
panes: PaneManifest { panes },
|
||||
connected_clients: 2,
|
||||
is_current_session: false,
|
||||
available_layouts: vec![
|
||||
LayoutInfo::File("layout1".to_owned()),
|
||||
LayoutInfo::BuiltIn("layout2".to_owned()),
|
||||
LayoutInfo::File("layout3".to_owned()),
|
||||
],
|
||||
};
|
||||
let serialized = session_info.to_string();
|
||||
let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zellij-utils/src/kdl/mod.rs
|
||||
assertion_line: 2284
|
||||
assertion_line: 2459
|
||||
expression: serialized
|
||||
---
|
||||
name ""
|
||||
|
|
@ -9,4 +9,6 @@ tabs {
|
|||
panes {
|
||||
}
|
||||
connected_clients 0
|
||||
available_layouts {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zellij-utils/src/kdl/mod.rs
|
||||
assertion_line: 2377
|
||||
assertion_line: 2552
|
||||
expression: serialized
|
||||
---
|
||||
name "my session name"
|
||||
|
|
@ -78,4 +78,9 @@ panes {
|
|||
}
|
||||
}
|
||||
connected_clients 2
|
||||
available_layouts {
|
||||
layout1 source="file"
|
||||
layout2 source="built-in"
|
||||
layout3 source="file"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,6 +152,12 @@ message SessionManifest {
|
|||
repeated PaneManifest panes = 3;
|
||||
uint32 connected_clients = 4;
|
||||
bool is_current_session = 5;
|
||||
repeated LayoutInfo available_layouts = 6;
|
||||
}
|
||||
|
||||
message LayoutInfo {
|
||||
string name = 1;
|
||||
string source = 2;
|
||||
}
|
||||
|
||||
message ResurrectableSession {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ pub use super::generated_api::api::{
|
|||
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
|
||||
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
|
||||
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
|
||||
KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload,
|
||||
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
|
||||
ResurrectableSession as ProtobufResurrectableSession,
|
||||
KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
|
||||
ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo,
|
||||
PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession,
|
||||
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
|
||||
},
|
||||
input_mode::InputMode as ProtobufInputMode,
|
||||
|
|
@ -14,8 +14,8 @@ pub use super::generated_api::api::{
|
|||
style::Style as ProtobufStyle,
|
||||
};
|
||||
use crate::data::{
|
||||
CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
|
||||
PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
|
||||
CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo,
|
||||
PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
|
||||
};
|
||||
|
||||
use crate::errors::prelude::*;
|
||||
|
|
@ -453,6 +453,11 @@ impl TryFrom<SessionInfo> for ProtobufSessionManifest {
|
|||
.collect(),
|
||||
connected_clients: session_info.connected_clients as u32,
|
||||
is_current_session: session_info.is_current_session,
|
||||
available_layouts: session_info
|
||||
.available_layouts
|
||||
.into_iter()
|
||||
.filter_map(|l| ProtobufLayoutInfo::try_from(l).ok())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -485,10 +490,42 @@ impl TryFrom<ProtobufSessionManifest> for SessionInfo {
|
|||
panes,
|
||||
connected_clients: protobuf_session_manifest.connected_clients as usize,
|
||||
is_current_session: protobuf_session_manifest.is_current_session,
|
||||
available_layouts: protobuf_session_manifest
|
||||
.available_layouts
|
||||
.into_iter()
|
||||
.filter_map(|l| LayoutInfo::try_from(l).ok())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
|
||||
type Error = &'static str;
|
||||
fn try_from(layout_info: LayoutInfo) -> Result<Self, &'static str> {
|
||||
match layout_info {
|
||||
LayoutInfo::File(name) => Ok(ProtobufLayoutInfo {
|
||||
source: "file".to_owned(),
|
||||
name,
|
||||
}),
|
||||
LayoutInfo::BuiltIn(name) => Ok(ProtobufLayoutInfo {
|
||||
source: "built-in".to_owned(),
|
||||
name,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
|
||||
type Error = &'static str;
|
||||
fn try_from(protobuf_layout_info: ProtobufLayoutInfo) -> Result<Self, &'static str> {
|
||||
match protobuf_layout_info.source.as_str() {
|
||||
"file" => Ok(LayoutInfo::File(protobuf_layout_info.name)),
|
||||
"built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)),
|
||||
_ => Err("Unknown source for layout"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CopyDestination> for ProtobufCopyDestination {
|
||||
type Error = &'static str;
|
||||
fn try_from(copy_destination: CopyDestination) -> Result<Self, &'static str> {
|
||||
|
|
@ -1383,6 +1420,11 @@ fn serialize_session_update_event_with_non_default_values() {
|
|||
panes: PaneManifest { panes },
|
||||
connected_clients: 2,
|
||||
is_current_session: true,
|
||||
available_layouts: vec![
|
||||
LayoutInfo::File("layout 1".to_owned()),
|
||||
LayoutInfo::BuiltIn("layout2".to_owned()),
|
||||
LayoutInfo::File("layout3".to_owned()),
|
||||
],
|
||||
};
|
||||
let session_info_2 = SessionInfo {
|
||||
name: "session 2".to_owned(),
|
||||
|
|
@ -1392,6 +1434,11 @@ fn serialize_session_update_event_with_non_default_values() {
|
|||
},
|
||||
connected_clients: 0,
|
||||
is_current_session: false,
|
||||
available_layouts: vec![
|
||||
LayoutInfo::File("layout 1".to_owned()),
|
||||
LayoutInfo::BuiltIn("layout2".to_owned()),
|
||||
LayoutInfo::File("layout3".to_owned()),
|
||||
],
|
||||
};
|
||||
let session_infos = vec![session_info_1, session_info_2];
|
||||
let resurrectable_sessions = vec![];
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ enum CommandName {
|
|||
BlockCliPipeInput = 77;
|
||||
CliPipeOutput = 78;
|
||||
MessageToPlugin = 79;
|
||||
DisconnectOtherClients = 80;
|
||||
KillSessions = 81;
|
||||
}
|
||||
|
||||
message PluginCommand {
|
||||
|
|
@ -145,9 +147,14 @@ message PluginCommand {
|
|||
string block_cli_pipe_input_payload = 48;
|
||||
CliPipeOutputPayload cli_pipe_output_payload = 49;
|
||||
MessageToPluginPayload message_to_plugin_payload = 50;
|
||||
KillSessionsPayload kill_sessions_payload = 60;
|
||||
}
|
||||
}
|
||||
|
||||
message KillSessionsPayload {
|
||||
repeated string session_names = 1;
|
||||
}
|
||||
|
||||
message CliPipeOutputPayload {
|
||||
string pipe_name = 1;
|
||||
string output = 2;
|
||||
|
|
@ -185,6 +192,7 @@ message SwitchSessionPayload {
|
|||
optional uint32 tab_position = 2;
|
||||
optional uint32 pane_id = 3;
|
||||
optional bool pane_id_is_plugin = 4;
|
||||
optional event.LayoutInfo layout = 5;
|
||||
}
|
||||
|
||||
message RequestPluginPermissionPayload {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ pub use super::generated_api::api::{
|
|||
input_mode::InputMode as ProtobufInputMode,
|
||||
plugin_command::{
|
||||
plugin_command::Payload, CliPipeOutputPayload, CommandName, ContextItem, EnvVariable,
|
||||
ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, MessageToPluginPayload,
|
||||
MovePayload, NewPluginArgs as ProtobufNewPluginArgs, OpenCommandPanePayload,
|
||||
OpenFilePayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType,
|
||||
PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
|
||||
ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload,
|
||||
MessageToPluginPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs,
|
||||
OpenCommandPanePayload, OpenFilePayload, PaneId as ProtobufPaneId,
|
||||
PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
|
||||
RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload,
|
||||
SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload,
|
||||
WebRequestPayload,
|
||||
|
|
@ -574,6 +574,7 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
|
|||
name: payload.name,
|
||||
tab_position: payload.tab_position.map(|p| p as usize),
|
||||
pane_id,
|
||||
layout: payload.layout.and_then(|l| l.try_into().ok()),
|
||||
}))
|
||||
},
|
||||
_ => Err("Mismatched payload for SwitchSession"),
|
||||
|
|
@ -727,6 +728,16 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
|
|||
}),
|
||||
}))
|
||||
},
|
||||
_ => Err("Mismatched payload for MessageToPlugin"),
|
||||
},
|
||||
Some(CommandName::DisconnectOtherClients) => match protobuf_plugin_command.payload {
|
||||
None => Ok(PluginCommand::DisconnectOtherClients),
|
||||
_ => Err("Mismatched payload for DisconnectOtherClients"),
|
||||
},
|
||||
Some(CommandName::KillSessions) => match protobuf_plugin_command.payload {
|
||||
Some(Payload::KillSessionsPayload(KillSessionsPayload { session_names })) => {
|
||||
Ok(PluginCommand::KillSessions(session_names))
|
||||
},
|
||||
_ => Err("Mismatched payload for PipeOutput"),
|
||||
},
|
||||
None => Err("Unrecognized plugin command"),
|
||||
|
|
@ -1082,6 +1093,7 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
|
|||
tab_position: switch_to_session.tab_position.map(|t| t as u32),
|
||||
pane_id: switch_to_session.pane_id.map(|p| p.0),
|
||||
pane_id_is_plugin: switch_to_session.pane_id.map(|p| p.1),
|
||||
layout: switch_to_session.layout.and_then(|l| l.try_into().ok()),
|
||||
})),
|
||||
}),
|
||||
PluginCommand::OpenTerminalInPlace(cwd) => Ok(ProtobufPluginCommand {
|
||||
|
|
@ -1205,6 +1217,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
|
|||
})),
|
||||
})
|
||||
},
|
||||
PluginCommand::DisconnectOtherClients => Ok(ProtobufPluginCommand {
|
||||
name: CommandName::DisconnectOtherClients as i32,
|
||||
payload: None,
|
||||
}),
|
||||
PluginCommand::KillSessions(session_names) => Ok(ProtobufPluginCommand {
|
||||
name: CommandName::KillSessions as i32,
|
||||
payload: Some(Payload::KillSessionsPayload(KillSessionsPayload {
|
||||
session_names,
|
||||
})),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,12 @@ pub const COMPACT_BAR_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
|
|||
"assets/layouts/compact.swap.kdl"
|
||||
));
|
||||
|
||||
pub const WELCOME_LAYOUT: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/",
|
||||
"assets/layouts/welcome.kdl"
|
||||
));
|
||||
|
||||
pub const FISH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/",
|
||||
|
|
@ -345,7 +351,9 @@ impl Setup {
|
|||
/// 2. layout options
|
||||
/// (`layout.kdl` / `zellij --layout`)
|
||||
/// 3. config options (`config.kdl`)
|
||||
pub fn from_cli_args(cli_args: &CliArgs) -> Result<(Config, Layout, Options), ConfigError> {
|
||||
pub fn from_cli_args(
|
||||
cli_args: &CliArgs,
|
||||
) -> Result<(Config, Layout, Options, Config, Options), ConfigError> {
|
||||
// note that this can potentially exit the process
|
||||
Setup::handle_setup_commands(cli_args);
|
||||
let config = Config::try_from(cli_args)?;
|
||||
|
|
@ -355,8 +363,19 @@ impl Setup {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let mut config_without_layout = config.clone();
|
||||
let (layout, mut config) =
|
||||
Setup::parse_layout_and_override_config(cli_config_options.as_ref(), config, cli_args)?;
|
||||
|
||||
let config_options =
|
||||
apply_themes_to_config(&mut config, cli_config_options.clone(), cli_args)?;
|
||||
let config_options_without_layout =
|
||||
apply_themes_to_config(&mut config_without_layout, cli_config_options, cli_args)?;
|
||||
fn apply_themes_to_config(
|
||||
config: &mut Config,
|
||||
cli_config_options: Option<Options>,
|
||||
cli_args: &CliArgs,
|
||||
) -> Result<Options, ConfigError> {
|
||||
let config_options = match cli_config_options {
|
||||
Some(cli_config_options) => config.options.merge(cli_config_options),
|
||||
None => config.options.clone(),
|
||||
|
|
@ -371,6 +390,8 @@ impl Setup {
|
|||
if let Some(user_theme_dir) = user_theme_dir {
|
||||
config.themes = config.themes.merge(Themes::from_dir(user_theme_dir)?);
|
||||
}
|
||||
Ok(config_options)
|
||||
}
|
||||
|
||||
if let Some(Command::Setup(ref setup)) = &cli_args.command {
|
||||
setup
|
||||
|
|
@ -383,7 +404,13 @@ impl Setup {
|
|||
|_| {},
|
||||
);
|
||||
};
|
||||
Ok((config, layout, config_options))
|
||||
Ok((
|
||||
config,
|
||||
layout,
|
||||
config_options,
|
||||
config_without_layout,
|
||||
config_options_without_layout,
|
||||
))
|
||||
}
|
||||
|
||||
/// General setup helpers
|
||||
|
|
@ -667,7 +694,7 @@ mod setup_test {
|
|||
#[test]
|
||||
fn default_config_with_no_cli_arguments() {
|
||||
let cli_args = CliArgs::default();
|
||||
let (config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", config));
|
||||
assert_snapshot!(format!("{:#?}", layout));
|
||||
assert_snapshot!(format!("{:#?}", options));
|
||||
|
|
@ -682,7 +709,7 @@ mod setup_test {
|
|||
},
|
||||
..Default::default()
|
||||
}));
|
||||
let (_config, _layout, options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (_config, _layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", options));
|
||||
}
|
||||
#[test]
|
||||
|
|
@ -692,7 +719,7 @@ mod setup_test {
|
|||
"{}/src/test-fixtures/layout-with-options.kdl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)));
|
||||
let (_config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (_config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", options));
|
||||
assert_snapshot!(format!("{:#?}", layout));
|
||||
}
|
||||
|
|
@ -710,7 +737,7 @@ mod setup_test {
|
|||
},
|
||||
..Default::default()
|
||||
}));
|
||||
let (_config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (_config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", options));
|
||||
assert_snapshot!(format!("{:#?}", layout));
|
||||
}
|
||||
|
|
@ -725,7 +752,7 @@ mod setup_test {
|
|||
"{}/src/test-fixtures/layout-with-env-vars.kdl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)));
|
||||
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", config));
|
||||
}
|
||||
#[test]
|
||||
|
|
@ -739,7 +766,7 @@ mod setup_test {
|
|||
"{}/src/test-fixtures/layout-with-ui-config.kdl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)));
|
||||
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", config));
|
||||
}
|
||||
#[test]
|
||||
|
|
@ -753,7 +780,7 @@ mod setup_test {
|
|||
"{}/src/test-fixtures/layout-with-plugins-config.kdl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)));
|
||||
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", config));
|
||||
}
|
||||
#[test]
|
||||
|
|
@ -767,7 +794,7 @@ mod setup_test {
|
|||
"{}/src/test-fixtures/layout-with-themes-config.kdl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)));
|
||||
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", config));
|
||||
}
|
||||
#[test]
|
||||
|
|
@ -781,7 +808,7 @@ mod setup_test {
|
|||
"{}/src/test-fixtures/layout-with-keybindings-config.kdl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)));
|
||||
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
|
||||
assert_snapshot!(format!("{:#?}", config));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue