diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index fcdc363f..17f29943 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -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) => { diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index 8f70cc29..8040f79e 100644 --- a/default-plugins/session-manager/src/main.rs +++ b/default-plugins/session-manager/src/main.rs @@ -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, sessions: SessionList, resurrectable_sessions: ResurrectableSessions, search_term: String, - new_session_name: Option, + new_session_info: NewSessionInfo, renaming_session_name: Option, error: Option, - 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) { + fn load(&mut self, configuration: BTreeMap) { + 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, - self.colors, - ); - 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_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, + ); + }, + 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_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); + }, } - 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); + render_error(&error, height, width, x, y); } else { - render_controls_line(self.sessions.is_searching, rows, cols, self.colors); + 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,136 +248,165 @@ 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; + 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.renaming_session_name = Some(String::new()); + 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; - should_render = true; + } 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() { - 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 - } 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 - } 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 - } else if self - .resurrectable_sessions - .has_session(&renaming_session_name) - { - self.show_error("A resurrectable session by this name already exists."); - return; // s that we don't hide self - } else { - self.update_current_session_name_in_ui(&renaming_session_name); - 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() { - 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(); - if is_current_session { - if let Some((pane_id, is_plugin)) = selected_pane { - if is_plugin { - focus_plugin_pane(pane_id, true); + 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() { + self.show_error("New name must not be empty."); + 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; // 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; // so that we don't hide self + } else if self + .resurrectable_sessions + .has_session(&renaming_session_name) + { + self.show_error("A resurrectable session by this name already exists."); + return; // s that we don't hide self } else { - focus_terminal_pane(pane_id, true); + self.update_current_session_name_in_ui(&renaming_session_name); + rename_session(&renaming_session_name); + return; // s that we don't hide self } - } else if let Some(tab_position) = selected_tab { - go_to_tab(tab_position as u32); } - } else { - switch_session_with_focus(&selected_session_name, selected_tab, selected_pane); - } + 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(); + if is_current_session { + if let Some((pane_id, is_plugin)) = selected_pane { + if is_plugin { + focus_plugin_pane(pane_id, true); + } else { + focus_terminal_pane(pane_id, true); + } + } else if let Some(tab_position) = selected_tab { + go_to_tab(tab_position as u32); + } + } else { + switch_session_with_focus( + &selected_session_name, + selected_tab, + selected_pane, + ); + } + } + self.reset_selected_index(); + 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)); + } + }, } - 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(); + } + 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, + ); + } } diff --git a/default-plugins/session-manager/src/new_session_info.rs b/default-plugins/session-manager/src/new_session_info.rs new file mode 100644 index 00000000..6a2456f1 --- /dev/null +++ b/default-plugins/session-manager/src/new_session_info.rs @@ -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) { + match self.entering_new_session_info { + EnteringState::EnteringLayoutSearch => { + let new_session_layout: Option = 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) { + 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, 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 { + 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, + layout_search_results: Vec, + selected_layout_index: usize, + layout_search_term: String, +} + +impl LayoutList { + pub fn update_layout_list(&mut self, layout_list: Vec) { + 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 { + 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, +} diff --git a/default-plugins/session-manager/src/resurrectable_sessions.rs b/default-plugins/session-manager/src/resurrectable_sessions.rs index dfbb86f0..98887036 100644 --- a/default-plugins/session-manager/src/resurrectable_sessions.rs +++ b/default-plugins/session-manager/src/resurrectable_sessions.rs @@ -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, - Delete Session, - 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() { diff --git a/default-plugins/session-manager/src/session_list.rs b/default-plugins/session-manager/src/session_list.rs index fa63571e..01c75ad5 100644 --- a/default-plugins/session-manager/src/session_list.rs +++ b/default-plugins/session-manager/src/session_list.rs @@ -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 { + self.session_ui_infos + .iter() + .filter_map(|s| { + if !s.is_current_session { + Some(s.name.clone()) + } else { + None + } + }) + .collect() + } } #[derive(Debug, Clone, Default)] diff --git a/default-plugins/session-manager/src/ui/components.rs b/default-plugins/session-manager/src/ui/components.rs index 4d9839c0..d3c4afe8 100644 --- a/default-plugins/session-manager/src/ui/components.rs +++ b/default-plugins/session-manager/src/ui/components.rs @@ -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 ); }, @@ -323,7 +351,7 @@ impl LineToRender { if self.is_selected { self.line.clone() } else { - format!("\u{1b}[49m{}", line) + format!("\u{1b}[49m {}", line) } } pub fn add_truncated_results(&mut self, result_count: usize) { @@ -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 = ""; - 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, - ); - } 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, - ); - } + 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_line(session_name: &Option, is_searching: bool, colors: Colors) { - if is_searching { - return; - } - let new_session_shortcut_text = ""; - let new_session_shortcut = colors.magenta(new_session_shortcut_text); - let new_session = colors.bold("New session"); +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(""); - match session_name { - Some(session_name) => { + 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 > {}_ ({}, {} 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, + long_instruction, ); - }, - None => { - println!("\u{1b}[m > {new_session_shortcut} - {new_session}"); - }, + } else { + println!( + "\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() { + "" + } else { + new_session_info.name() + }; + let prompt = "New session name:"; + let long_instruction = "to correct"; + let esc = colors.magenta(""); + 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, + ); } } -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, 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: {}_ ", + 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: {}_ ( 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(""); - let rename_text = colors.bold("Rename session"); - let enter = colors.magenta(""); - let select = colors.bold("Switch to selected"); - let esc = colors.magenta(""); - let to_hide = colors.bold("Hide"); +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}[{y};{x}H\u{1b}[1mHelp: Fill in the form to start a new session." + ); + } + }, + ActiveScreen::AttachToSession => { + let arrows = colors.magenta("<←↓↑→>"); + let navigate = colors.bold("Navigate"); + let enter = colors.magenta(""); + let select = colors.bold("Attach"); + let rename = colors.magenta(""); + let rename_text = colors.bold("Rename"); + let disconnect = colors.magenta(""); + let disconnect_text = colors.bold("Disconnect others"); + let kill = colors.magenta(""); + let kill_text = colors.bold("Kill"); + let kill_all = colors.magenta(""); + let kill_all_text = colors.bold("Kill all"); - if max_cols >= 104 { - print!( - "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}" - ); - } else if max_cols >= 73 { - let navigate = colors.bold("Navigate"); - let select = colors.bold("Switch"); - let rename_text = colors.bold("Rename"); - print!( - "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}" - ); - } else if max_cols >= 28 { - print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{rename}/{esc}"); + if max_cols > 90 { + print!( + "\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}[{y};{x}H{rename}/{disconnect}/{kill}/{kill_all}"); + } + }, + ActiveScreen::ResurrectSession => { + let arrows = colors.magenta("<↓↑>"); + let navigate = colors.bold("Navigate"); + let enter = colors.magenta(""); + let select = colors.bold("Resurrect"); + let del = colors.magenta(""); + let del_text = colors.bold("Delete"); + let del_all = colors.magenta(""); + 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}"); + } + }, } } diff --git a/default-plugins/session-manager/src/ui/mod.rs b/default-plugins/session-manager/src/ui/mod.rs index 94a1a8a7..b86db946 100644 --- a/default-plugins/session-manager/src/ui/mod.rs +++ b/default-plugins/session-manager/src/ui/mod.rs @@ -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); diff --git a/default-plugins/session-manager/src/ui/welcome_screen.rs b/default-plugins/session-manager/src/ui/welcome_screen.rs new file mode 100644 index 00000000..ccf20c50 --- /dev/null +++ b/default-plugins/session-manager/src/ui/welcome_screen.rs @@ -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#"  +  +  _y$ y@g_ +  ya@@@@L4@@@@gy_ +  u@@@@@@F "@@@@@@@@y_ +  _a@@, @@@P~` __ ~T@@@@@@@@g +  _yg@@@@@$ "~ _yg@@@@gy `~PR@F~_yggy_ +  y$@@@@@@PF _y$@@@@@@@@@@gy_ 4@@@@@@@y_ +  g@@@@@F~ _yg@@@@@@@@@@@@@@@@@@g_ ~F@@@@@@ +  $@@@F yg@@@@@@@@@@@@@@@@@@@@@@@@gy 9@@F" +  $@@@$ 4@@@@@@~@@@@@@@@@@@@@@@@@@@@@ "_yg$ +  $@@@$ 4@@@@@ ~@@@@@@@@@@@@@@@@@@@ g@@@@ +  $@@@$ 4@@@@@@y ~@@@@@@@@@@@@@@@@@ $@@@F +  $@@@$ 4@@@@@@@@y ~@@@@@@@@@@@@@@@ `~_y_ +  $@@@$ 4@@@@@@@P` _g@@@@@@@@@@@@@@@ y@@@@ +  $@@@$ 4@@@@@P` _y@@@@@@@@@@@@@@@@@ 4@@@@ +  $@@@$ 4@@@@$_ y@@@@@ $@@@@ 4@@@@ +  $@@@$ 4@@@@@@g@@@@@@@@@@@@@@@@@@@@@ ~@@@ +  $@@P 7R@@@@@@@@@@@@@@@@@@@@@@@@P~ _@g_7@ +  ~~_yg@gy_ ~5@@@@@@@@@@@@@@@@@@F~ _yg@@@@y +  R@@@@@@@ ~~@@@@@@@@@@@P~` ya@@@@@@@@F +  ~5@@@@ 4@gy `~4@@@@F~ y@@@@@@@P~` +  `~P 4@@@@gy_ `` _yr a@@@@@F~ +  5@@@@@@@gg@@~_g@@@~` +  ~~@@@@@@@^y@F~ +  `~4@F ~ +  +  "#; diff --git a/src/commands.rs b/src/commands.rs index 6074ea7d..52dff211 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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,24 +384,25 @@ 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) { - Ok(results) => results, - Err(e) => { - if let ConfigError::KdlError(error) = e { - let report: Report = error.into(); - eprintln!("{:?}", report); - } else { - eprintln!("{}", e); - } - process::exit(1); - }, - }; + 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 { + let report: Report = error.into(); + eprintln!("{:?}", report); + } else { + eprintln!("{}", e); + } + process::exit(1); + }, + }; let mut reconnect_to_session: Option = None; 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; } diff --git a/src/sessions.rs b/src/sessions.rs index c577acc6..58722071 100644 --- a/src/sessions.rs +++ b/src/sessions.rs @@ -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, diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 437f465b..00895570 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -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, pub layout: Box, + pub config_options: Box, screen_thread: Option>, pty_thread: Option>, plugin_thread: Option>, @@ -650,6 +655,21 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { } break; }, + ServerInstruction::DisconnectAllClientsExcept(client_id) => { + let client_ids: Vec = 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, 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), diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 6791213c..6b6e345e 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -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)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap new file mode 100644 index 00000000..d463df1c --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap @@ -0,0 +1,10 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 6131 +expression: "format!(\"{:#?}\", switch_session_event)" +--- +Some( + DisconnectAllClientsExcept( + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap new file mode 100644 index 00000000..f254f039 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap @@ -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, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap new file mode 100644 index 00000000..46f6819a --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap @@ -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, + ), +) diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 8791e128..e6787c24 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -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) { 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) { 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, tab_position: Option, pane_id: Option<(u32, bool)>, + layout: Option, ) -> 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) { + 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, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index c312480d..5b0b6bea 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -577,6 +577,8 @@ pub(crate) struct Screen { default_shell: Option, styled_underlines: bool, arrow_fonts: bool, + layout_dir: Option, + default_layout_name: Option, } impl Screen { @@ -592,12 +594,14 @@ impl Screen { copy_options: CopyOptions, debug: bool, default_layout: Box, + default_layout_name: Option, default_shell: Option, session_serialization: bool, serialize_pane_viewport: bool, scrollback_lines_to_serialize: Option, styled_underlines: bool, arrow_fonts: bool, + layout_dir: Option, ) -> 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 = HashSet::new(); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 0352eb96..2ed9eb35 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -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); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 34076d70..4ce4dbc1 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -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]); diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 975e3499..dd5d1807 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -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>(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)] diff --git a/zellij-utils/assets/layouts/welcome.kdl b/zellij-utils/assets/layouts/welcome.kdl new file mode 100644 index 00000000..1884fe1f --- /dev/null +++ b/zellij-utils/assets/layouts/welcome.kdl @@ -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 diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index c095bed2..6d4ed729 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -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, +} +#[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)] diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index d9b528ae..5127bb81 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -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, } @@ -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, #[prost(bool, optional, tag = "4")] pub pane_id_is_plugin: ::core::option::Option, + #[prost(message, optional, tag = "5")] + pub layout: ::core::option::Option, } #[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, } } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index f1e01115..d001b75f 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -765,6 +765,28 @@ pub struct SessionInfo { pub panes: PaneManifest, pub connected_clients: usize, pub is_current_session: bool, + pub available_layouts: Vec, +} + +#[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, ) -> 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, pub tab_position: Option, pub pane_id: Option<(u32, bool)>, // (id, is_plugin) + pub layout: Option, +} + +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), // one or more session names } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 26ad521c..5b323399 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -442,6 +442,7 @@ pub enum ServerContext { UnblockCliPipeInput, CliPipeOutput, AssociatePipeWithClient, + DisconnectAllClientsExcept, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 23c05d4f..eb8fe521 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -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)] diff --git a/zellij-utils/src/input/keybinds.rs b/zellij-utils/src/input/keybinds.rs index eaed03fd..ab13f711 100644 --- a/zellij-utils/src/input/keybinds.rs +++ b/zellij-utils/src/input/keybinds.rs @@ -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. diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 82dab15e..7f65d605 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -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, + default_layout_name: &Option, + ) -> Vec { + 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, @@ -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, + ) -> Result { + 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, + 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 { + Ok(String::from_utf8(setup::WELCOME_LAYOUT.to_vec())?) + } + pub fn new_tab(&self) -> (TiledPaneLayout, Vec) { 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, } } } diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 71f48fc4..e911f391 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -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 = 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(); diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap index 4a231c37..32925ec1 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap @@ -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 { +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap index 99e3e03f..8244f04a 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap @@ -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" +} diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 0d964a35..e26a9c23 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -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 { diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index d9e5341d..21ff1eff 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -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 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 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 for ProtobufLayoutInfo { + type Error = &'static str; + fn try_from(layout_info: LayoutInfo) -> Result { + 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 for LayoutInfo { + type Error = &'static str; + fn try_from(protobuf_layout_info: ProtobufLayoutInfo) -> Result { + 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 for ProtobufCopyDestination { type Error = &'static str; fn try_from(copy_destination: CopyDestination) -> Result { @@ -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![]; diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 6ffb0345..b02dd73f 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -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 { diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index fa570a26..a5272d42 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -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 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 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 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 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, + })), + }), } } } diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 2c40f29d..bf34a894 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -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,21 +363,34 @@ 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 = match cli_config_options { - Some(cli_config_options) => config.options.merge(cli_config_options), - None => config.options.clone(), - }; - config.themes = config.themes.merge(get_default_themes()); + 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, + cli_args: &CliArgs, + ) -> Result { + let config_options = match cli_config_options { + Some(cli_config_options) => config.options.merge(cli_config_options), + None => config.options.clone(), + }; - let user_theme_dir = config_options.theme_dir.clone().or_else(|| { - get_theme_dir(cli_args.config_dir.clone().or_else(find_default_config_dir)) - .filter(|dir| dir.exists()) - }); - if let Some(user_theme_dir) = user_theme_dir { - config.themes = config.themes.merge(Themes::from_dir(user_theme_dir)?); + config.themes = config.themes.merge(get_default_themes()); + + let user_theme_dir = config_options.theme_dir.clone().or_else(|| { + get_theme_dir(cli_args.config_dir.clone().or_else(find_default_config_dir)) + .filter(|dir| dir.exists()) + }); + 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 { @@ -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)); } }