diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | default-plugins/session-manager/Cargo.toml | 1 | ||||
-rw-r--r-- | default-plugins/session-manager/src/main.rs | 52 | ||||
-rw-r--r-- | default-plugins/session-manager/src/resurrectable_sessions.rs | 338 | ||||
-rw-r--r-- | default-plugins/session-manager/src/ui/components.rs | 53 | ||||
-rw-r--r-- | zellij-server/src/background_jobs.rs | 200 | ||||
-rw-r--r-- | zellij-server/src/plugins/zellij_exports.rs | 54 | ||||
-rw-r--r-- | zellij-server/src/screen.rs | 24 | ||||
-rw-r--r-- | zellij-tile/src/shim.rs | 16 | ||||
-rw-r--r-- | zellij-utils/assets/prost/api.event.rs | 10 | ||||
-rw-r--r-- | zellij-utils/assets/prost/api.plugin_command.rs | 10 | ||||
-rw-r--r-- | zellij-utils/src/data.rs | 8 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/event.proto | 6 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/event.rs | 43 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.proto | 3 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.rs | 15 |
16 files changed, 737 insertions, 97 deletions
diff --git a/Cargo.lock b/Cargo.lock index f471a020c..8afdcd0e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3325,6 +3325,7 @@ dependencies = [ "ansi_term", "chrono", "fuzzy-matcher", + "humantime", "unicode-width", "zellij-tile", ] diff --git a/default-plugins/session-manager/Cargo.toml b/default-plugins/session-manager/Cargo.toml index c2030f961..97100ed69 100644 --- a/default-plugins/session-manager/Cargo.toml +++ b/default-plugins/session-manager/Cargo.toml @@ -10,3 +10,4 @@ zellij-tile = { path = "../../zellij-tile" } chrono = "0.4.0" fuzzy-matcher = "0.3.7" unicode-width = "0.1.10" +humantime = "2.1.0" diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index bd0b1f2b7..7e39307d9 100644 --- a/default-plugins/session-manager/src/main.rs +++ b/default-plugins/session-manager/src/main.rs @@ -1,3 +1,4 @@ +mod resurrectable_sessions; mod session_list; mod ui; use zellij_tile::prelude::*; @@ -5,18 +6,24 @@ use zellij_tile::prelude::*; use std::collections::BTreeMap; use ui::{ - components::{render_controls_line, render_new_session_line, render_prompt, Colors}, + components::{ + render_controls_line, render_new_session_line, render_prompt, render_resurrection_toggle, + Colors, + }, SessionUiInfo, }; +use resurrectable_sessions::ResurrectableSessions; use session_list::SessionList; #[derive(Default)] struct State { session_name: Option<String>, sessions: SessionList, + resurrectable_sessions: ResurrectableSessions, search_term: String, new_session_name: Option<String>, + browsing_resurrection_sessions: bool, colors: Colors, } @@ -45,7 +52,9 @@ impl ZellijPlugin for State { Event::PermissionRequestResult(_result) => { should_render = true; }, - Event::SessionUpdate(session_infos) => { + Event::SessionUpdate(session_infos, resurrectable_session_list) => { + self.resurrectable_sessions + .update(resurrectable_session_list); self.update_session_infos(session_infos); should_render = true; }, @@ -55,6 +64,11 @@ impl ZellijPlugin for State { } fn render(&mut self, rows: usize, cols: usize) { + if self.browsing_resurrection_sessions { + self.resurrectable_sessions.render(rows, cols); + return; + } + render_resurrection_toggle(cols, false); render_prompt( self.new_session_name.is_some(), &self.search_term, @@ -94,12 +108,16 @@ impl State { } should_render = true; } else if let Key::Down = key { - if self.new_session_name.is_none() { + if self.browsing_resurrection_sessions { + self.resurrectable_sessions.move_selection_down(); + } else if self.new_session_name.is_none() { self.sessions.move_selection_down(); } should_render = true; } else if let Key::Up = key { - if self.new_session_name.is_none() { + if self.browsing_resurrection_sessions { + self.resurrectable_sessions.move_selection_up(); + } else if self.new_session_name.is_none() { self.sessions.move_selection_up(); } should_render = true; @@ -108,6 +126,8 @@ impl State { self.handle_selection(); } else if let Some(new_session_name) = self.new_session_name.as_mut() { new_session_name.push(character); + } else if self.browsing_resurrection_sessions { + self.resurrectable_sessions.handle_character(character); } else { self.search_term.push(character); self.sessions @@ -121,6 +141,8 @@ impl State { } else { new_session_name.pop(); } + } else if self.browsing_resurrection_sessions { + self.resurrectable_sessions.handle_backspace(); } else { self.search_term.pop(); self.sessions @@ -153,13 +175,33 @@ impl State { hide_self(); } should_render = true; + } else if let Key::BackTab = key { + self.browsing_resurrection_sessions = !self.browsing_resurrection_sessions; + 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 { hide_self(); } should_render } fn handle_selection(&mut self) { - if let Some(new_session_name) = &self.new_session_name { + 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) { diff --git a/default-plugins/session-manager/src/resurrectable_sessions.rs b/default-plugins/session-manager/src/resurrectable_sessions.rs new file mode 100644 index 000000000..beed254fd --- /dev/null +++ b/default-plugins/session-manager/src/resurrectable_sessions.rs @@ -0,0 +1,338 @@ +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::*; + +#[derive(Debug, Default)] +pub struct ResurrectableSessions { + pub all_resurrectable_sessions: Vec<(String, Duration)>, + pub selected_index: Option<usize>, + pub selected_search_index: Option<usize>, + pub search_results: Vec<SearchResult>, + pub is_searching: bool, + pub search_term: String, + pub delete_all_dead_sessions_warning: bool, +} + +impl ResurrectableSessions { + pub fn update(&mut self, mut list: Vec<(String, Duration)>) { + list.sort_by(|a, b| a.1.cmp(&b.1)); + self.all_resurrectable_sessions = list; + } + pub fn render(&self, rows: usize, columns: usize) { + if self.delete_all_dead_sessions_warning { + self.render_delete_all_sessions_warning(rows, columns); + 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 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); + } + fn render_search_results(&self, table_rows: usize, _table_columns: usize) -> Table { + let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row + let (first_row_index_to_render, last_row_index_to_render) = self.range_to_render( + table_rows, + self.search_results.len(), + self.selected_search_index, + ); + for i in first_row_index_to_render..last_row_index_to_render { + if let Some(search_result) = self.search_results.get(i) { + let is_selected = Some(i) == self.selected_search_index; + let mut table_cells = vec![ + self.render_session_name( + &search_result.session_name, + Some(search_result.indices.clone()), + ), + self.render_ctime(&search_result.ctime), + self.render_more_indication_or_enter_as_needed( + i, + first_row_index_to_render, + last_row_index_to_render, + self.search_results.len(), + is_selected, + ), + ]; + if is_selected { + table_cells = table_cells.drain(..).map(|t| t.selected()).collect(); + } + table = table.add_styled_row(table_cells); + } + } + table + } + fn render_all_entries(&self, table_rows: usize, _table_columns: usize) -> Table { + let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row + let (first_row_index_to_render, last_row_index_to_render) = self.range_to_render( + table_rows, + self.all_resurrectable_sessions.len(), + self.selected_index, + ); + for i in first_row_index_to_render..last_row_index_to_render { + if let Some(session) = self.all_resurrectable_sessions.get(i) { + let is_selected = Some(i) == self.selected_index; + let mut table_cells = vec![ + self.render_session_name(&session.0, None), + self.render_ctime(&session.1), + self.render_more_indication_or_enter_as_needed( + i, + first_row_index_to_render, + last_row_index_to_render, + self.all_resurrectable_sessions.len(), + is_selected, + ), + ]; + if is_selected { + table_cells = table_cells.drain(..).map(|t| t.selected()).collect(); + } + table = table.add_styled_row(table_cells); + } + } + table + } + fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize) { + if rows == 0 || columns == 0 { + return; + } + let session_count = self.all_resurrectable_sessions.len(); + let session_count_len = session_count.to_string().chars().count(); + 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_x_location = + columns.saturating_sub(warning_description_text.chars().count()) / 2; + let confirmation_x_location = 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, + 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, + ); + } + fn range_to_render( + &self, + table_rows: usize, + results_len: usize, + selected_index: Option<usize>, + ) -> (usize, usize) { + if table_rows <= results_len { + let row_count_to_render = table_rows.saturating_sub(1); // 1 for the title + let first_row_index_to_render = selected_index + .unwrap_or(0) + .saturating_sub(row_count_to_render / 2); + let last_row_index_to_render = first_row_index_to_render + row_count_to_render; + (first_row_index_to_render, last_row_index_to_render) + } else { + let first_row_index_to_render = 0; + let last_row_index_to_render = results_len; + (first_row_index_to_render, last_row_index_to_render) + } + } + fn render_session_name(&self, session_name: &str, indices: Option<Vec<usize>>) -> Text { + let text = Text::new(&session_name).color_range(0, ..); + match indices { + Some(indices) => text.color_indices(1, indices), + None => text, + } + } + fn render_ctime(&self, ctime: &Duration) -> Text { + let duration = format_duration(ctime.clone()).to_string(); + let duration_parts = duration.split_whitespace(); + let mut formatted_duration = String::new(); + for part in duration_parts { + if !part.ends_with('s') { + if !formatted_duration.is_empty() { + formatted_duration.push(' '); + } + formatted_duration.push_str(part); + } + } + if formatted_duration.is_empty() { + formatted_duration.push_str("<1m"); + } + let duration_len = formatted_duration.chars().count(); + Text::new(format!("Created {} ago", formatted_duration)).color_range(2, 8..9 + duration_len) + } + fn render_more_indication_or_enter_as_needed( + &self, + i: usize, + first_row_index_to_render: usize, + last_row_index_to_render: usize, + results_len: usize, + is_selected: bool, + ) -> Text { + if is_selected { + Text::new(format!("<ENTER> - Resurrect Session")).color_range(3, 0..7) + } else if i == first_row_index_to_render && i > 0 { + Text::new(format!("+ {} more", first_row_index_to_render)).color_range(1, ..) + } else if i == last_row_index_to_render.saturating_sub(1) + && last_row_index_to_render < results_len + { + Text::new(format!( + "+ {} more", + results_len.saturating_sub(last_row_index_to_render) + )) + .color_range(1, ..) + } else { + 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() { + if *selected_index == self.search_results.len().saturating_sub(1) { + *selected_index = 0; + } else { + *selected_index = *selected_index + 1; + } + } else { + self.selected_search_index = Some(0); + } + } else { + if let Some(selected_index) = self.selected_index.as_mut() { + if *selected_index == self.all_resurrectable_sessions.len().saturating_sub(1) { + *selected_index = 0; + } else { + *selected_index = *selected_index + 1; + } + } else { + self.selected_index = Some(0); + } + } + } + pub fn move_selection_up(&mut self) { + if self.is_searching { + if let Some(selected_index) = self.selected_search_index.as_mut() { + if *selected_index == 0 { + *selected_index = self.search_results.len().saturating_sub(1); + } else { + *selected_index = selected_index.saturating_sub(1); + } + } else { + self.selected_search_index = Some(self.search_results.len().saturating_sub(1)); + } + } else { + if let Some(selected_index) = self.selected_index.as_mut() { + if *selected_index == 0 { + *selected_index = self.all_resurrectable_sessions.len().saturating_sub(1); + } else { + *selected_index = selected_index.saturating_sub(1); + } + } else { + self.selected_index = Some(self.all_resurrectable_sessions.len().saturating_sub(1)); + } + } + } + pub fn get_selected_session_name(&self) -> Option<String> { + if self.is_searching { + self.selected_search_index + .and_then(|i| self.search_results.get(i)) + .map(|search_result| search_result.session_name.clone()) + } else { + self.selected_index + .and_then(|i| self.all_resurrectable_sessions.get(i)) + .map(|session_name_and_creation_time| session_name_and_creation_time.0.clone()) + } + } + pub fn delete_selected_session(&mut self) { + self.selected_index + .and_then(|i| { + if self.all_resurrectable_sessions.len() > i { + // optimistic update + if i == 0 { + self.selected_index = None; + } else if i == self.all_resurrectable_sessions.len().saturating_sub(1) { + self.selected_index = Some(i.saturating_sub(1)); + } + Some(self.all_resurrectable_sessions.remove(i)) + } else { + None + } + }) + .map(|session_name_and_creation_time| { + delete_dead_session(&session_name_and_creation_time.0) + }); + } + fn delete_all_sessions(&mut self) { + // optimistic update + self.all_resurrectable_sessions = vec![]; + self.delete_all_dead_sessions_warning = false; + delete_all_dead_sessions(); + } + pub fn show_delete_all_sessions_warning(&mut self) { + self.delete_all_dead_sessions_warning = true; + } + pub fn handle_character(&mut self, character: char) { + if self.delete_all_dead_sessions_warning && character == 'y' { + self.delete_all_sessions(); + } else if self.delete_all_dead_sessions_warning && character == 'n' { + self.delete_all_dead_sessions_warning = false; + } else { + self.search_term.push(character); + self.update_search_term(); + } + } + pub fn handle_backspace(&mut self) { + self.search_term.pop(); + self.update_search_term(); + } + fn update_search_term(&mut self) { + let mut matches = vec![]; + let matcher = SkimMatcherV2::default().use_cache(true); + for (session_name, ctime) in &self.all_resurrectable_sessions { + if let Some((score, indices)) = matcher.fuzzy_indices(&session_name, &self.search_term) + { + matches.push(SearchResult { + session_name: session_name.to_owned(), + ctime: ctime.clone(), + score, + indices, + }); + } + } + matches.sort_by(|a, b| b.score.cmp(&a.score)); + self.search_results = matches; + self.is_searching = !self.search_term.is_empty(); + self.selected_search_index = Some(0); + } +} + +#[derive(Debug)] +pub struct SearchResult { + score: i64, + indices: Vec<usize>, + session_name: String, + ctime: Duration, +} diff --git a/default-plugins/session-manager/src/ui/components.rs b/default-plugins/session-manager/src/ui/components.rs index 72aa21812..be3da41df 100644 --- a/default-plugins/session-manager/src/ui/components.rs +++ b/default-plugins/session-manager/src/ui/components.rs @@ -478,12 +478,63 @@ pub fn minimize_lines( 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!("{}\n", prompt); + println!("\u{1b}[H{}\n", prompt); } else { println!("\n"); } } +pub fn render_resurrection_toggle(cols: usize, resurrection_screen_is_active: bool) { + let key_indication_text = "<TAB>"; + let running_sessions_text = "Running"; + let exited_sessions_text = "Exited"; + 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_x = key_indication_x + key_indication_len; + let second_ribbon_x = first_ribbon_x + first_ribbon_length; + print_text_with_coordinates( + Text::new(key_indication_text).color_range(3, ..), + key_indication_x, + 0, + 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, + ); + } +} + pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool, colors: Colors) { if is_searching { return; diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index 3f2777b76..5fa6ffcb5 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -1,12 +1,11 @@ use zellij_utils::async_std::task; use zellij_utils::consts::{ session_info_cache_file_name, session_info_folder_for_session, session_layout_cache_file_name, - ZELLIJ_SOCK_DIR, + ZELLIJ_SESSION_INFO_CACHE_DIR, |