aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/session-manager
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-01-29 11:53:56 +0100
committerGitHub <noreply@github.com>2026-01-29 11:53:56 +0100
commitcdbdf2b6679c580d4b0c8332694cd8ee9117d49f (patch)
treef262277077de2891c3952663f69f6906356c4e04 /default-plugins/session-manager
parentfix: session creation time on musl (for "zellij ls") (#4645) (diff)
downloadsl0ppij-cdbdf2b6679c580d4b0c8332694cd8ee9117d49f.tar.gz
sl0ppij-cdbdf2b6679c580d4b0c8332694cd8ee9117d49f.tar.xz
sl0ppij-cdbdf2b6679c580d4b0c8332694cd8ee9117d49f.zip
Allow saving current session for resurrection on demand (#4654)
* initial implementation * session manager ui and plugin api * remove event * cleanups and formatting * add pr
Diffstat (limited to 'default-plugins/session-manager')
-rw-r--r--default-plugins/session-manager/src/main.rs45
-rw-r--r--default-plugins/session-manager/src/ui/components.rs115
2 files changed, 154 insertions, 6 deletions
diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs
index c89b0049..1a76fdb9 100644
--- a/default-plugins/session-manager/src/main.rs
+++ b/default-plugins/session-manager/src/main.rs
@@ -10,7 +10,7 @@ use new_session_info::NewSessionInfo;
use ui::{
components::{
render_controls_line, render_error, render_new_session_block, render_prompt,
- render_renaming_session_screen, render_screen_toggle, Colors,
+ render_renaming_session_screen, render_screen_toggle, render_unsaved_changes_line, Colors,
},
welcome_screen::{render_banner, render_welcome_boundaries},
SessionUiInfo,
@@ -19,7 +19,7 @@ use ui::{
use resurrectable_sessions::ResurrectableSessions;
use session_list::SessionList;
-#[derive(Clone, Debug, Copy)]
+#[derive(Clone, Debug, Copy, PartialEq)]
enum ActiveScreen {
NewSession,
AttachToSession,
@@ -47,6 +47,7 @@ struct State {
show_kill_all_sessions_warning: bool,
request_ids: Vec<String>,
is_web_client: bool,
+ current_session_last_saved_time: Option<u64>,
}
register_plugin!(State);
@@ -61,11 +62,16 @@ impl ZellijPlugin for State {
self.active_screen = ActiveScreen::NewSession;
}
self.new_session_info.is_welcome_screen = self.is_welcome_screen;
+
+ self.current_session_last_saved_time = current_session_last_saved_time();
+ set_timeout(1.0);
+
subscribe(&[
EventType::ModeUpdate,
EventType::SessionUpdate,
EventType::Key,
EventType::RunCommandResult,
+ EventType::Timer,
]);
}
@@ -94,6 +100,11 @@ impl ZellijPlugin for State {
fn update(&mut self, event: Event) -> bool {
let mut should_render = false;
match event {
+ Event::Timer(_) => {
+ self.current_session_last_saved_time = current_session_last_saved_time();
+ set_timeout(1.0);
+ should_render = true;
+ },
Event::ModeUpdate(mode_info) => {
self.colors = Colors::new(mode_info.style.colors);
self.is_web_client = mode_info.is_web_client.unwrap_or(false);
@@ -156,7 +167,8 @@ impl ZellijPlugin for State {
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;
+ let bottom_lines = 7;
+ let room_for_list = height.saturating_sub(bottom_lines);
self.sessions.update_rows(room_for_list);
let list =
self.sessions
@@ -172,8 +184,25 @@ impl ZellijPlugin for State {
}
if let Some(error) = self.error.as_ref() {
render_error(&error, height, width, x, y);
+ } else if self.active_screen == ActiveScreen::AttachToSession && !self.is_welcome_screen {
+ let help_offset = render_controls_line(
+ self.active_screen,
+ width,
+ self.colors,
+ x,
+ rows.saturating_sub(1),
+ );
+ // Adjust position and width based on whether controls line shows "Help: "
+ let adjusted_x = x + help_offset;
+ let adjusted_width = width.saturating_sub(help_offset);
+ render_unsaved_changes_line(
+ adjusted_width,
+ adjusted_x,
+ rows,
+ self.current_session_last_saved_time,
+ );
} else {
- render_controls_line(self.active_screen, width, self.colors, x + 1, rows);
+ let _ = render_controls_line(self.active_screen, width, self.colors, x, rows);
}
if self.is_welcome_screen {
render_welcome_boundaries(rows, cols); // explicitly done in the end to override some
@@ -395,6 +424,14 @@ impl State {
hide_self();
}
},
+ BareKey::Char('a') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ if !self.is_welcome_screen {
+ // we don't want to save welcome screen sessions
+ if let Err(e) = save_session() {
+ self.show_error(&format!("Couldn't save session: {}", e));
+ }
+ }
+ },
_ => {},
}
}
diff --git a/default-plugins/session-manager/src/ui/components.rs b/default-plugins/session-manager/src/ui/components.rs
index dede06d9..8310d2d3 100644
--- a/default-plugins/session-manager/src/ui/components.rs
+++ b/default-plugins/session-manager/src/ui/components.rs
@@ -1,4 +1,6 @@
+use humantime::format_duration;
use std::path::PathBuf;
+use std::time::Duration;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
@@ -927,13 +929,19 @@ pub fn render_controls_line(
colors: Colors,
x: usize,
y: usize,
-) {
- match active_screen {
+) -> usize {
+ const HELP_PREFIX_LEN: usize = 6;
+ let x = x + 1;
+
+ let shows_help_prefix = 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."
);
+ true
+ } else {
+ false
}
},
ActiveScreen::AttachToSession => {
@@ -950,8 +958,12 @@ pub fn render_controls_line(
print!(
"\u{1b}[m\u{1b}[{y};{x}HHelp: {rename} - {rename_text}, {disconnect} - {disconnect_text}, {kill} - {kill_text}, {kill_all} - {kill_all_text}"
);
+ true
} else if max_cols >= 28 {
print!("\u{1b}[m\u{1b}[{y};{x}H{rename}/{disconnect}/{kill}/{kill_all}");
+ false
+ } else {
+ false
}
},
ActiveScreen::ResurrectSession => {
@@ -968,13 +980,112 @@ pub fn render_controls_line(
print!(
"\u{1b}[m\u{1b}[{y};{x}HHelp: {arrows} - {navigate}, {enter} - {select}, {del} - {del_text}, {del_all} - {del_all_text}"
);
+ true
} else if max_cols >= 28 {
print!("\u{1b}[m\u{1b}[{y};{x}H{arrows}/{enter}/{del}/{del_all}");
+ false
+ } else {
+ false
}
},
+ };
+
+ if shows_help_prefix {
+ HELP_PREFIX_LEN
+ } else {
+ 0
+ }
+}
+
+fn format_elapsed_time(elapsed_millis: u64) -> String {
+ // Convert elapsed milliseconds to Duration
+ let elapsed_duration = Duration::from_millis(elapsed_millis);
+
+ // Use humantime to format the duration, same as in resurrectable_sessions.rs
+ let duration_str = format_duration(elapsed_duration).to_string();
+ let duration_parts = duration_str.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() {
+ format!("just now")
+ } else {
+ format!("{} ago", formatted_duration)
}
}
+pub fn render_unsaved_changes_line(
+ max_cols: usize,
+ x: usize,
+ y: usize,
+ last_saved_timestamp: Option<u64>,
+) {
+ // Declare all text components
+ let shortcut_text = "<Ctrl a>";
+ let full_action_text = "Save current session for resurrection";
+ let medium_action_text = "Save session";
+ let short_action_text = "Save";
+ let separator = " - ";
+ let space = " ";
+
+ let time_text = match last_saved_timestamp {
+ Some(timestamp) => {
+ let elapsed = format_elapsed_time(timestamp);
+ format!("(saved {})", elapsed)
+ },
+ None => "(not saved)".to_string(),
+ };
+
+ // Calculate component widths
+ let shortcut_width = shortcut_text.width();
+ let separator_width = separator.width();
+ let space_width = space.width();
+ let time_width = time_text.width();
+
+ // Calculate total widths for each display mode
+ // Format: "{shortcut}{separator}{action}{space}{time}"
+ let full_width =
+ shortcut_width + separator_width + full_action_text.width() + space_width + time_width;
+ let medium_width =
+ shortcut_width + separator_width + medium_action_text.width() + space_width + time_width;
+ let short_width =
+ shortcut_width + separator_width + short_action_text.width() + space_width + time_width;
+ let minimal_width = shortcut_width + space_width + time_width;
+
+ // Select appropriate message based on available width
+ let msg = if max_cols >= full_width {
+ format!(
+ "{}{}{}{}{}",
+ shortcut_text, separator, full_action_text, space, time_text
+ )
+ } else if max_cols >= medium_width {
+ format!(
+ "{}{}{}{}{}",
+ shortcut_text, separator, medium_action_text, space, time_text
+ )
+ } else if max_cols >= short_width {
+ format!(
+ "{}{}{}{}{}",
+ shortcut_text, separator, short_action_text, space, time_text
+ )
+ } else if max_cols >= minimal_width {
+ format!("{}{}{}", shortcut_text, space, time_text)
+ } else {
+ return; // Not enough space to render
+ };
+
+ let text = Text::new(&msg)
+ .color_substring(3, shortcut_text)
+ .color_substring(2, &time_text);
+ print_text_with_coordinates(text, x, y, None, None);
+}
+
// Maps the various prompts and UI elements to the colors to present them with
//
// Since this plugin predates the UI components, this is a developer