From cdbdf2b6679c580d4b0c8332694cd8ee9117d49f Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Thu, 29 Jan 2026 11:53:56 +0100 Subject: [PATCH] Allow saving current session for resurrection on demand (#4654) * initial implementation * session manager ui and plugin api * remove event * cleanups and formatting * add pr --- CHANGELOG.md | 1 + default-plugins/session-manager/src/main.rs | 45 ++++++- .../session-manager/src/ui/components.rs | 115 +++++++++++++++++- zellij-server/src/background_jobs.rs | 18 ++- zellij-server/src/plugins/mod.rs | 16 +++ zellij-server/src/plugins/wasm_bridge.rs | 2 + zellij-server/src/plugins/zellij_exports.rs | 69 ++++++++++- zellij-server/src/pty.rs | 45 ++++++- zellij-server/src/route.rs | 8 ++ zellij-server/src/screen.rs | 59 ++++++++- zellij-tile/src/shim.rs | 76 +++++++++++- .../assets/prost/api.plugin_command.rs | 44 ++++++- .../prost_ipc/client_server_contract.rs | 8 +- zellij-utils/src/cli.rs | 2 + .../client_server_contract/common_types.proto | 2 + zellij-utils/src/data.rs | 2 + zellij-utils/src/errors.rs | 4 + zellij-utils/src/input/actions.rs | 3 + zellij-utils/src/ipc/protobuf_conversion.rs | 24 ++-- zellij-utils/src/plugin_api/action.rs | 3 +- zellij-utils/src/plugin_api/event.proto | 1 + .../src/plugin_api/plugin_command.proto | 19 +++ zellij-utils/src/plugin_api/plugin_command.rs | 31 +++-- 23 files changed, 564 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e151d242d..67be0769c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix: resolve short layout names as files if possible (https://github.com/zellij-org/zellij/pull/4640) * fix: restore focused tab position on reattaching to a detached session (https://github.com/zellij-org/zellij/pull/4115) * fix: session creation time (all session created 0s ago bug) on musl targets (https://github.com/zellij-org/zellij/pull/4645) +* feat: allow explicitly saving current session for resurrection (rather than waiting on the resurrection interval) (https://github.com/zellij-org/zellij/pull/4654) ## [0.43.1] - 2025-08-08 diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index c89b00493..1a76fdb9f 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, is_web_client: bool, + current_session_last_saved_time: Option, } 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 dede06d9c..8310d2d39 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, +) { + // Declare all text components + let shortcut_text = ""; + 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 diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index 22828e156..371f99b93 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -190,6 +190,15 @@ pub(crate) fn background_jobs_main( }, BackgroundJob::ReportLayoutInfo(session_layout) => { *current_session_layout.lock().unwrap() = session_layout; + + // Update session save time for plugin query + let timestamp_millis = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + let _ = bus + .senders + .send_to_plugin(PluginInstruction::UpdateSessionSaveTime(timestamp_millis)); }, BackgroundJob::ReadAllSessionInfosOnMachine => { // this job should only be run once and it keeps track of other sessions (as well @@ -220,6 +229,13 @@ pub(crate) fn background_jobs_main( current_session_info, current_session_layout, ); + + // Send SavedCurrentSession instruction to plugin thread + let timestamp_millis = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + as u64; } let mut session_infos_on_machine = read_other_live_session_states(¤t_session_name); @@ -511,7 +527,7 @@ fn job_already_running( } } -fn write_session_state_to_disk( +pub fn write_session_state_to_disk( current_session_name: String, current_session_info: SessionInfo, current_session_layout: (String, BTreeMap), diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index c6c931c89..a72c1ffac 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -207,6 +207,10 @@ pub enum PluginInstruction { }, LayoutListUpdate(Vec, Vec), RequestStateUpdateForPlugin(PluginId), + UpdateSessionSaveTime(u64), // u64 = milliseconds since UNIX epoch + GetLastSessionSaveTime { + response_channel: crossbeam::channel::Sender>, + }, Exit, } @@ -263,6 +267,10 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::RequestStateUpdateForPlugin(..) => { PluginContext::RequestStateUpdateForPlugin }, + PluginInstruction::UpdateSessionSaveTime(..) => PluginContext::UpdateSessionSaveTime, + PluginInstruction::GetLastSessionSaveTime { .. } => { + PluginContext::GetLastSessionSaveTime + }, } } } @@ -1203,6 +1211,14 @@ pub(crate) fn plugin_thread_main( PluginInstruction::RequestStateUpdateForPlugin(plugin_id) => { wasm_bridge.state_update_for_plugin(plugin_id); }, + PluginInstruction::UpdateSessionSaveTime(timestamp_millis) => { + // Store timestamp in WasmBridge (as Unix epoch for internal use) + *wasm_bridge.last_session_save_time.lock().unwrap() = Some(timestamp_millis); + }, + PluginInstruction::GetLastSessionSaveTime { response_channel } => { + let timestamp = *wasm_bridge.last_session_save_time.lock().unwrap(); + let _ = response_channel.send(timestamp); + }, PluginInstruction::Exit => { break; }, diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index bdbe25f3c..7244a2d39 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -187,6 +187,7 @@ pub struct WasmBridge { base_modes: HashMap, downloader: Downloader, previous_pane_render_report: Option, + pub last_session_save_time: Arc>>, // milliseconds since UNIX epoch } impl WasmBridge { @@ -252,6 +253,7 @@ impl WasmBridge { base_modes: HashMap::new(), downloader, previous_pane_render_report: None, + last_session_save_time: Arc::new(Mutex::new(None)), } } pub fn load_plugin( diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 5b03c0e68..184936bdc 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -62,11 +62,12 @@ use zellij_utils::{ }, plugin_command::{ dump_layout_response, dump_session_layout_response, parse_layout_response, - ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse, + save_session_response, ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse, ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse, ProtobufGetLayoutDirResponse, ProtobufGetPanePidResponse, ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse, + ProtobufSaveSessionResponse, }, plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}, }, @@ -129,6 +130,10 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::ParseLayout(layout_string) => parse_layout(env, layout_string), PluginCommand::GetLayoutDir => get_layout_dir(env), PluginCommand::GetFocusedPaneInfo => get_focused_pane_info(env), + PluginCommand::SaveSession => save_session(env), + PluginCommand::CurrentSessionLastSavedTime => { + current_session_last_saved_time(env) + }, PluginCommand::OpenFile(file_to_open, context) => { open_file(env, file_to_open, context) }, @@ -2564,6 +2569,64 @@ fn dump_session_layout(env: &PluginEnv, tab_index: Option) { .with_context(|| format!("failed to send session layout to plugin {}", env.name())); } +fn save_session(env: &PluginEnv) { + use save_session_response::Result as SaveSessionResult; + let (completion_tx, completion_rx) = oneshot::channel(); + + let send_result = env.senders.send_to_screen(ScreenInstruction::SaveSession( + env.client_id, + Some(NotificationEnd::new(completion_tx)), + )); + + let response = if let Err(e) = send_result { + ProtobufSaveSessionResponse { + result: Some(SaveSessionResult::Error(format!( + "Failed to send save request: {}", + e + ))), + } + } else { + let wait_forever = false; + let _result = wait_for_action_completion(completion_rx, "save_session", wait_forever); + ProtobufSaveSessionResponse { + result: Some(SaveSessionResult::Success(true)), + } + }; + + let _ = wasi_write_object(env, &response.encode_to_vec()); +} + +fn current_session_last_saved_time(env: &PluginEnv) { + use zellij_utils::plugin_api::plugin_command::ProtobufCurrentSessionLastSavedTimeResponse; + + let (response_tx, response_rx) = crossbeam::channel::bounded(1); + let send_result = env + .senders + .send_to_plugin(PluginInstruction::GetLastSessionSaveTime { + response_channel: response_tx, + }); + + let saved_timestamp_millis = if send_result.is_ok() { + response_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .ok() + .flatten() + } else { + None + }; + + let timestamp_millis = saved_timestamp_millis.map(|saved_time| { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + now.saturating_sub(saved_time) + }); + + let response = ProtobufCurrentSessionLastSavedTimeResponse { timestamp_millis }; + let _ = wasi_write_object(env, &response.encode_to_vec()); +} + fn list_clients(env: &PluginEnv) { let _ = env.senders.to_screen.as_ref().map(|sender| { sender.send(ScreenInstruction::ListClientsToPlugin( @@ -3842,7 +3905,9 @@ fn check_command_permission( | PluginCommand::GetLayoutDir | PluginCommand::GetFocusedPaneInfo | PluginCommand::DumpLayout(..) - | PluginCommand::ParseLayout(..) => PermissionType::ReadApplicationState, + | PluginCommand::ParseLayout(..) + | PluginCommand::SaveSession + | PluginCommand::CurrentSessionLastSavedTime => PermissionType::ReadApplicationState, PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => { PermissionType::Reconfigure }, diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 82a24b555..587fd18df 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -1,3 +1,4 @@ +use crate::background_jobs::write_session_state_to_disk; use crate::background_jobs::BackgroundJob; use crate::route::NotificationEnd; use crate::terminal_bytes::TerminalBytes; @@ -19,7 +20,7 @@ use std::{collections::HashMap, os::unix::io::RawFd, path::PathBuf}; use zellij_utils::{ data::{ CommandOrPlugin, Event, FloatingPaneCoordinates, GetPanePidResponse, NewPanePlacement, - OriginatingPlugin, + OriginatingPlugin, SessionInfo, }, errors::prelude::*, errors::{ContextType, PtyContext}, @@ -110,6 +111,12 @@ pub enum PtyInstruction { response_channel: crossbeam::channel::Sender, }, LogLayoutToHd(SessionLayoutMetadata), + SaveSessionToDisk { + session_name: String, + session_info: SessionInfo, + session_layout_metadata: SessionLayoutMetadata, + completion_tx: Option, + }, FillPluginCwd( Option, // should float bool, // should be opened in place @@ -160,6 +167,7 @@ impl From<&PtyInstruction> for PtyContext { PtyInstruction::DumpLayout(..) => PtyContext::DumpLayout, PtyInstruction::DumpLayoutToPlugin { .. } => PtyContext::DumpLayoutToPlugin, PtyInstruction::LogLayoutToHd(..) => PtyContext::LogLayoutToHd, + PtyInstruction::SaveSessionToDisk { .. } => PtyContext::SaveSessionToDisk, PtyInstruction::FillPluginCwd(..) => PtyContext::FillPluginCwd, PtyInstruction::ListClientsMetadata(..) => PtyContext::ListClientsMetadata, PtyInstruction::Reconfigure { .. } => PtyContext::Reconfigure, @@ -760,6 +768,41 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { } } }, + PtyInstruction::SaveSessionToDisk { + session_name, + session_info, + mut session_layout_metadata, + completion_tx: _completion_tx, // Dropped at end to signal completion + } => { + pty.populate_session_layout_metadata(&mut session_layout_metadata); + match session_serialization::serialize_session_layout( + session_layout_metadata.into(), + ) { + Ok(kdl_and_files) => { + write_session_state_to_disk( + session_name, + session_info, + kdl_and_files.clone(), + ); + + // Update session save time for plugin query + let timestamp_millis = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + let _ = pty.bus.senders.send_to_plugin( + PluginInstruction::UpdateSessionSaveTime(timestamp_millis), + ); + + let _ = pty.bus.senders.send_to_background_jobs( + BackgroundJob::ReportLayoutInfo(kdl_and_files), + ); + }, + Err(e) => { + log::error!("Failed to serialize layout: {}", e); + }, + }; + }, PtyInstruction::FillPluginCwd( should_float, should_be_open_in_place, diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index f643f0244..7ab11dc04 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -389,6 +389,14 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::SaveSession => { + senders + .send_to_screen(ScreenInstruction::SaveSession( + client_id, + Some(NotificationEnd::new(completion_tx)), + )) + .with_context(err_context)?; + }, Action::EditScrollback => { senders .send_to_screen(ScreenInstruction::EditScrollback( diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 3bbfc6703..17e832308 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -262,6 +262,7 @@ pub enum ScreenInstruction { DumpScreen(String, ClientId, bool, Option), DumpLayout(Option, ClientId, Option), // PathBuf is the default configured // shell + SaveSession(ClientId, Option), DumpLayoutToPlugin { plugin_id: PluginId, tab_index: Option, @@ -634,6 +635,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ClearScreen(..) => ScreenContext::ClearScreen, ScreenInstruction::DumpScreen(..) => ScreenContext::DumpScreen, ScreenInstruction::DumpLayout(..) => ScreenContext::DumpLayout, + ScreenInstruction::SaveSession(..) => ScreenContext::SaveSession, ScreenInstruction::DumpLayoutToPlugin { .. } => ScreenContext::DumpLayoutToPlugin, ScreenInstruction::GetFocusedPaneInfo { .. } => ScreenContext::GetFocusedPaneInfo, ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback, @@ -3914,7 +3916,6 @@ pub(crate) fn screen_thread_main( let blocking_notification = if set_blocking { completion_tx } else { None }; - log::info!("client_or_tab_index: {:?}", client_or_tab_index); match client_or_tab_index { ClientTabIndexOrPaneId::ClientId(client_id) => { active_tab_and_connected_client_id_with_first_tab_fallback!(screen, client_id, |tab: &mut Tab, client_id: Option| { @@ -6168,6 +6169,62 @@ pub(crate) fn screen_thread_main( screen.dump_layout_to_hd()?; } }, + ScreenInstruction::SaveSession(_client_id, completion_tx) => { + let err_context = || "Failed to save session"; + + screen.update_active_pane_ids(); + let pane_manifest = screen.generate_and_report_pane_state()?; + let tab_infos = screen.generate_and_report_tab_state()?; + + #[cfg(not(test))] + let (available_layouts, _layout_errors) = Layout::list_available_layouts( + screen.layout_dir.clone(), + &screen.default_layout_name, + ); + #[cfg(test)] + let available_layouts = vec![]; + + let session_info = SessionInfo { + name: screen.session_name.clone(), + tabs: tab_infos, + panes: pane_manifest, + connected_clients: screen.active_tab_indices.keys().len(), + is_current_session: true, + available_layouts, + web_clients_allowed: screen.web_sharing.web_clients_allowed(), + web_client_count: screen + .connected_clients + .borrow() + .iter() + .filter(|(_client_id, is_web_client)| **is_web_client) + .count(), + plugins: Default::default(), + tab_history: screen.tab_history.clone(), + pane_history: screen + .pane_history + .iter() + .map(|(k, v)| (*k, v.iter().map(|v| (*v).into()).collect())) + .collect(), + }; + + let session_layout_metadata = if screen.session_serialization { + screen.get_layout_metadata(Some(screen.default_shell.clone()), None) + } else { + // Create empty metadata if serialization is disabled + SessionLayoutMetadata::new(screen.default_layout.clone()) + }; + + screen + .bus + .senders + .send_to_pty(PtyInstruction::SaveSessionToDisk { + session_name: screen.session_name.clone(), + session_info, + session_layout_metadata, + completion_tx, + }) + .with_context(err_context)?; + }, ScreenInstruction::RenameSession( name, client_id, diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 56687e5c2..f3f8572e0 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -9,14 +9,17 @@ use zellij_utils::errors::prelude::*; use zellij_utils::input::actions::Action; pub use zellij_utils::plugin_api; use zellij_utils::plugin_api::event::ProtobufPaneScrollbackResponse; +use zellij_utils::plugin_api::generated_api::api::plugin_command::save_session_response; use zellij_utils::plugin_api::plugin_command::{ dump_layout_response, dump_session_layout_response, get_focused_pane_info_response, - parse_layout_response, CreateTokenResponse, ListTokensResponse, ProtobufDeleteLayoutResponse, + parse_layout_response, CreateTokenResponse, ListTokensResponse, + ProtobufCurrentSessionLastSavedTimeResponse, ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse, ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse, ProtobufGetLayoutDirResponse, ProtobufGetPanePidResponse, ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse, - RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse, + ProtobufSaveSessionResponse, RenameWebTokenResponse, RevokeAllWebTokensResponse, + RevokeTokenResponse, }; use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}; @@ -186,6 +189,75 @@ pub fn get_focused_pane_info() -> Result<(usize, PaneId), String> { } } +/// Save the current session state to disk immediately. +/// +/// This triggers an immediate write of the current session metadata and layout +/// to the session cache directory (~/.cache/zellij/contract_version_1/session_info//). +/// +/// # Returns +/// +/// - `Ok(())` if the save request was successfully sent +/// - `Err(String)` if there was an error sending the request +/// +/// # Example +/// +/// ```no_run +/// use zellij_tile::prelude::*; +/// +/// // Save the current session +/// match save_session() { +/// Ok(()) => println!("Session saved successfully"), +/// Err(e) => eprintln!("Failed to save session: {}", e), +/// } +/// ``` +pub fn save_session() -> Result<(), String> { + let plugin_command = PluginCommand::SaveSession; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + + unsafe { host_run_plugin_command() }; + + let response_bytes = + bytes_from_stdin().map_err(|e| format!("Failed to read response: {:?}", e))?; + let protobuf_response = ProtobufSaveSessionResponse::decode(response_bytes.as_slice()) + .map_err(|e| format!("Failed to decode response: {}", e))?; + + match protobuf_response.result { + Some(save_session_response::Result::Success(_)) => Ok(()), + Some(save_session_response::Result::Error(error)) => Err(error), + None => Err("Server returned empty response".to_string()), + } +} + +/// Returns the elapsed time in milliseconds since the current session state was last saved to disk. +/// +/// Returns `None` if the session has never been saved during this session. +/// The returned value is the number of milliseconds elapsed since the last save, not a Unix epoch timestamp. +/// +/// # Example +/// +/// ```no_run +/// use zellij_tile::prelude::*; +/// +/// if let Some(elapsed_millis) = current_session_last_saved_time() { +/// println!("Session was last saved {} ms ago", elapsed_millis); +/// } else { +/// println!("Session has not been saved yet"); +/// } +/// ``` +pub fn current_session_last_saved_time() -> Option { + let plugin_command = PluginCommand::CurrentSessionLastSavedTime; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; + + let protobuf_response = + ProtobufCurrentSessionLastSavedTimeResponse::decode(bytes_from_stdin().unwrap().as_slice()) + .unwrap(); + + protobuf_response.timestamp_millis +} + // Host Functions /// Open a file in the user's default `$EDITOR` in a new pane diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index d11cc9e0c..b586527a8 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -3,7 +3,7 @@ pub struct PluginCommand { #[prost(enumeration="CommandName", tag="1")] pub name: i32, - #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136")] + #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138")] pub payload: ::core::option::Option, } /// Nested message and enum types in `PluginCommand`. @@ -255,6 +255,10 @@ pub mod plugin_command { TogglePaneBorderlessPayload(super::TogglePaneBorderlessPayload), #[prost(message, tag="136")] SetPaneBorderlessPayload(super::SetPaneBorderlessPayload), + #[prost(message, tag="137")] + SaveSessionPayload(super::SaveSessionPayload), + #[prost(message, tag="138")] + CurrentSessionLastSavedTimePayload(super::CurrentSessionLastSavedTimePayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -1246,6 +1250,38 @@ pub struct FocusedPaneInfo { #[prost(message, optional, tag="2")] pub focused_pane_id: ::core::option::Option, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SaveSessionPayload { +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SaveSessionResponse { + #[prost(oneof="save_session_response::Result", tags="1, 2")] + pub result: ::core::option::Option, +} +/// Nested message and enum types in `SaveSessionResponse`. +pub mod save_session_response { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Result { + #[prost(bool, tag="1")] + Success(bool), + #[prost(string, tag="2")] + Error(::prost::alloc::string::String), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CurrentSessionLastSavedTimePayload { +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CurrentSessionLastSavedTimeResponse { + /// None if never saved, otherwise milliseconds elapsed since last save + #[prost(uint64, optional, tag="1")] + pub timestamp_millis: ::core::option::Option, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum CommandName { @@ -1414,6 +1450,8 @@ pub enum CommandName { GetFocusedPaneInfo = 183, TogglePaneBorderless = 184, SetPaneBorderless = 185, + SaveSession = 186, + CurrentSessionLastSavedTime = 187, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -1587,6 +1625,8 @@ impl CommandName { CommandName::GetFocusedPaneInfo => "GetFocusedPaneInfo", CommandName::TogglePaneBorderless => "TogglePaneBorderless", CommandName::SetPaneBorderless => "SetPaneBorderless", + CommandName::SaveSession => "SaveSession", + CommandName::CurrentSessionLastSavedTime => "CurrentSessionLastSavedTime", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1757,6 +1797,8 @@ impl CommandName { "GetFocusedPaneInfo" => Some(Self::GetFocusedPaneInfo), "TogglePaneBorderless" => Some(Self::TogglePaneBorderless), "SetPaneBorderless" => Some(Self::SetPaneBorderless), + "SaveSession" => Some(Self::SaveSession), + "CurrentSessionLastSavedTime" => Some(Self::CurrentSessionLastSavedTime), _ => None, } } diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs index a5dc31bd8..abca86cc7 100644 --- a/zellij-utils/assets/prost_ipc/client_server_contract.rs +++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs @@ -119,7 +119,7 @@ pub struct RgbColor { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Action { - #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 95, 96, 90, 91, 92, 93, 94")] + #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 95, 96, 90, 91, 92, 93, 94, 97")] pub action_type: ::core::option::Option, } /// Nested message and enum types in `Action`. @@ -319,6 +319,8 @@ pub mod action { NewBlockingPane(super::NewBlockingPaneAction), #[prost(message, tag="94")] OverrideLayout(super::OverrideLayoutAction), + #[prost(message, tag="97")] + SaveSession(super::SaveSessionAction), } } // Action message definitions (all 92 variants) @@ -354,6 +356,10 @@ pub struct DumpLayoutAction { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct SaveSessionAction { +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct EditScrollbackAction { } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 3704ca56e..5f4d17d00 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -642,6 +642,8 @@ pub enum CliAction { }, /// Dump current layout to stdout DumpLayout, + /// Save the current session state to disk immediately + SaveSession, /// Open the pane scrollback in your default editor EditScrollback, /// Scroll up in the focused pane diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index ce66881a0..848d4d42b 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -232,6 +232,7 @@ message Action { SwitchSessionAction switch_session = 92; NewBlockingPaneAction new_blocking_pane = 93; OverrideLayoutAction override_layout = 94; + SaveSessionAction save_session = 97; } } @@ -245,6 +246,7 @@ message SwitchFocusAction {} message MovePaneBackwardsAction {} message ClearScreenAction {} message DumpLayoutAction {} +message SaveSessionAction {} message EditScrollbackAction {} message ScrollUpAction {} message ScrollDownAction {} diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 15d74b54b..3a749d6f8 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -3335,4 +3335,6 @@ pub enum PluginCommand { ParseLayout(String), // String contains raw KDL layout GetLayoutDir, GetFocusedPaneInfo, + SaveSession, + CurrentSessionLastSavedTime, } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 081baf8ba..25265f895 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -259,6 +259,7 @@ pub enum ScreenContext { ClearScreen, DumpScreen, DumpLayout, + SaveSession, EditScrollback, GetPaneScrollback, ScrollUp, @@ -418,6 +419,7 @@ pub enum PtyContext { SpawnInPlaceTerminal, DumpLayout, LogLayoutToHd, + SaveSessionToDisk, FillPluginCwd, DumpLayoutToPlugin, ListClientsMetadata, @@ -474,6 +476,8 @@ pub enum PluginContext { UserInput, LayoutListUpdate, RequestStateUpdateForPlugin, + UpdateSessionSaveTime, + GetLastSessionSaveTime, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 27d880d3c..31fd67e05 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -166,6 +166,8 @@ pub enum Action { }, /// Dumps DumpLayout, + /// Save the current session state to disk + SaveSession, /// Scroll up in focus pane. EditScrollback, ScrollUp, @@ -523,6 +525,7 @@ impl Action { include_scrollback: full, }]), CliAction::DumpLayout => Ok(vec![Action::DumpLayout]), + CliAction::SaveSession => Ok(vec![Action::SaveSession]), CliAction::EditScrollback => Ok(vec![Action::EditScrollback]), CliAction::ScrollUp => Ok(vec![Action::ScrollUp]), CliAction::ScrollDown => Ok(vec![Action::ScrollDown]), diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index 7810d4ef3..eacf85081 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -730,16 +730,16 @@ impl From PageScrollDownAction, PageScrollUpAction, PaneIdWithPlugin, PaneNameInputAction, PreviousSwapLayoutAction, QueryTabNamesAction, QuitAction, RenamePluginPaneAction, RenameSessionAction, RenameTabAction, RenameTerminalPaneAction, ResizeAction, - RunAction, ScrollDownAction, ScrollDownAtAction, ScrollToBottomAction, - ScrollToTopAction, ScrollUpAction, ScrollUpAtAction, SearchAction, SearchInputAction, - SearchToggleOptionAction, SetPaneBorderlessAction, SkipConfirmAction, StackPanesAction, - StartOrReloadPluginAction, SwitchFocusAction, SwitchModeForAllClientsAction, - SwitchSessionAction, SwitchToModeAction, TabNameInputAction, ToggleActiveSyncTabAction, - ToggleFloatingPanesAction, ToggleFocusFullscreenAction, ToggleGroupMarkingAction, - ToggleMouseModeAction, TogglePaneBorderlessAction, TogglePaneEmbedOrFloatingAction, - TogglePaneFramesAction, TogglePaneInGroupAction, TogglePanePinnedAction, - ToggleTabAction, UndoRenamePaneAction, UndoRenameTabAction, WriteAction, - WriteCharsAction, + RunAction, SaveSessionAction, ScrollDownAction, ScrollDownAtAction, + ScrollToBottomAction, ScrollToTopAction, ScrollUpAction, ScrollUpAtAction, + SearchAction, SearchInputAction, SearchToggleOptionAction, SetPaneBorderlessAction, + SkipConfirmAction, StackPanesAction, StartOrReloadPluginAction, SwitchFocusAction, + SwitchModeForAllClientsAction, SwitchSessionAction, SwitchToModeAction, + TabNameInputAction, ToggleActiveSyncTabAction, ToggleFloatingPanesAction, + ToggleFocusFullscreenAction, ToggleGroupMarkingAction, ToggleMouseModeAction, + TogglePaneBorderlessAction, TogglePaneEmbedOrFloatingAction, TogglePaneFramesAction, + TogglePaneInGroupAction, TogglePanePinnedAction, ToggleTabAction, UndoRenamePaneAction, + UndoRenameTabAction, WriteAction, WriteCharsAction, }; use std::collections::HashMap; @@ -1305,6 +1305,9 @@ impl From crate::input::actions::Action::ToggleGroupMarking => { ActionType::ToggleGroupMarking(ToggleGroupMarkingAction {}) }, + crate::input::actions::Action::SaveSession => { + ActionType::SaveSession(SaveSessionAction {}) + }, }; Self { @@ -1390,6 +1393,7 @@ impl TryFrom }) }, ActionType::DumpLayout(_) => Ok(crate::input::actions::Action::DumpLayout), + ActionType::SaveSession(_) => Ok(crate::input::actions::Action::SaveSession), ActionType::EditScrollback(_) => Ok(crate::input::actions::Action::EditScrollback), ActionType::ScrollUp(_) => Ok(crate::input::actions::Action::ScrollUp), ActionType::ScrollUpAt(scroll_action) => { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index bc5bc87c1..7903be807 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -1777,7 +1777,8 @@ impl TryFrom for ProtobufAction { | Action::TogglePaneBorderless { pane_id: _ } | Action::SetPaneBorderless { .. } | Action::SkipConfirm { action: _ } - | Action::SwitchSession { .. } => Err("Unsupported action"), + | Action::SwitchSession { .. } + | Action::SaveSession => Err("Unsupported action"), } } } diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 101253871..f5dadcb98 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -504,3 +504,4 @@ message ActionCompletePayload { optional PaneId pane_id = 2; repeated ContextItem context = 3; } + diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 9c529013c..6ef1ad340 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -178,6 +178,8 @@ enum CommandName { GetFocusedPaneInfo = 183; TogglePaneBorderless = 184; SetPaneBorderless = 185; + SaveSession = 186; + CurrentSessionLastSavedTime = 187; } message PluginCommand { @@ -305,6 +307,8 @@ message PluginCommand { GetFocusedPaneInfoPayload get_focused_pane_info_payload = 134; TogglePaneBorderlessPayload toggle_pane_borderless_payload = 135; SetPaneBorderlessPayload set_pane_borderless_payload = 136; + SaveSessionPayload save_session_payload = 137; + CurrentSessionLastSavedTimePayload current_session_last_saved_time_payload = 138; } } @@ -906,3 +910,18 @@ message FocusedPaneInfo { uint32 focused_tab_index = 1; PaneId focused_pane_id = 2; } + +message SaveSessionPayload {} + +message SaveSessionResponse { + oneof result { + bool success = 1; + string error = 2; + } +} + +message CurrentSessionLastSavedTimePayload {} + +message CurrentSessionLastSavedTimeResponse { + optional uint64 timestamp_millis = 1; // None if never saved, otherwise milliseconds elapsed since last save +} diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index da0f45999..691a6c459 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -6,12 +6,14 @@ pub use super::generated_api::api::{ delete_layout_response, dump_layout_response, dump_session_layout_response, edit_layout_response, get_focused_pane_info_response, get_pane_pid_response, parse_layout_response, plugin_command::Payload, rename_layout_response, - save_layout_response, BreakPanesToNewTabPayload, BreakPanesToTabWithIndexPayload, - ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload, - ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseMultiplePanesPayload, - CloseTabWithIndexPayload, CommandName, ContextItem, CopyToClipboardPayload, - CreateTokenResponse as ProtobufCreateTokenResponse, CreateTokenResponse, CursorPosition, - DeleteLayoutPayload, DeleteLayoutResponse as ProtobufDeleteLayoutResponse, + save_layout_response, save_session_response, BreakPanesToNewTabPayload, + BreakPanesToTabWithIndexPayload, ChangeFloatingPanesCoordinatesPayload, + ChangeHostFolderPayload, ClearScreenForPaneIdPayload, CliPipeOutputPayload, + CloseMultiplePanesPayload, CloseTabWithIndexPayload, CommandName, ContextItem, + CopyToClipboardPayload, CreateTokenResponse as ProtobufCreateTokenResponse, + CreateTokenResponse, CurrentSessionLastSavedTimePayload, + CurrentSessionLastSavedTimeResponse as ProtobufCurrentSessionLastSavedTimeResponse, + CursorPosition, DeleteLayoutPayload, DeleteLayoutResponse as ProtobufDeleteLayoutResponse, DumpLayoutPayload, DumpLayoutResponse as ProtobufDumpLayoutResponse, DumpSessionLayoutPayload, DumpSessionLayoutResponse as ProtobufDumpSessionLayoutResponse, EditLayoutPayload, EditLayoutResponse as ProtobufEditLayoutResponse, @@ -44,7 +46,8 @@ pub use super::generated_api::api::{ RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload, RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload, RunActionPayload, RunCommandPayload, SaveLayoutPayload, - SaveLayoutResponse as ProtobufSaveLayoutResponse, ScrollDownInPaneIdPayload, + SaveLayoutResponse as ProtobufSaveLayoutResponse, SaveSessionPayload, + SaveSessionResponse as ProtobufSaveSessionResponse, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload, SetPaneBorderlessPayload, SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowCursorPayload, @@ -2103,6 +2106,10 @@ impl TryFrom for PluginCommand { }, Some(CommandName::GetLayoutDir) => Ok(PluginCommand::GetLayoutDir), Some(CommandName::GetFocusedPaneInfo) => Ok(PluginCommand::GetFocusedPaneInfo), + Some(CommandName::SaveSession) => Ok(PluginCommand::SaveSession), + Some(CommandName::CurrentSessionLastSavedTime) => { + Ok(PluginCommand::CurrentSessionLastSavedTime) + }, None => Err("Unrecognized plugin command"), } } @@ -3443,6 +3450,16 @@ impl TryFrom for ProtobufPluginCommand { GetFocusedPaneInfoPayload {}, )), }), + PluginCommand::SaveSession => Ok(ProtobufPluginCommand { + name: CommandName::SaveSession as i32, + payload: Some(Payload::SaveSessionPayload(SaveSessionPayload {})), + }), + PluginCommand::CurrentSessionLastSavedTime => Ok(ProtobufPluginCommand { + name: CommandName::CurrentSessionLastSavedTime as i32, + payload: Some(Payload::CurrentSessionLastSavedTimePayload( + CurrentSessionLastSavedTimePayload {}, + )), + }), } } }