diff options
| author | Aram Drevekenin <aram@poor.dev> | 2024-02-06 14:26:14 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-02-06 14:26:14 +0100 |
| commit | 6b20a958f4e8db5614fffb27fe5d32f07ddfe855 (patch) | |
| tree | c8383fb7941b5e0c23cb348a655f2ea0fafd8668 /default-plugins | |
| parent | docs(readme): add sponsor (diff) | |
| download | sl0ppij-6b20a958f4e8db5614fffb27fe5d32f07ddfe855.tar.gz sl0ppij-6b20a958f4e8db5614fffb27fe5d32f07ddfe855.tar.xz sl0ppij-6b20a958f4e8db5614fffb27fe5d32f07ddfe855.zip | |
feat(sessions): welcome screen (#3112)
* prototype - can send layout name for new session from session-manager
* feat(sessions): ui for selecting layout for new session in the session-manager
* fix: send available layouts to plugins
* make tests compile
* fix tests
* improve ui
* fix: respect built-in layouts
* ui for built-in layouts
* some cleanups
* style(fmt): rustfmt
* welcome screen ui
* fix: make sure layout config is not shared between sessions
* allow disconnecting other users from current session and killing other sessions
* fix: respect default layout
* add welcome screen layout
* tests(plugins): new api methods
* fix(session-manager): do not quit welcome screen on esc and break
* fix(plugins): adjust permissions
* style(fmt): rustfmt
* style(fmt): fix warnings
Diffstat (limited to 'default-plugins')
| -rw-r--r-- | default-plugins/fixture-plugin-for-tests/src/main.rs | 10 | ||||
| -rw-r--r-- | default-plugins/session-manager/src/main.rs | 473 | ||||
| -rw-r--r-- | default-plugins/session-manager/src/new_session_info.rs | 265 | ||||
| -rw-r--r-- | default-plugins/session-manager/src/resurrectable_sessions.rs | 37 | ||||
| -rw-r--r-- | default-plugins/session-manager/src/session_list.rs | 12 | ||||
| -rw-r--r-- | default-plugins/session-manager/src/ui/components.rs | 382 | ||||
| -rw-r--r-- | default-plugins/session-manager/src/ui/mod.rs | 7 | ||||
| -rw-r--r-- | default-plugins/session-manager/src/ui/welcome_screen.rs | 168 |
8 files changed, 1054 insertions, 300 deletions
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<String>, sessions: SessionList, resurrectable_sessions: ResurrectableSessions, search_term: String, - new_session_name: Option<String>, + new_session_info: NewSessionInfo, renaming_session_name: Option<String>, error: Option<String>, - browsing_resurrection_sessions: bool, + active_screen: ActiveScreen, colors: Colors, + is_welcome_screen: bool, + show_kill_all_sessions_warning: bool, } register_plugin!(State); impl ZellijPlugin for State { - fn load(&mut self, _configuration: BTreeMap<String, String>) { + fn load(&mut self, configuration: BTreeMap<String, String>) { + self.is_welcome_screen = configuration + .get("welcome_screen") + .map(|v| v == "true") + .unwrap_or(false); + if self.is_welcome_screen { + self.active_screen = ActiveScreen::NewSession; + } subscribe(&[ EventType::ModeUpdate, EventType::SessionUpdate, @@ -55,6 +80,12 @@ impl ZellijPlugin for State { should_render = true; }, Event::SessionUpdate(session_infos, resurrectable_session_list) => { + for session_info in &session_infos { + if session_info.is_current_session { + self.new_session_info + .update_layout_list(session_info.available_layouts.clone()); + } + } self.resurrectable_sessions .update(resurrectable_session_list); self.update_session_infos(session_infos); @@ -66,36 +97,53 @@ impl ZellijPlugin for State { } fn render(&mut self, rows: usize, cols: usize) { - if self.browsing_resurrection_sessions { - self.resurrectable_sessions.render(rows, cols); - return; - } else if let Some(new_session_name) = self.renaming_session_name.as_ref() { - render_renaming_session_screen(&new_session_name, rows, cols); - return; + let (x, y, width, height) = self.main_menu_size(rows, cols); + + if self.is_welcome_screen { + render_banner(x, 0, rows.saturating_sub(height), width); } - render_resurrection_toggle(cols, false); - render_prompt( - self.new_session_name.is_some(), - &self.search_term, - 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(); + if let Key::Down = key { + self.new_session_info.handle_key(key); + should_render = true; + } else if let Key::Up = key { + self.new_session_info.handle_key(key); + should_render = true; + } else if let Key::Char(character) = key { + if character == '\n' { + self.handle_selection(); + } else { + self.new_session_info.handle_key(key); } should_render = true; - } else if let Key::Left = key { - if self.new_session_name.is_none() { - self.sessions.result_shrink(); + } 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 { - 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(); - } + self.sessions.move_selection_down(); 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.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.new_session_name.as_mut() { + } 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; - } 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(); + new_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; + self.active_screen = ActiveScreen::NewSession; + should_render = true; + } else if let Key::Ctrl('r') = key { + 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.new_session_name = Some(String::new()); + self.show_error("Must select session before killing it."); } should_render = true; - } else if let Key::Ctrl('r') = key { - if self.sessions.is_searching || self.browsing_resurrection_sessions { - // no-op - } else if self.renaming_session_name.is_some() { - self.renaming_session_name = None; + } else 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.renaming_session_name = Some(String::new()); + 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 { + } else 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; + 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 { - switch_session(Some(new_session_name)); + self.resurrectable_sessions.handle_character(character); } - } 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 + 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(); } - } 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); + } + should_render + } + fn handle_selection(&mut self) { + 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<String>) { + match self.entering_new_session_info { + EnteringState::EnteringLayoutSearch => { + let new_session_layout: Option<LayoutInfo> = self.selected_layout_info(); + let new_session_name = if self.name.is_empty() { + None + } else { + Some(self.name.as_str()) + }; + if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) { + match new_session_layout { + Some(new_session_layout) => { + switch_session_with_layout(new_session_name, new_session_layout) + }, + None => { + switch_session(new_session_name); + }, + } + } + self.name.clear(); + self.layout_list.clear_selection(); + hide_self(); + }, + EnteringState::EnteringName => { + self.entering_new_session_info = EnteringState::EnteringLayoutSearch; + }, + } + } + pub fn update_layout_list(&mut self, layout_info: Vec<LayoutInfo>) { + self.layout_list.update_layout_list(layout_info); + } + pub fn layout_list(&self) -> Vec<(LayoutInfo, bool)> { + // bool - is_selected + self.layout_list + .layout_list + .iter() + .enumerate() + .map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index)) + .collect() + } + pub fn layouts_to_render(&self) -> Vec<(LayoutInfo, Vec<usize>, bool)> { + // (layout_info, + // search_indices, + // is_selected) + if self.is_searching() { + self.layout_search_results() + .into_iter() + .map(|(layout_search_result, is_selected)| { + ( + layout_search_result.layout_info, + layout_search_result.indices, + is_selected, + ) + }) + .collect() + } else { + self.layout_list() + .into_iter() + .map(|(layout_info, is_selected)| (layout_info, vec![], is_selected)) + .collect() + } + } + pub fn layout_search_results(&self) -> Vec<(LayoutSearchResult, bool)> { + // bool - is_selected + self.layout_list + .layout_search_results + .iter() + .enumerate() + .map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index)) + .collect() + } + pub fn is_searching(&self) -> bool { + !self.layout_list.layout_search_term.is_empty() + } + pub fn layout_count(&self) -> usize { + self.layout_list.layout_list.len() + } + pub fn selected_layout_info(&self) -> Option<LayoutInfo> { + self.layout_list.selected_layout_info() + } + fn update_layout_search_term(&mut self) { + if self.layout_list.layout_search_term.is_empty() { + self.layout_list.clear_selection(); + self.layout_list.layout_search_results = vec![]; + } else { + let mut matches = vec![]; + let matcher = SkimMatcherV2::default().use_cache(true); + for layout_info in &self.layout_list.layout_list { + if let Some((score, indices)) = + matcher.fuzzy_indices(&layout_info.name(), &self.layout_list.layout_search_term) + { + matches.push(LayoutSearchResult { + layout_info: layout_info.clone(), + score, + indices, + }); + } + } + matches.sort_by(|a, b| b.score.cmp(&a.score)); + self.layout_list.layout_search_results = matches; + self.layout_list.clear_selection(); + } + } + fn move_selection_up(&mut self) { + self.layout_list.move_selection_up(); + } + fn move_selection_down(&mut self) { + self.layout_list.move_selection_down(); + } +} + +#[derive(Default)] +pub struct LayoutList { + layout_list: Vec<LayoutInfo>, + layout_search_results: Vec<LayoutSearchResult>, + selected_layout_index: usize, + layout_search_term: String, +} + +impl LayoutList { + pub fn update_layout_list(&mut self, layout_list: Vec<LayoutInfo>) { + let old_layout_length = self.layout_list.len(); + self.layout_list = layout_list; + if old_layout_length != self.layout_list.len() { + // honestly, this is just the UX choice that sucks the least... + self.clear_selection(); + } + } + pub fn selected_layout_info(&self) -> Option<LayoutInfo> { + if !self.layout_search_term.is_empty() { + self.layout_search_results + .get(self.selected_layout_index) + .map(|l| l.layout_info.clone()) + } else { + self.layout_list.get(self.selected_layout_index).cloned() + } + } + pub fn clear_selection(&mut self) { + self.selected_layout_index = 0; + } + fn max_index(&self) -> usize { + if self.layout_search_term.is_empty() { + self.layout_list.len().saturating_sub(1) + } else { + self.layout_search_results.len().saturating_sub(1) + } + } + fn move_selection_up(&mut self) { + let max_index = self.max_index(); + if self.selected_layout_index > 0 { + self.selected_layout_index -= 1; + } else { + self.selected_layout_index = max_index; + } + } + fn move_selection_down(&mut self) { + let max_index = self.max_index(); + if self.selected_layout_index < max_index { + self.selected_layout_index += 1; + } else { + self.selected_layout_index = 0; + } + } +} + +#[derive(Clone)] +pub struct LayoutSearchResult { + pub layout_info: LayoutInfo, + pub score: i64, + pub indices: Vec<usize>, +} 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, <DEL> - Delete Session, <Ctrl d> - Delete all sessions" - )) - .color_range(3, 6..10) - .color_range(3, 23..29) - .color_range(3, 47..56); - print_text_with_coordinates(controls_line, 0, rows.saturating_sub(1), None, None); - } pub fn move_selection_down(&mut self) { if self.is_searching { if let Some(selected_index) = self.selected_search_index.as_mut() { 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<String> { + self.session_ui_infos + .iter() + .filter_map(|s| { + if !s.is_current_session { + Some(s.name.clone()) + } else { + None + } + }) + .collect() + } } #[derive(Debug, Clone, Default)] 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\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 + ); + }, + } + } + 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\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 ); }, @@ -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 = "<TAB>"; - let running_sessions_text = "Running"; - let exited_sessions_text = "Exited"; + let (new_session_text, running_sessions_text, exited_sessions_text) = if max_cols > 66 { + ("New Session", "Attach to Session", "Resurrect Session") + } else { + ("New", "Attach", "Resurrect") + }; let key_indication_len = key_indication_text.chars().count() + 1; - let first_ribbon_length = running_sessions_text.chars().count() + 4; - let second_ribbon_length = exited_sessions_text.chars().count() + 4; - let key_indication_x = - cols.saturating_sub(key_indication_len + first_ribbon_length + second_ribbon_length); + let first_ribbon_length = new_session_text.chars().count() + 4; + let second_ribbon_length = running_sessions_text.chars().count() + 4; + let third_ribbon_length = exited_sessions_text.chars().count() + 4; + let total_len = + key_indication_len + first_ribbon_length + second_ribbon_length + third_ribbon_length; + let key_indication_x = x; let first_ribbon_x = key_indication_x + key_indication_len; let second_ribbon_x = first_ribbon_x + first_ribbon_length; + let third_ribbon_x = second_ribbon_x + second_ribbon_length; + let mut new_session_text = Text::new(new_session_text); + let mut running_sessions_text = Text::new(running_sessions_text); + let mut exited_sessions_text = Text::new(exited_sessions_text); + match active_screen { + ActiveScreen::NewSession => { + new_session_text = new_session_text.selected(); + }, + ActiveScreen::AttachToSession => { + running_sessions_text = running_sessions_text.selected(); + }, + ActiveScreen::ResurrectSession => { + exited_sessions_text = exited_sessions_text.selected(); + }, + } print_text_with_coordinates( Text::new(key_indication_text).color_range(3, ..), key_indication_x, - 0, + y, None, None, ); - if resurrection_screen_is_active { - print_ribbon_with_coordinates( - Text::new(running_sessions_text), - first_ribbon_x, - 0, - None, - None, - ); - print_ribbon_with_coordinates( - Text::new(exited_sessions_text).selected(), - second_ribbon_x, - 0, - None, - None, - ); - } 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<String>, is_searching: bool, colors: Colors) { - if is_searching { - return; - } - let new_session_shortcut_text = "<Ctrl w>"; - let new_session_shortcut = colors.magenta(new_session_shortcut_text); - let new_session = colors.bold("New session"); +pub fn render_new_session_block( + new_session_info: &NewSessionInfo, + colors: Colors, + max_rows_of_new_session_block: usize, + max_cols_of_new_session_block: usize, + x: usize, + y: usize, +) { let enter = colors.magenta("<ENTER>"); - 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() { + "<RANDOM>" + } else { + new_session_info.name() + }; + let prompt = "New session name:"; + let long_instruction = "to correct"; + let esc = colors.magenta("<ESC>"); + if max_cols_of_new_session_block + > prompt.width() + long_instruction.width() + new_session_name.width() + 15 + { + println!( + "\u{1b}[m{}{}: {} ({} to correct)", + format!("\u{1b}[{};{}H", y + 1, x + 1), + colors.green("New session name"), + colors.orange(new_session_name), + esc, + ); + } else { + println!( + "\u{1b}[m{}{}: {} {}", + format!("\u{1b}[{};{}H", y + 1, x + 1), + colors.green("New session name"), + colors.orange(new_session_name), + esc, + ); + } + render_layout_selection_list( + new_session_info, + max_rows_of_new_session_block.saturating_sub(1), + max_cols_of_new_session_block, + x, + y + 1, + ); } } -pub fn render_error(error_text: &str, rows: usize, columns: usize) { +pub fn render_layout_selection_list( + new_session_info: &NewSessionInfo, + max_rows_of_new_session_block: usize, + max_cols_of_new_session_block: usize, + x: usize, + y: usize, +) { + let layout_search_term = new_session_info.layout_search_term(); + let search_term_len = layout_search_term.width(); + let layout_indication_line = if max_cols_of_new_session_block > 73 + search_term_len { + Text::new(format!( + "New session layout: {}_ (Search and select from list, <ENTER> when done)", + layout_search_term + )) + .color_range(2, ..20 + search_term_len) + .color_range(3, 20..20 + search_term_len) + .color_range(3, 52 + search_term_len..59 + search_term_len) + } else { + Text::new(format!( + "New session layout: {}_ <ENTER>", + layout_search_term + )) + .color_range(2, ..20 + search_term_len) + .color_range(3, 20..20 + search_term_len) + .color_range(3, 22 + search_term_len..) + }; + print_text_with_coordinates(layout_indication_line, x, y + 1, None, None); + println!(); + let mut table = Table::new(); + for (i, (layout_info, indices, is_selected)) in + new_session_info.layouts_to_render().into_iter().enumerate() + { + let layout_name = layout_info.name(); + let layout_name_len = layout_name.width(); + let is_builtin = layout_info.is_builtin(); + if i > max_rows_of_new_session_block { + break; + } else { + let mut layout_cell = if is_builtin { + Text::new(format!("{} (built-in)", layout_name)) + .color_range(1, 0..layout_name_len) + .color_range(0, layout_name_len + 1..) + .color_indices(3, indices) + } else { + Text::new(format!("{}", layout_name)) + .color_range(1, ..) + .color_indices(3, indices) + }; + if is_selected { + layout_cell = layout_cell.selected(); + } + let arrow_cell = if is_selected { + Text::new(format!("<↓↑>")).selected().color_range(3, ..) + } else { + Text::new(format!(" ")).color_range(3, ..) + }; + table = table.add_styled_row(vec![arrow_cell, layout_cell]); + } + } + print_table_with_coordinates(table, x, y + 3, None, None); +} + +pub fn render_error(error_text: &str, rows: usize, columns: usize, x: usize, y: usize) { print_text_with_coordinates( Text::new(format!("Error: {}", error_text)).color_range(3, ..), - 0, - rows, + x, + y + rows, Some(columns), None, ); } -pub fn render_renaming_session_screen(new_session_name: &str, rows: usize, columns: usize) { +pub fn render_renaming_session_screen( + new_session_name: &str, + rows: usize, + columns: usize, + x: usize, + y: usize, +) { if rows == 0 || columns == 0 { return; } - let prompt_text = "NEW NAME FOR CURRENT SESSION"; - let new_session_name = format!("{}_", new_session_name); - let prompt_y_location = (rows / 2).saturating_sub(1); - let session_name_y_location = (rows / 2) + 1; - let prompt_x_location = columns.saturating_sub(prompt_text.chars().count()) / 2; - let session_name_x_location = columns.saturating_sub(new_session_name.chars().count()) / 2; - print_text_with_coordinates( - Text::new(prompt_text).color_range(0, ..), - prompt_x_location, - prompt_y_location, - None, - None, - ); - print_text_with_coordinates( - Text::new(new_session_name).color_range(3, ..), - session_name_x_location, - session_name_y_location, - None, - None, + let text = Text::new(format!( + "New name for current session: {}_ (<ENTER> when done)", + new_session_name + )) + .color_range(2, ..29) + .color_range( + 3, + 33 + new_session_name.width()..40 + new_session_name.width(), ); + print_text_with_coordinates(text, x, y, None, None); } -pub fn render_controls_line(is_searching: bool, row: usize, max_cols: usize, colors: Colors) { - let (arrows, navigate) = if is_searching { - (colors.magenta("<↓↑>"), colors.bold("Navigate")) - } else { - (colors.magenta("<←↓↑→>"), colors.bold("Navigate and Expand")) - }; - let rename = colors.magenta("<Ctrl r>"); - let rename_text = colors.bold("Rename session"); - let enter = colors.magenta("<ENTER>"); - let select = colors.bold("Switch to selected"); - let esc = colors.magenta("<ESC>"); - let to_hide = colors.bold("Hide"); +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("<ENTER>"); + let select = colors.bold("Attach"); + let rename = colors.magenta("<Ctrl r>"); + let rename_text = colors.bold("Rename"); + let disconnect = colors.magenta("<Ctrl x>"); + let disconnect_text = colors.bold("Disconnect others"); + let kill = colors.magenta("<Del>"); + let kill_text = colors.bold("Kill"); + let kill_all = colors.magenta("<Ctrl d>"); + let kill_all_text = colors.bold("Kill all"); - if max_cols >= 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("<ENTER>"); + let select = colors.bold("Resurrect"); + let del = colors.magenta("<DEL>"); + let del_text = colors.bold("Delete"); + let del_all = colors.magenta("<Ctrl d>"); + let del_all_text = colors.bold("Delete all"); + + if max_cols > 83 { + print!( + "\u{1b}[m\u{1b}[{y};{x}HHelp: {arrows} - {navigate}, {enter} - {select}, {del} - {del_text}, {del_all} - {del_all_text}" + ); + } else if max_cols >= 28 { + print!("\u{1b}[m\u{1b}[{y};{x}H{arrows}/{enter}/{del}/{del_all}"); + } + }, } } 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#" [38;2;0;0;0m + [38;2;0;0;0m + [38;2;0;0;0m [38;2;160;186;139m_[38;2;142;164;125my[38;2;148;171;129m$ [38;2;79;88;77my[38;2;151;175;132m@[38;2;131;151;117mg[38;2;163;189;141m_ + [38;2;0;0;0m [38;2;114;130;103my[38;2;156;181;136ma[38;2;159;184;138m@[38;2;163;189;141m@@@[38;2;73;82;73mL[38;2;103;117;95m4[38;2;163;189;141m@@@@[38;2;160;185;138mg[38;2;123;141;110my[38;2;110;125;100m_ + [38;2;0;0;0m [38;2;82;92;79mu[38;2;163;189;141m@@@@@@[38;2;160;185;138mF [38;2;85;96;82m"[38;2;143;165;126m@[38;2;163;189;141m@@@@@@[38;2;148;171;129m@[38;2;145;167;127my[38;2;162;188;140m_ + [38;2;0;0;0m [38;2;190;97;107m_[38;2;165;84;95ma[38;2;174;88;99m@[38;2;190;97;107m@[38;2;117;59;73m, [38;2;159;184;138m@[38;2;163;189;141m@[38;2;160;186;139m@[38;2;147;170;129mP[38;2;163;189;141m~[38;2;66;73;67m` [38;2;126;159;190m_[38;2;118;149;179m_ [38;2;106;121;97m~[38;2;120;138;108mT[38;2;135;156;120m@[38;2;163;189;141m@@@@@@[38;2;162;188;140m@[38;2;132;152;117mg + [38;2;0;0;0m [38;2;130;66;79m_[38;2;144;73;86my[38;2;187;95;105mg[38;2;189;96;106m@[38;2;190;97;107m@@@@[38;2;174;89;99m$ [38;2;118;136;107m"[38;2;128;147;114m~ [38;2;89;110;138m_[38;2;97;120;149my[38;2;124;156;187mg[38;2;125;158;189m@[38;2;126;159;190m@@[38;2;123;156;186m@[38;2;112;140;170mg[38;2;84;103;131my [38;2;120;138;108m`[38;2;162;188;140m~[38;2;128;148;115mP[38;2;146;168;128mR[38;2;135;155;119m@[38;2;139;160;122mF[38;2;158;183;137m~[38;2;233;202;138m_[38;2;196;170;119my[38;2;234;203;139mg[38;2;217;188;130mg[38;2;180;155;111my[38;2;166;143;104m_ + [38;2;0;0;0m [38;2;168;85;97my[38;2;174;89;99m$[38;2;190;97;107m@@@@@@[38;2;184;94;104mP[38;2;139;70;83mF [38;2;125;158;189m_[38;2;112;141;171my[38;2;116;146;176m$[38;2;126;159;190m@@@@@@@@@@[38;2;125;158;189mg[38;2;100;125;153my[38;2;101;127;155m_ [38;2;206;179;125m4[38;2;232;202;138m@[38;2;234;203;139m@@@@@[38;2;212;183;127m@[38;2;206;178;124my[38;2;145;124;93m_ + [38;2;0;0;0m [38;2;189;96;106mg[38;2;190;97;107m@@@@[38;2;178;91;101m@[38;2;168;85;97mF[38;2;187;95;105m~ [38;2;65;79;104m_[38;2;91;113;141my[38;2;120;151;182mg[38;2;124;157;188m@[38;2;126;159;190m@@@@@@@@@@@@@@@@[38;2;119;149;180m@[38;2;101;126;155mg[38;2;126;159;190m_ [38;2;200;173;121m~[38;2;180;156;111mF[38;2;217;188;130m@[38;2;234;203;139m@@@@[38;2;196;169;119m@ + [38;2;0;0;0m [38;2;183;93;104m$[38;2;190;97;107m@@@[38;2;189;97;107mF [38;2;103;128;157my[38;2;126;159;190mg@@@@@@@@@@@@@@@@@@@@@@@@[38;2;122;154;185mg[38;2;75;92;118my [38;2;206;178;124m9[38;2;234;203;139m@[38;2;199;172;121m@[38;2;193;167;118mF[38;2;106;90;73m" + [38;2;0;0;0m [38;2;182;93;103m$[38;2;190;97;107m@@@[38;2;130;66;79m$ [38;2;79;97;124m4[38;2;126;159;190m@@@@@[38;2;112;140;170m@[38;2;126;159;190m~[38;2;104;130;159m@[38;2;126;159;190m@@@@@@@@@@@@@@@@@@@@ [38;2;82;68;61m"[38;2;154;132;98m_[38;2;183;157;112my[38;2;226;196;135mg[38;2;208;180;126m$ + [38;2;0;0;0m [38;2;178;90;101m$[38;2;190;97;107m@@@[38;2;131;66;79m$ [38;2;80;98;125m4[38;2;126;159;190m@@@@[38;2;96;120;148m@ [38;2;108;136;165m~[38;2;105;132;161m@[38;2;126;159;190m@@@@@@@@@@@@@@@@@@ [38;2;168;144;105mg[38;2;234;203;139m@@@[38;2;233;202;138m@ + [38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;131;66;80m$ [38;2;80;98;125m4[38;2;126;159;190m@@@@@[38;2;118;148;178m@[38;2;91;113;141my [38;2;109;136;166m~[38;2;107;134;163m@[38;2;126;159;190m@@@@@@@@@@@@@@@@ [38;2;182;157;112m$[38;2;234;203;139m@@[38;2;233;202;138m@[38;2;224;194;134mF + [38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;132;67;80m$ [38;2;80;98;125m4[38;2;126;159;190m@@@@@@@[38;2;116;146;176m@[38;2;79;98;124my [38;2;109;137;166m~[38;2;112;141;171m@[38;2;126;159;190m@@@@@@@@@@@@@@ [38;2;140;120;90m`[38;2;189;163;116m~[38;2;234;203;139m_[38;2;161;138;101my[38;2;182;157;112m_ + [38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;134;68;81m$ [38;2;80;99;125m4[38;2;126;159;190m@@@@@@@[38;2;114;143;173mP[38;2;113;142;172m` [38;2;56;67;92m_[38;2;98;122;151mg[38;2;125;158;189m@[38;2;126;159;190m@@@@@@@@@@@@@@ [38;2;148;127;95my[38;2;234;203;139m@@@[38;2;213;184;128m@ + [38;2;0;0;0m [38;2;173;88;99m$[38;2;190;97;107m@@@[38;2;139;70;83m$ [38;2;80;99;126m4[38;2;126;159;190m@@@@@[38;2;117;147;177mP[38;2;119;150;180m` [38;2;52;62;86m_[38;2;109;137;166my[38;2;125;158;189m@[38;2;126;159;190m@@@@@@@@@@@@@@@@ [38;2;163;140;102m4[38;2;234;203;139m@@@[38;2;216;187;129m@ + [38;2;0;0;0m [38;2;174;88;99m$[38;2;190;97;107m@@@[38;2;143;73;85m$ [38;2;80;99;126m4[38;2;126;159;190m@@@@[38;2;104;129;158m$[38;2;54;64;89m_ [38;2;108;136;165my[38;2;125;158;189m@[38;2;126;159;190m@@@[38;2;110;139;168m@[38;2;126;159;190m [38;2;114;144;174m$[38;2;126;159;190m@@@@ [38;2;136;116;88m4[38;2;234;203;139m@@@[38;2;218;189;131m@ + [38;2;0;0;0m [38;2;174;88;99m$[38;2;190;97;107m@@@[38;2;145;73;86m$ [38;2;81;99;126m4[38;2;126;159;190m@@@@@@[38;2;113;142;172mg[38;2;124;157;188m@[38;2;126;159;190m@@@@@[38;2;125;158;188m@[38;2;107;135;164m@[38;2;107;134;163m@@@@@@@[38;2;108;136;165m@[38;2;121;153;184m@[38;2;126;159;190m@@@@ [38;2;198;171;120m~[38;2;208;180;125m@[38;2;234;203;139m@[38;2;220;191;132m@ + [38;2;0;0;0m [38;2;174;89;99m$[38;2;190;97;107m@@[38;2;184;93;104mP [38;2;91;113;141m7[38;2;109;137;166mR[38;2;126;159;190m@@@@@@@@@@@@@@@@@@@@@@@[38;2;123;155;186m@[38;2;111;139;169mP[38;2;84;104;131m~ [38;2;117;134;106m_[38;2;129;149;115m@[38;2;140;162;124mg[38;2;83;93;80m_[38;2;174;150;108m7[38;2;222;192;133m@ + [38;2;0;0;0m [38;2;145;73;86m~[38;2;168;85;97m~[38;2;140;162;124m_[38;2;125;144;112my[38;2;158;183;137mg[38;2;157;182;137m@[38;2;146;169;128mg[38;2;111;127;101my[38;2;61;67;64m_ [38;2;115;145;175m~[38;2;91;113;141m5[38;2;121;152;182m@[38;2;126;159;190m@@@@@@@@@@@@@@@@[38;2;108;136;165m@[38;2;105;131;160mF[38;2;98;122;151m~ [38;2;145;167;127m_[38;2;130;150;116my[38;2;162;188;140mg[38;2;163;189;141m@@@@[38;2;137;158;121my + [38;2;0;0;0m [38;2;140;161;123mR[38;2;163;189;141m@@@@@@[38;2;113;129;102m@ [38;2;82;101;128m~[38;2;126;159;190m~[38;2;101;126;154m@[38;2;126;159;190m@@@@@@@@@[38;2;125;158;189m@[38;2;118;148;178mP[38;2;126;159;190m~[38;2;84;103;130m` [38;2;112;127;102my[38;2;153;177;133ma[38;2;154;179;134m@[38;2;163;189;141m@@@@@@[38;2;132;152;117m@[38;2;117;134;106mF + [38;2;0;0;0m [38;2;147;170;129m~[38;2;116;132;105m5[38;2;153;177;133m@[38;2;163;189;141m@@@ [38;2;117;134;106m4[38;2;160;185;138m@[38;2;144;166;126mg[38;2;107;122;98my [38;2;62;74;99m`[38;2;122;154;184m~[38;2;109;137;167m4[38;2;124;157;188m@[38;2;126;159;190m@@[38;2;114;143;173m@[38;2;109;137;166mF[38;2;115;145;175m~ [38;2;122;140;110my[38;2;155;179;135m@[38;2;163;189;141m@@@@@[38;2;160;186;139m@[38;2;146;169;128mP[38;2;163;189;141m~[38;2;61;67;63m` + [38;2;0;0;0m [38;2;162;188;140m`[38;2;163;189;141m~[38;2;143;165;126mP [38;2;99;112;92m4[38;2;163;189;141m@@@@[38;2;162;187;140mg[38;2;127;145;113my[38;2;120;137;108m_ [38;2;105;131;160m`[38;2;104;130;159m` [38;2;133;154;118m_[38;2;130;149;116my[38;2;126;145;113mr [38;2;137;158;121ma[38;2;163;189;141m@@@@[38;2;144;167;127m@[38;2;139;160;122mF[38;2;138;159;122m~ + [38;2;0;0;0m [38;2;116;132;105m5[38;2;154;178;134m@[38;2;163;189;141m@@@@@[38;2;152;176;133m@[38;2;163;189;141mgg[38;2;147;169;128m@[38;2;161;187;140m@[38;2;148;171;129m~[38;2;103;117;95m_[38;2;162;188;140mg[38;2;163;189;141m@@[38;2;122;140;110m@[38;2;163;189;141m~[38;2;140;161;123m` + [38;2;0;0;0m [38;2;95;108;89m~[38;2;163;189;141m~[38;2;128;147;114m@[38;2;163;189;141m@@@@@[38;2;145;168;127m@[38;2;82;92;79m^[38;2;129;149;115my[38;2;155;180;135m@[38;2;145;168;127mF[38;2;160;185;139m~ + [38;2;0;0;0m [38;2;71;78;71m`[38;2;157;181;136m~[38;2;140;162;124m4[38;2;149;173;131m@[38;2;151;175;132mF [38;2;108;124;99m~ + [38;2;0;0;0m + [38;2;0;0;0m "#; |
