summaryrefslogtreecommitdiffstats
path: root/default-plugins/session-manager
diff options
context:
space:
mode:
Diffstat (limited to 'default-plugins/session-manager')
-rw-r--r--default-plugins/session-manager/Cargo.toml1
-rw-r--r--default-plugins/session-manager/src/main.rs52
-rw-r--r--default-plugins/session-manager/src/resurrectable_sessions.rs338
-rw-r--r--default-plugins/session-manager/src/ui/components.rs53
4 files changed, 438 insertions, 6 deletions
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;