aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-04-17 14:08:48 +0200
committerGitHub <noreply@github.com>2026-04-17 14:08:48 +0200
commitaa29cbaf6631af0afca6aae427a97b3e478393b3 (patch)
tree0ee1cab0f7bdc66cf151f38341252701338c1853
parentfix(windows): skip modifier re-encoding for Char keys in cast_crossterm_key (... (diff)
downloadsl0ppij-aa29cbaf6631af0afca6aae427a97b3e478393b3.tar.gz
sl0ppij-aa29cbaf6631af0afca6aae427a97b3e478393b3.tar.xz
sl0ppij-aa29cbaf6631af0afca6aae427a97b3e478393b3.zip
Some idle CPU and disk/io fixes, new CommandChanged plugin Event (#5063)
* initial implementation * fix mouse cpu spam * prevent excess metadata write and prune sessions on startup * consolidate CommandChanged and PaneCommandChanged * tests * rustfmt * add pr
-rw-r--r--CHANGELOG.md1
-rw-r--r--default-plugins/session-manager/src/main.rs71
-rw-r--r--default-plugins/session-manager/src/resurrectable_sessions.rs2
-rw-r--r--src/commands.rs18
-rw-r--r--zellij-client/src/input_handler.rs2
-rw-r--r--zellij-server/src/background_jobs.rs362
-rw-r--r--zellij-server/src/lib.rs11
-rw-r--r--zellij-server/src/panes/grid.rs91
-rw-r--r--zellij-server/src/panes/terminal_pane.rs13
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs1
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs60
-rw-r--r--zellij-server/src/pty.rs184
-rw-r--r--zellij-server/src/route.rs79
-rw-r--r--zellij-server/src/screen.rs65
-rw-r--r--zellij-server/src/tab/mod.rs13
-rw-r--r--zellij-server/src/tab/mouse_handler.rs13
-rw-r--r--zellij-server/src/terminal_bytes.rs8
-rw-r--r--zellij-server/src/unit/pty_tests.rs479
-rw-r--r--zellij-server/src/unit/screen_tests.rs4
-rw-r--r--zellij-tile/src/shim.rs88
-rw-r--r--zellij-utils/assets/prost/api.event.rs19
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs36
-rw-r--r--zellij-utils/src/consts.rs23
-rw-r--r--zellij-utils/src/data.rs14
-rw-r--r--zellij-utils/src/errors.rs2
-rw-r--r--zellij-utils/src/plugin_api/event.proto9
-rw-r--r--zellij-utils/src/plugin_api/event.rs41
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto16
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs94
29 files changed, 1548 insertions, 271 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 276e7ba7..8290e4be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]
+* fix: idle CPU + disk i/o, CommandChanged plugin Event, GetSessionList plugin command (https://github.com/zellij-org/zellij/pull/5063)
## [0.44.1] - 2026-04-07
* fix: don't display default ports as offline in `share` plugin (https://github.com/zellij-org/zellij/pull/4908)
diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs
index afade742..b6031011 100644
--- a/default-plugins/session-manager/src/main.rs
+++ b/default-plugins/session-manager/src/main.rs
@@ -54,6 +54,8 @@ struct State {
request_ids: Vec<String>,
is_web_client: bool,
current_session_last_saved_time: Option<u64>,
+ is_visible: bool,
+ refresh_timer_armed: bool,
}
register_plugin!(State);
@@ -76,17 +78,20 @@ impl ZellijPlugin for State {
self.active_screen = ActiveScreen::SingleScreen;
}
self.single_screen_state.is_welcome_screen = self.is_welcome_screen;
- if !self.is_welcome_screen {
- set_timeout(0.1); // for the current_session_last_saved_time polling
- }
+ self.is_visible = true;
subscribe(&[
EventType::ModeUpdate,
EventType::SessionUpdate,
EventType::Key,
EventType::RunCommandResult,
EventType::Timer,
+ EventType::Visible,
]);
rename_plugin_pane(get_plugin_ids().plugin_id, "Session Manager");
+ self.refresh_session_list();
+ if !self.is_welcome_screen {
+ self.arm_refresh_timer();
+ }
}
fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
@@ -119,12 +124,29 @@ impl ZellijPlugin for State {
let mut should_render = false;
match event {
Event::Timer(_) => {
+ self.refresh_timer_armed = false;
+ if !self.is_visible {
+ return false;
+ }
let new_saved_time = current_session_last_saved_time();
if new_saved_time != self.current_session_last_saved_time {
self.current_session_last_saved_time = new_saved_time;
should_render = true;
}
- set_timeout(1.0);
+ if self.refresh_session_list() {
+ should_render = true;
+ }
+ self.arm_refresh_timer();
+ },
+ Event::Visible(is_visible) => {
+ let was_visible = self.is_visible;
+ self.is_visible = is_visible;
+ if is_visible && !was_visible {
+ if self.refresh_session_list() {
+ should_render = true;
+ }
+ self.arm_refresh_timer();
+ }
},
Event::ModeUpdate(mode_info) => {
self.colors = Colors::new(mode_info.style.colors);
@@ -1171,6 +1193,47 @@ impl State {
}
self.session_name = Some(new_name.to_owned());
}
+ fn arm_refresh_timer(&mut self) {
+ if !self.refresh_timer_armed {
+ set_timeout(1.0);
+ self.refresh_timer_armed = true;
+ }
+ }
+
+ fn refresh_session_list(&mut self) -> bool {
+ let snapshot = match get_session_list() {
+ Ok(snapshot) => snapshot,
+ Err(_) => return false,
+ };
+ for session_info in &snapshot.live_sessions {
+ if session_info.is_current_session {
+ self.new_session_info
+ .update_layout_list(session_info.available_layouts.clone());
+ }
+ }
+ self.resurrectable_sessions
+ .update(snapshot.resurrectable_sessions);
+ self.update_session_infos(snapshot.live_sessions);
+ if !self.is_multi_screen {
+ self.single_screen_state.update_search_term(
+ &self.sessions.session_ui_infos,
+ &self.resurrectable_sessions.all_resurrectable_sessions,
+ );
+ let previous_selection = self.single_screen_state.layout_list.selected_layout_index;
+ let previous_search_term = self
+ .single_screen_state
+ .layout_list
+ .layout_search_term
+ .clone();
+ self.single_screen_state.layout_list = self.new_session_info.get_layout_list_clone();
+ self.single_screen_state.layout_list.layout_search_term = previous_search_term;
+ self.single_screen_state.layout_list.update_search_term();
+ self.single_screen_state.layout_list.selected_layout_index =
+ previous_selection.min(self.single_screen_state.layout_list.max_index());
+ }
+ true
+ }
+
fn update_session_infos(&mut self, session_infos: Vec<SessionInfo>) {
let session_ui_infos: Vec<SessionUiInfo> = session_infos
.iter()
diff --git a/default-plugins/session-manager/src/resurrectable_sessions.rs b/default-plugins/session-manager/src/resurrectable_sessions.rs
index 3b03d507..a25d8b36 100644
--- a/default-plugins/session-manager/src/resurrectable_sessions.rs
+++ b/default-plugins/session-manager/src/resurrectable_sessions.rs
@@ -19,7 +19,7 @@ pub struct ResurrectableSessions {
impl ResurrectableSessions {
pub fn update(&mut self, mut list: Vec<(String, Duration)>) {
- list.sort_by(|a, b| a.1.cmp(&b.1));
+ list.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
self.all_resurrectable_sessions = list;
if self.is_searching {
self.update_search_term();
diff --git a/src/commands.rs b/src/commands.rs
index a86d1949..03151275 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -81,19 +81,31 @@ pub(crate) fn kill_all_sessions(yes: bool) {
}
pub(crate) fn delete_all_sessions(yes: bool, force: bool) {
+ use std::collections::BTreeMap;
+ use zellij_server::background_jobs::scan_session_list_default_dirs;
+
let active_sessions: Vec<String> = get_sessions()
.unwrap_or_default()
.iter()
.map(|s| s.0.clone())
.collect();
- let resurrectable_sessions = get_resurrectable_sessions();
+ let (_live_sessions, resurrectable_map) =
+ scan_session_list_default_dirs(&String::new(), &[], &BTreeMap::new());
+ let mut resurrectable_sessions: Vec<(String, Duration)> =
+ resurrectable_map.into_iter().collect();
+ if force {
+ for entry in get_resurrectable_sessions() {
+ if !resurrectable_sessions.iter().any(|(n, _)| *n == entry.0) {
+ resurrectable_sessions.push(entry);
+ }
+ }
+ }
let dead_sessions: Vec<_> = if force {
resurrectable_sessions
} else {
resurrectable_sessions
- .iter()
+ .into_iter()
.filter(|(name, _)| !active_sessions.contains(name))
- .cloned()
.collect()
};
if !yes {
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs
index 8b404f7b..8c6c7b28 100644
--- a/zellij-client/src/input_handler.rs
+++ b/zellij-client/src/input_handler.rs
@@ -320,8 +320,6 @@ impl InputHandler {
}
}
fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) {
- // This dispatch handles all of the output(s) to terminal
- // pane(s).
self.dispatch_action(
Action::MouseEvent {
event: *mouse_event,
diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs
index 5509c5fd..0ea1704d 100644
--- a/zellij-server/src/background_jobs.rs
+++ b/zellij-server/src/background_jobs.rs
@@ -4,7 +4,7 @@ use zellij_utils::consts::{
VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR,
};
#[allow(unused_imports)]
-use zellij_utils::data::{Event, HttpVerb, SessionInfo, WebServerStatus};
+use zellij_utils::data::{Event, HttpVerb, LayoutInfo, SessionInfo, WebServerStatus};
use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType};
use zellij_utils::input::layout::RunPlugin;
#[allow(unused_imports)]
@@ -23,7 +23,7 @@ use isahc::{config::RedirectPolicy, HttpClient, Request};
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::io::Write;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
@@ -43,7 +43,6 @@ pub enum BackgroundJob {
DisplayPaneError(Vec<PaneId>, String),
AnimatePluginLoading(u32), // u32 - plugin_id
StopPluginLoadingAnimation(u32), // u32 - plugin_id
- ReadAllSessionInfosOnMachine, // u32 - plugin_id
ReportSessionInfo(String, SessionInfo), // String - session name
ReportPluginList(BTreeMap<PluginId, RunPlugin>), // String - session name
ReportLayoutInfo((String, BTreeMap<String, String>)), // BTreeMap<file_name, pane_contents>
@@ -86,9 +85,6 @@ impl From<&BackgroundJob> for BackgroundJobContext {
BackgroundJob::StopPluginLoadingAnimation(..) => {
BackgroundJobContext::StopPluginLoadingAnimation
},
- BackgroundJob::ReadAllSessionInfosOnMachine => {
- BackgroundJobContext::ReadAllSessionInfosOnMachine
- },
BackgroundJob::ReportSessionInfo(..) => BackgroundJobContext::ReportSessionInfo,
BackgroundJob::ReportLayoutInfo(..) => BackgroundJobContext::ReportLayoutInfo,
BackgroundJob::RunCommand(..) => BackgroundJobContext::RunCommand,
@@ -114,11 +110,25 @@ impl From<&BackgroundJob> for BackgroundJobContext {
static LONG_FLASH_DURATION_MS: u64 = 1000;
static FLASH_DURATION_MS: u64 = 400; // Doherty threshold
static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500;
-static SESSION_READ_DURATION: u64 = 1000;
+static SESSION_METADATA_WRITE_INTERVAL_MS: u64 = 1000;
+static UPDATE_AND_REPORT_CWDS_INTERVAL_MS: u64 = 1000;
static DEFAULT_SERIALIZATION_INTERVAL: u64 = 60000;
static REPAINT_DELAY_MS: u64 = 10;
static HELP_TEXT_DEBOUNCE_DURATION: u64 = 5000;
+#[derive(Clone)]
+pub struct SessionScanState {
+ pub current_session_name: Arc<Mutex<String>>,
+ pub current_session_info: Arc<Mutex<SessionInfo>>,
+ pub current_session_plugin_list: Arc<Mutex<BTreeMap<PluginId, RunPlugin>>>,
+}
+
+static SESSION_SCAN_STATE: std::sync::OnceLock<SessionScanState> = std::sync::OnceLock::new();
+
+pub fn session_scan_state() -> Option<&'static SessionScanState> {
+ SESSION_SCAN_STATE.get()
+}
+
#[allow(unused_variables)] // web_server_base_url used only with web_server_capability feature
pub(crate) fn background_jobs_main(
bus: Bus<BackgroundJob>,
@@ -134,6 +144,12 @@ pub(crate) fn background_jobs_main(
let current_session_plugin_list: Arc<Mutex<BTreeMap<PluginId, RunPlugin>>> =
Arc::new(Mutex::new(BTreeMap::new()));
let current_session_layout = Arc::new(Mutex::new((String::new(), BTreeMap::new())));
+
+ let _ = SESSION_SCAN_STATE.set(SessionScanState {
+ current_session_name: current_session_name.clone(),
+ current_session_info: current_session_info.clone(),
+ current_session_plugin_list: current_session_plugin_list.clone(),
+ });
let last_serialization_time = Arc::new(Mutex::new(Instant::now()));
let serialization_interval = serialization_interval.map(|s| s * 1000); // convert to
// milliseconds
@@ -151,6 +167,56 @@ pub(crate) fn background_jobs_main(
// We needn't do anything with the runtime, but it should exist at this point.
let runtime = crate::global_async_runtime::get_tokio_runtime();
+ {
+ let senders = bus.senders.clone();
+ let serialization_ms = serialization_interval.unwrap_or(DEFAULT_SERIALIZATION_INTERVAL);
+ runtime.spawn(async move {
+ let mut ticker =
+ tokio::time::interval(std::time::Duration::from_millis(serialization_ms));
+ ticker.tick().await;
+ loop {
+ ticker.tick().await;
+ let _ = senders.send_to_screen(ScreenInstruction::SerializeLayoutForResurrection);
+ }
+ });
+ }
+
+ {
+ let senders = bus.senders.clone();
+ runtime.spawn(async move {
+ let mut ticker = tokio::time::interval(std::time::Duration::from_millis(
+ UPDATE_AND_REPORT_CWDS_INTERVAL_MS,
+ ));
+ ticker.tick().await;
+ loop {
+ ticker.tick().await;
+ let _ = senders.send_to_pty(PtyInstruction::UpdateAndReportCwds);
+ }
+ });
+ }
+
+ if !disable_session_metadata {
+ let current_session_name = current_session_name.clone();
+ let current_session_info = current_session_info.clone();
+ let current_session_layout = current_session_layout.clone();
+ runtime.spawn(async move {
+ let mut ticker = tokio::time::interval(std::time::Duration::from_millis(
+ SESSION_METADATA_WRITE_INTERVAL_MS,
+ ));
+ ticker.tick().await;
+ loop {
+ ticker.tick().await;
+ let name = current_session_name.lock().unwrap().clone();
+ if name.is_empty() {
+ continue;
+ }
+ let info = current_session_info.lock().unwrap().clone();
+ let layout = current_session_layout.lock().unwrap().clone();
+ write_session_state_to_disk(name, info, layout);
+ }
+ });
+ }
+
loop {
let (event, mut err_ctx) = bus.recv().with_context(err_context)?;
err_ctx.add_call(ContextType::BackgroundJob((&event).into()));
@@ -225,84 +291,6 @@ pub(crate) fn background_jobs_main(
.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
- // as this one's) infos (metadata mostly) and sends it to the screen which in turn
- // forwards it to plugins and other places it needs to be
- if running_jobs.get(&job).is_some() {
- continue;
- }
- running_jobs.insert(job, Instant::now());
- runtime.spawn({
- let senders = bus.senders.clone();
- let current_session_info = current_session_info.clone();
- let current_session_name = current_session_name.clone();
- let current_session_layout = current_session_layout.clone();
- let current_session_plugin_list = current_session_plugin_list.clone();
- let last_serialization_time = last_serialization_time.clone();
- async move {
- loop {
- let current_session_name =
- current_session_name.lock().unwrap().to_string();
- let current_session_info = current_session_info.lock().unwrap().clone();
- let available_layouts = current_session_info.available_layouts.clone();
- let current_session_layout =
- current_session_layout.lock().unwrap().clone();
- if !disable_session_metadata {
- write_session_state_to_disk(
- current_session_name.clone(),
- 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(&current_session_name);
- for (session_name, session_info) in session_infos_on_machine.iter_mut()
- {
- if session_name == &current_session_name {
- let current_session_plugin_list =
- current_session_plugin_list.lock().unwrap().clone();
- session_info.populate_plugin_list(current_session_plugin_list);
- // these are not serialized, so must be explicitly added
- session_info.available_layouts = available_layouts.clone();
- }
- }
- let resurrectable_sessions =
- find_resurrectable_sessions(&session_infos_on_machine);
- let _ = senders.send_to_screen(ScreenInstruction::UpdateSessionInfos(
- session_infos_on_machine,
- resurrectable_sessions,
- ));
- let _ = senders.send_to_pty(PtyInstruction::UpdateAndReportCwds);
- if last_serialization_time
- .lock()
- .unwrap()
- .elapsed()
- .as_millis()
- >= serialization_interval
- .unwrap_or(DEFAULT_SERIALIZATION_INTERVAL)
- .into()
- {
- let _ = senders.send_to_screen(
- ScreenInstruction::SerializeLayoutForResurrection,
- );
- *last_serialization_time.lock().unwrap() = Instant::now();
- }
- tokio::time::sleep(std::time::Duration::from_millis(
- SESSION_READ_DURATION,
- ))
- .await;
- }
- }
- });
- },
BackgroundJob::RunCommand(
plugin_id,
client_id,
@@ -669,6 +657,13 @@ fn job_already_running(
}
}
+fn file_content_changed(path: &std::path::Path, new_content: &[u8]) -> bool {
+ match std::fs::read(path) {
+ Ok(existing) => existing != new_content,
+ Err(_) => true,
+ }
+}
+
pub fn write_session_state_to_disk(
current_session_name: String,
current_session_info: SessionInfo,
@@ -676,38 +671,82 @@ pub fn write_session_state_to_disk(
) {
let metadata_cache_file_name = session_info_cache_file_name(&current_session_name);
let (current_session_layout, layout_files_to_write) = current_session_layout;
- let _wrote_metadata_file =
- std::fs::create_dir_all(session_info_folder_for_session(&current_session_name).as_path())
- .and_then(|_| std::fs::File::create(metadata_cache_file_name))
- .and_then(|mut f| write!(f, "{}", current_session_info.to_string()));
+ let new_metadata = current_session_info.to_string();
+ if file_content_changed(&metadata_cache_file_name, new_metadata.as_bytes()) {
+ let _wrote_metadata_file = std::fs::create_dir_all(
+ session_info_folder_for_session(&current_session_name).as_path(),
+ )
+ .and_then(|_| std::fs::File::create(&metadata_cache_file_name))
+ .and_then(|mut f| write!(f, "{}", new_metadata));
+ }
if !current_session_layout.is_empty() {
let layout_cache_file_name = session_layout_cache_file_name(&current_session_name);
- let _wrote_layout_file = std::fs::create_dir_all(
- session_info_folder_for_session(&current_session_name).as_path(),
- )
- .and_then(|_| std::fs::File::create(layout_cache_file_name))
- .and_then(|mut f| write!(f, "{}", current_session_layout))
- .and_then(|_| {
- let session_info_folder = session_info_folder_for_session(&current_session_name);
- for (external_file_name, external_file_contents) in layout_files_to_write {
- std::fs::File::create(session_info_folder.join(external_file_name))
+ if file_content_changed(&layout_cache_file_name, current_session_layout.as_bytes()) {
+ let _wrote_layout_file = std::fs::create_dir_all(
+ session_info_folder_for_session(&current_session_name).as_path(),
+ )
+ .and_then(|_| std::fs::File::create(&layout_cache_file_name))
+ .and_then(|mut f| write!(f, "{}", current_session_layout));
+ }
+ let session_info_folder = session_info_folder_for_session(&current_session_name);
+ for (external_file_name, external_file_contents) in layout_files_to_write {
+ let external_file_path = session_info_folder.join(&external_file_name);
+ if file_content_changed(&external_file_path, external_file_contents.as_bytes()) {
+ std::fs::File::create(&external_file_path)
.and_then(|mut f| write!(f, "{}", external_file_contents))
.unwrap_or_else(|e| {
log::error!("Failed to write layout metadata file: {:?}", e);
});
}
- Ok(())
- });
+ }
+ }
+}
+
+pub fn scan_session_list(
+ current_session_name: &str,
+ available_layouts: &[LayoutInfo],
+ current_session_plugin_list: &BTreeMap<PluginId, RunPlugin>,
+ sock_dir: &Path,
+ session_info_cache_dir: &Path,
+) -> (BTreeMap<String, SessionInfo>, BTreeMap<String, Duration>) {
+ let mut session_infos_on_machine =
+ read_other_live_session_states(current_session_name, sock_dir, session_info_cache_dir);
+ for (name, info) in session_infos_on_machine.iter_mut() {
+ if name == current_session_name {
+ info.populate_plugin_list(current_session_plugin_list.clone());
+ info.available_layouts = available_layouts.to_vec();
+ }
}
+ let resurrectable_sessions =
+ find_resurrectable_sessions(&session_infos_on_machine, session_info_cache_dir);
+ (session_infos_on_machine, resurrectable_sessions)
+}
+
+pub fn scan_session_list_default_dirs(
+ current_session_name: &str,
+ available_layouts: &[LayoutInfo],
+ current_session_plugin_list: &BTreeMap<PluginId, RunPlugin>,
+) -> (BTreeMap<String, SessionInfo>, BTreeMap<String, Duration>) {
+ scan_session_list(
+ current_session_name,
+ available_layouts,
+ current_session_plugin_list,
+ &*ZELLIJ_SOCK_DIR,
+ &*ZELLIJ_SESSION_INFO_CACHE_DIR,
+ )
}
-fn read_other_live_session_states(current_session_name: &str) -> BTreeMap<String, SessionInfo> {
+fn read_other_live_session_states(
+ current_session_name: &str,
+ sock_dir: &Path,
+ session_info_cache_dir: &Path,
+) -> BTreeMap<String, SessionInfo> {
let mut other_session_names: Vec<(String, Duration)> = vec![];
let mut session_infos_on_machine = BTreeMap::new();
// we do this so that the session infos will be actual and we're
// reasonably sure their session is running
- if let Ok(files) = fs::read_dir(&*ZELLIJ_SOCK_DIR) {
+ if let Ok(files) = fs::read_dir(sock_dir) {
files.for_each(|file| {
if let Ok(file) = file {
if let Ok(file_name) = file.file_name().into_string() {
@@ -716,7 +755,6 @@ fn read_other_live_session_states(current_session_name: &str) -> BTreeMap<String
.ok()
.and_then(|f| f.created().ok().or_else(|| f.modified().ok()))
.and_then(|d| d.elapsed().ok())
- .map(|d| Duration::from_secs(d.as_secs()))
.unwrap_or_default();
other_session_names.push((file_name, creation_time));
}
@@ -726,7 +764,9 @@ fn read_other_live_session_states(current_session_name: &str) -> BTreeMap<String
}
for (session_name, creation_time) in other_session_names {
- let session_cache_file_name = session_info_cache_file_name(&session_name);
+ let session_cache_file_name = session_info_cache_dir
+ .join(&session_name)
+ .join("session-metadata.kdl");
if let Ok(raw_session_info) = fs::read_to_string(&session_cache_file_name) {
if let Ok(mut session_info) =
SessionInfo::from_string(&raw_session_info, &current_session_name)
@@ -741,8 +781,9 @@ fn read_other_live_session_states(current_session_name: &str) -> BTreeMap<String
fn find_resurrectable_sessions(
session_infos_on_machine: &BTreeMap<String, SessionInfo>,
+ session_info_cache_dir: &Path,
) -> BTreeMap<String, Duration> {
- match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) {
+ match fs::read_dir(session_info_cache_dir) {
Ok(files_in_session_info_folder) => {
let files_that_are_folders = files_in_session_info_folder
.filter_map(|f| f.ok().map(|f| f.path()))
@@ -754,7 +795,7 @@ fn find_resurrectable_sessions(
// this is not a dead session...
return None;
}
- let layout_file_name = session_layout_cache_file_name(&session_name);
+ let layout_file_name = folder_name.join("session-layout.kdl");
let ctime = match std::fs::metadata(&layout_file_name)
.and_then(|metadata| metadata.created())
{
@@ -773,9 +814,7 @@ fn find_resurrectable_sessions(
},
};
let elapsed_duration = ctime
- .map(|ctime| {
- Duration::from_secs(ctime.elapsed().ok().unwrap_or_default().as_secs())
- })
+ .and_then(|ctime| ctime.elapsed().ok())
.unwrap_or_default();
Some((session_name, elapsed_duration))
})
@@ -823,3 +862,108 @@ fn query_webserver_via_ipc(web_server_base_url: &str) -> Result<WebServerStatus>
Ok(WebServerStatus::Offline)
}
+
+#[cfg(test)]
+#[cfg(unix)]
+mod tests {
+ use super::*;
+ use std::os::unix::net::UnixListener;
+ use tempfile::tempdir;
+ use zellij_utils::data::SessionInfo;
+
+ fn make_socket(dir: &std::path::Path, name: &str) -> UnixListener {
+ UnixListener::bind(dir.join(name)).expect("bind unix socket")
+ }
+
+ fn write_metadata(info_dir: &std::path::Path, session: &str, info: &SessionInfo) {
+ let folder = info_dir.join(session);
+ std::fs::create_dir_all(&folder).unwrap();
+ std::fs::write(folder.join("session-metadata.kdl"), info.to_string()).unwrap();
+ }
+
+ fn write_layout(info_dir: &std::path::Path, session: &str) {
+ let folder = info_dir.join(session);
+ std::fs::create_dir_all(&folder).unwrap();
+ std::fs::write(folder.join("session-layout.kdl"), "layout { }").unwrap();
+ }
+
+ #[test]
+ fn scan_session_list_returns_empty_when_no_peers() {
+ let sock_dir = tempdir().unwrap();
+ let info_dir = tempdir().unwrap();
+ let (live, resurrectable) = scan_session_list(
+ "me",
+ &[],
+ &BTreeMap::new(),
+ sock_dir.path(),
+ info_dir.path(),
+ );
+ assert!(live.is_empty());
+ assert!(resurrectable.is_empty());
+ }
+
+ #[test]
+ fn scan_session_list_finds_peer_from_socket_and_metadata() {
+ let sock_dir = tempdir().unwrap();
+ let info_dir = tempdir().unwrap();
+ let peer = "peer-alpha";
+ let _listener = make_socket(sock_dir.path(), peer);
+ write_metadata(info_dir.path(), peer, &SessionInfo::new(peer.to_string()));
+
+ let (live, resurrectable) = scan_session_list(
+ "me",
+ &[],
+ &BTreeMap::new(),
+ sock_dir.path(),
+ info_dir.path(),
+ );
+ assert_eq!(live.len(), 1);
+ assert!(live.contains_key(peer));
+ assert!(resurrectable.is_empty());
+ }
+
+ #[test]
+ fn scan_session_list_finds_resurrectable_from_orphan_metadata() {
+ let sock_dir = tempdir().unwrap();
+ let info_dir = tempdir().unwrap();
+ write_layout(info_dir.path(), "dead-beta");
+
+ let (live, resurrectable) = scan_session_list(
+ "me",
+ &[],
+ &BTreeMap::new(),
+ sock_dir.path(),
+ info_dir.path(),
+ );
+ assert!(live.is_empty());
+ assert_eq!(resurrectable.len(), 1);
+ assert!(resurrectable.contains_key("dead-beta"));
+ }
+
+ #[test]
+ fn scan_session_list_separates_live_from_resurrectable() {
+ let sock_dir = tempdir().unwrap();
+ let info_dir = tempdir().unwrap();
+ for name in ["live-a", "live-b", "live-c"] {
+ let _listener = make_socket(sock_dir.path(), name);
+ write_metadata(info_dir.path(), name, &SessionInfo::new(name.to_string()));
+ std::mem::forget(_listener);
+ }
+ for name in ["dead-a", "dead-b"] {
+ write_layout(info_dir.path(), name);
+ }
+
+ let (live, resurrectable) = scan_session_list(
+ "me",
+ &[],
+ &BTreeMap::new(),
+ sock_dir.path(),
+ info_dir.path(),
+ );
+ assert_eq!(live.len(), 3);
+ assert_eq!(resurrectable.len(), 2);
+ for name in ["live-a", "live-b", "live-c"] {
+ assert!(!resurrectable.contains_key(name));
+ }
+ }
+}
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index 5e3cd6a6..1f2b4d14 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -10,7 +10,7 @@ pub mod output;
pub mod panes;
pub mod tab;
-mod background_jobs;
+pub mod background_jobs;
mod global_async_runtime;
mod logging_pipe;
mod pane_groups;
@@ -218,12 +218,11 @@ impl SessionConfiguration {
pub fn set_client_runtime_configuration(&mut self, client_id: ClientId, client_config: Config) {
self.runtime_config.insert(client_id, client_config);
}
- pub fn get_client_keybinds(&self, client_id: &ClientId) -> Keybinds {
+ pub fn get_client_keybinds(&self, client_id: &ClientId) -> &Keybinds {
self.runtime_config
.get(client_id)
- .or_else(|| Some(&self.saved_config))
- .map(|c| c.keybinds.clone())
- .unwrap_or_default()
+ .map(|c| &c.keybinds)
+ .unwrap_or(&self.saved_config.keybinds)
}
pub fn get_client_default_input_mode(&self, client_id: &ClientId) -> InputMode {
self.runtime_config
@@ -339,7 +338,7 @@ impl SessionMetaData {
pub fn get_client_keybinds_and_mode(
&self,
client_id: &ClientId,
- ) -> Option<(Keybinds, &InputMode, InputMode)> {
+ ) -> Option<(&Keybinds, &InputMode, InputMode)> {
// (keybinds, current_input_mode,
// default_input_mode)
let client_keybinds = self.session_configuration.get_client_keybinds(client_id);
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs
index 1445435c..c3e5a588 100644
--- a/zellij-server/src/panes/grid.rs
+++ b/zellij-server/src/panes/grid.rs
@@ -350,6 +350,33 @@ fn subtract_isize_from_usize(u: usize, i: isize) -> usize {
}
}
+pub fn parse_osc7_path(raw: &[u8]) -> Option<std::path::PathBuf> {
+ let s = std::str::from_utf8(raw).ok()?;
+ let rest = s.strip_prefix("file://")?;
+ let slash_idx = rest.find('/')?;
+ let encoded_path = &rest[slash_idx..];
+ let mut out = Vec::with_capacity(encoded_path.len());
+ let bytes = encoded_path.as_bytes();
+ let mut i = 0;
+ while i < bytes.len() {
+ if bytes[i] == b'%' {
+ let end = i.checked_add(3)?;
+ if end > bytes.len() {
+ return None;
+ }
+ let hex = std::str::from_utf8(&bytes[i + 1..end]).ok()?;
+ let byte = u8::from_str_radix(hex, 16).ok()?;
+ out.push(byte);
+ i = end;
+ } else {
+ out.push(bytes[i]);
+ i += 1;
+ }
+ }
+ let decoded = String::from_utf8(out).ok()?;
+ Some(std::path::PathBuf::from(decoded))
+}
+
macro_rules! dump_screen {
($lines:expr) => {{
let mut is_first = true;
@@ -605,6 +632,7 @@ pub struct Grid {
pub focus_event_tracking: bool,
pub search_results: SearchResult,
pub pending_clipboard_update: Option<String>,
+ pub pending_osc7_cwd: Option<std::path::PathBuf>,
/// Pending desktop notifications: (payload, terminator)
/// Payload is the semicolon-joined params after "99", terminator is "\x07" or "\x1b\\"
pub pending_desktop_notifications: Vec<(String, String)>,
@@ -892,6 +920,7 @@ impl Grid {
search_results: Default::default(),
sixel_grid,
pending_clipboard_update: None,
+ pending_osc7_cwd: None,
pending_desktop_notifications: Vec::new(),
ui_component_bytes: None,
style,
@@ -2324,9 +2353,19 @@ impl Grid {
best.map(|(_layer, plugin_id, pattern, matched, ctx)| (plugin_id, pattern, matched, ctx))
}
- pub fn set_hover_position(&mut self, new_pos: Option<Position>) {
+ pub fn set_hover_position(&mut self, new_pos: Option<Position>) -> bool {
if self.hover_position == new_pos {
- return;
+ return false;
+ }
+
+ let has_hover_consumer = self
+ .plugin_highlights
+ .values()
+ .any(|highlights| highlights.iter().any(|(_, c)| c.on_hover));
+
+ if !has_hover_consumer {
+ self.hover_position = new_pos;
+ return false;
}
// Mark the canonical group containing the old hover row dirty.
@@ -2340,6 +2379,7 @@ impl Grid {
self.hover_position = new_pos;
self.recompute_hover_tooltip();
+ true
}
/// Mark all physical rows belonging to the logical line group that contains
@@ -3428,6 +3468,14 @@ impl Perform for Grid {
}
},
+ b"7" => {
+ if let Some(raw) = params.get(1) {
+ if let Some(path) = parse_osc7_path(raw) {
+ self.pending_osc7_cwd = Some(path);
+ }
+ }
+ },
+
// Set color index.
b"4" => {
for chunk in params[1..].chunks(2) {
@@ -4799,3 +4847,42 @@ fn is_selection_boundary_character(character: char) -> bool {
#[cfg(test)]
#[path = "./unit/grid_tests.rs"]
mod grid_tests;
+
+#[cfg(test)]
+mod osc7_parser_tests {
+ use super::parse_osc7_path;
+ use std::path::PathBuf;
+
+ #[test]
+ fn osc7_parser_extracts_path() {
+ let raw = b"file://host/home/user/foo";
+ assert_eq!(parse_osc7_path(raw), Some(PathBuf::from("/home/user/foo")));
+ }
+
+ #[test]
+ fn osc7_parser_decodes_percent_encoded_path() {
+ let raw = b"file://localhost/tmp/with%20space";
+ assert_eq!(parse_osc7_path(raw), Some(PathBuf::from("/tmp/with space")));
+ }
+
+ #[test]
+ fn osc7_parser_rejects_missing_scheme() {
+ assert_eq!(parse_osc7_path(b"/home/user"), None);
+ }
+
+ #[test]
+ fn osc7_parser_rejects_missing_path() {
+ assert_eq!(parse_osc7_path(b"file://host"), None);
+ }
+
+ #[test]
+ fn osc7_parser_rejects_truncated_percent_escape() {
+ assert_eq!(parse_osc7_path(b"file://host/foo%2"), None);
+ }
+
+ #[test]
+ fn osc7_parser_rejects_non_utf8_input() {
+ let raw = &[0xFFu8, 0xFE, 0xFD];
+ assert_eq!(parse_osc7_path(raw), None);
+ }
+}
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs
index bed9881d..5aeef4f2 100644
--- a/zellij-server/src/panes/terminal_pane.rs
+++ b/zellij-server/src/panes/terminal_pane.rs
@@ -594,6 +594,10 @@ impl Pane for TerminalPane {
self.grid.pending_desktop_notifications.drain(..).collect()
}
+ fn drain_osc7_cwd(&mut self) -> Option<std::path::PathBuf> {
+ self.grid.pending_osc7_cwd.take()
+ }
+
fn start_selection(&mut self, start: &Position, _client_id: ClientId) {
self.grid.start_selection(start);
self.set_should_render(true);
@@ -987,9 +991,12 @@ impl Pane for TerminalPane {
self.grid.clear_plugin_highlights(plugin_id);
self.set_should_render(true);
}
- fn set_hover_position(&mut self, position: Option<Position>) {
- self.grid.set_hover_position(position);
- self.set_should_render(true);
+ fn set_hover_position(&mut self, position: Option<Position>) -> bool {
+ let changed = self.grid.set_hover_position(position);
+ if changed {
+ self.set_should_render(true);
+ }
+ changed
}
fn cached_hover_tooltip(&self) -> Option<String> {
self.grid.cached_hover_tooltip.clone()
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs
index 31d7e74b..86db429d 100644
--- a/zellij-server/src/plugins/wasm_bridge.rs
+++ b/zellij-server/src/plugins/wasm_bridge.rs
@@ -2151,6 +2151,7 @@ fn check_event_permission(
| Event::FailedToWriteConfigToDisk(..)
| Event::CommandPaneReRun(..)
| Event::CwdChanged(..)
+ | Event::CommandChanged(..)
| Event::AvailableLayoutInfo(..)
| Event::PluginConfigurationChanged(..)
| Event::HighlightClicked { .. }
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 24113d0b..1a941026 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -82,9 +82,9 @@ use zellij_utils::{
ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse, ProtobufGetPaneInfoResponse,
ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse,
- ProtobufGetSessionEnvironmentVariablesResponse, ProtobufGetTabInfoResponse,
- ProtobufHideFloatingPanesResponse, ProtobufNewTabResponse, ProtobufNewTabsResponse,
- ProtobufOpenCommandPaneBackgroundResponse,
+ ProtobufGetSessionEnvironmentVariablesResponse, ProtobufGetSessionListResponse,
+ ProtobufGetTabInfoResponse, ProtobufHideFloatingPanesResponse, ProtobufNewTabResponse,
+ ProtobufNewTabsResponse, ProtobufOpenCommandPaneBackgroundResponse,
ProtobufOpenCommandPaneFloatingNearPluginResponse,
ProtobufOpenCommandPaneFloatingResponse,
ProtobufOpenCommandPaneInPlaceOfPaneIdResponse,
@@ -124,9 +124,9 @@ macro_rules! apply_action {
$env.capabilities.clone(),
$env.client_attributes.clone(),
$env.default_shell.clone(),
- $env.default_layout.clone(),
+ &$env.default_layout,
None,
- $env.keybinds.clone(),
+ &$env.keybinds,
$env.default_mode.clone(),
None,
) {
@@ -438,6 +438,7 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
},
PluginCommand::WatchFilesystem => watch_filesystem(env),
PluginCommand::ListWindowsVolumes => list_windows_volumes(env),
+ PluginCommand::GetSessionList => get_session_list(env),
PluginCommand::DumpSessionLayout { tab_index } => {
dump_session_layout(env, tab_index)
},
@@ -1449,9 +1450,9 @@ fn run_action(env: &PluginEnv, mut action: Action, context: BTreeMap<String, Str
capabilities,
client_attributes,
default_shell,
- default_layout,
+ &default_layout,
None,
- keybinds,
+ &keybinds,
default_mode,
None,
) {
@@ -4079,6 +4080,44 @@ fn get_pane_running_command(env: &PluginEnv, pane_id: PaneId) {
write_pane_running_command_response(env, pane_id, response);
}
+fn get_session_list(env: &PluginEnv) {
+ use crate::background_jobs::{scan_session_list_default_dirs, session_scan_state};
+ use zellij_utils::data::{GetSessionListResponse, SessionListSnapshot};
+
+ let response = match session_scan_state() {
+ Some(state) => {
+ let (session_name, available_layouts, plugin_list) = {
+ let name = state.current_session_name.lock().unwrap().clone();
+ let info = state.current_session_info.lock().unwrap().clone();
+ let plugins = state.current_session_plugin_list.lock().unwrap().clone();
+ (name, info.available_layouts, plugins)
+ };
+
+ let (live_sessions_map, resurrectable_sessions_map) =
+ scan_session_list_default_dirs(&session_name, &available_layouts, &plugin_list);
+
+ let _ = env
+ .senders
+ .send_to_screen(ScreenInstruction::UpdateSessionInfos(
+ live_sessions_map.clone(),
+ resurrectable_sessions_map.clone(),
+ ));
+
+ let snapshot = SessionListSnapshot {
+ live_sessions: live_sessions_map.into_values().collect(),
+ resurrectable_sessions: resurrectable_sessions_map.into_iter().collect(),
+ };
+ GetSessionListResponse::Ok(snapshot)
+ },
+ None => GetSessionListResponse::Err("Session-scan state not initialized".to_string()),
+ };
+
+ let protobuf_response = ProtobufGetSessionListResponse::from(response);
+ wasi_write_object(env, &protobuf_response.encode_to_vec())
+ .with_context(|| "failed to write get_session_list response".to_string())
+ .non_fatal();
+}
+
fn send_get_pane_running_command_request(
env: &PluginEnv,
pane_id: PaneId,
@@ -4446,9 +4485,9 @@ fn try_edit_layout(
env.capabilities.clone(),
env.client_attributes.clone(),
env.default_shell.clone(),
- env.default_layout.clone(),
+ &env.default_layout,
None,
- env.keybinds.clone(),
+ &env.keybinds,
env.default_mode.clone(),
None,
)
@@ -5311,7 +5350,8 @@ fn check_command_permission(
| PluginCommand::SaveSession
| PluginCommand::CurrentSessionLastSavedTime
| PluginCommand::GetPaneInfo(..)
- | PluginCommand::GetTabInfo(..) => PermissionType::ReadApplicationState,
+ | PluginCommand::GetTabInfo(..)
+ | PluginCommand::GetSessionList => PermissionType::ReadApplicationState,
PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => {
PermissionType::Reconfigure
},
diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs
index 0fdcddc4..478a60e3 100644
--- a/zellij-server/src/pty.rs
+++ b/zellij-server/src/pty.rs
@@ -154,6 +154,7 @@ pub enum PtyInstruction {
response_channel: crossbeam::channel::Sender<GetPaneCwdResponse>,
},
UpdateAndReportCwds,
+ NotifyCwdFromOsc7(u32, PathBuf),
Exit,
}
@@ -186,6 +187,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::GetPaneRunningCommand { .. } => PtyContext::GetPaneRunningCommand,
PtyInstruction::GetPaneCwd { .. } => PtyContext::GetPaneCwd,
PtyInstruction::UpdateAndReportCwds => PtyContext::UpdateAndReportCwds,
+ PtyInstruction::NotifyCwdFromOsc7(..) => PtyContext::NotifyCwdFromOsc7,
PtyInstruction::Exit => PtyContext::Exit,
}
}
@@ -202,6 +204,9 @@ pub(crate) struct Pty {
post_command_discovery_hook: Option<String>,
plugin_cwds: HashMap<u32, PathBuf>, // plugin_id -> cwd
terminal_cwds: HashMap<u32, PathBuf>, // terminal_id -> cwd
+ pane_activity_flags: HashMap<u32, std::sync::Arc<std::sync::atomic::AtomicBool>>,
+ terminal_cmds: HashMap<u32, Vec<String>>,
+ terminal_foreground_cmds: HashMap<u32, Vec<String>>,
}
pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
@@ -890,6 +895,9 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
PtyInstruction::UpdateAndReportCwds => {
pty.update_and_report_cwds();
},
+ PtyInstruction::NotifyCwdFromOsc7(terminal_id, path) => {
+ pty.notify_cwd_from_osc7(terminal_id, path);
+ },
PtyInstruction::Exit => break,
}
}
@@ -914,6 +922,9 @@ impl Pty {
post_command_discovery_hook,
plugin_cwds: HashMap::new(),
terminal_cwds: HashMap::new(),
+ pane_activity_flags: HashMap::new(),
+ terminal_cmds: HashMap::new(),
+ terminal_foreground_cmds: HashMap::new(),
}
}
pub fn get_default_terminal(
@@ -1118,13 +1129,15 @@ impl Pty {
os_input.spawn_terminal(terminal_action, quit_cb, self.default_editor.clone())
})
.with_context(err_context)?;
+ let activity_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let terminal_bytes = async_runtime().spawn({
let err_context =
|terminal_id: u32| format!("failed to run async task for terminal {terminal_id}");
let senders = self.bus.senders.clone();
let debug_to_file = self.debug_to_file;
+ let activity_flag = activity_flag.clone();
async move {
- TerminalBytes::new(terminal_id, reader, senders, debug_to_file)
+ TerminalBytes::new(terminal_id, reader, senders, debug_to_file, activity_flag)
.listen()
.await
.with_context(|| err_context(terminal_id))
@@ -1133,6 +1146,7 @@ impl Pty {
});
self.task_handles.insert(terminal_id, terminal_bytes);
+ self.pane_activity_flags.insert(terminal_id, activity_flag);
if let Some(child_pid) = child_pid {
self.id_to_child_pid.insert(terminal_id, child_pid);
self.capture_initial_cwd(terminal_id, child_pid);
@@ -1318,18 +1332,28 @@ impl Pty {
}
match reader_result {
Ok(reader) => {
+ let activity_flag =
+ std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let terminal_bytes = async_runtime().spawn({
let senders = self.bus.senders.clone();
let debug_to_file = self.debug_to_file;
+ let activity_flag = activity_flag.clone();
async move {
- TerminalBytes::new(terminal_id, reader, senders, debug_to_file)
- .listen()
- .await
- .context("failed to spawn terminals for layout")
- .fatal();
+ TerminalBytes::new(
+ terminal_id,
+ reader,
+ senders,
+ debug_to_file,
+ activity_flag,
+ )
+ .listen()
+ .await
+ .context("failed to spawn terminals for layout")
+ .fatal();
}
});
self.task_handles.insert(terminal_id, terminal_bytes);
+ self.pane_activity_flags.insert(terminal_id, activity_flag);
},
_ => match run_command {
Some(run_command) => {
@@ -1486,18 +1510,28 @@ impl Pty {
}
match reader_result {
Ok(reader) => {
+ let activity_flag =
+ std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let terminal_bytes = async_runtime().spawn({
let senders = self.bus.senders.clone();
let debug_to_file = self.debug_to_file;
+ let activity_flag = activity_flag.clone();
async move {
- TerminalBytes::new(terminal_id, reader, senders, debug_to_file)
- .listen()
- .await
- .context("failed to spawn terminals for layout")
- .fatal();
+ TerminalBytes::new(
+ terminal_id,
+ reader,
+ senders,
+ debug_to_file,
+ activity_flag,
+ )
+ .listen()
+ .await
+ .context("failed to spawn terminals for layout")
+ .fatal();
}
});
self.task_handles.insert(terminal_id, terminal_bytes);
+ self.pane_activity_flags.insert(terminal_id, activity_flag);
},
_ => match run_command {
Some(run_command) => {
@@ -1779,6 +1813,10 @@ impl Pty {
.with_context(err_context)
.non_fatal();
}
+ self.pane_activity_flags.remove(&id);
+ self.terminal_cwds.remove(&id);
+ self.terminal_cmds.remove(&id);
+ self.terminal_foreground_cmds.remove(&id);
self.bus
.os_input
.as_ref()
@@ -1865,13 +1903,15 @@ impl Pty {
os_input.re_run_command_in_terminal(id, run_command, quit_cb)
})
.with_context(err_context)?;
+ let activity_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let terminal_bytes = async_runtime().spawn({
let err_context =
|pane_id| format!("failed to run async task for pane {pane_id:?}");
let senders = self.bus.senders.clone();
let debug_to_file = self.debug_to_file;
+ let activity_flag = activity_flag.clone();
async move {
- TerminalBytes::new(id, reader, senders, debug_to_file)
+ TerminalBytes::new(id, reader, senders, debug_to_file, activity_flag)
.listen()
.await
.with_context(|| err_context(pane_id))
@@ -1880,6 +1920,7 @@ impl Pty {
});
self.task_handles.insert(id, terminal_bytes);
+ self.pane_activity_flags.insert(id, activity_flag);
if let Some(child_pid) = child_pid {
self.id_to_child_pid.insert(id, child_pid);
self.capture_initial_cwd(id, child_pid);
@@ -2023,28 +2064,44 @@ impl Pty {
}
pub fn update_and_report_cwds(&mut self) {
- let terminal_ids: Vec<u32> = self.id_to_child_pid.keys().copied().collect();
+ use std::sync::atomic::Ordering;
- let pids: Vec<_> = terminal_ids
+ let active_terminal_ids: Vec<u32> = self
+ .id_to_child_pid
+ .keys()
+ .copied()
+ .filter(|id| {
+ self.pane_activity_flags
+ .get(id)
+ .map(|f| f.swap(false, Ordering::Relaxed))
+ .unwrap_or(false)
+ })
+ .collect();
+
+ if active_terminal_ids.is_empty() {
+ return;
+ }
+
+ let pids: Vec<_> = active_terminal_ids
.iter()
- .filter_map(|id| self.id_to_child_pid.get(&id))
+ .filter_map(|id| self.id_to_child_pid.get(id))
.copied()
.collect();
- let (pids_to_cwds, _) = self
+ let (pids_to_cwds, pids_to_cmds) = self
.bus
.os_input
.as_ref()
.map(|os_input| os_input.get_cwds(pids))
.unwrap_or_default();
- for terminal_id in terminal_ids {
- let process_id = self.id_to_child_pid.get(&terminal_id);
+ for terminal_id in &active_terminal_ids {
+ let process_id = self.id_to_child_pid.get(terminal_id);
let cwd = process_id.and_then(|pid| pids_to_cwds.get(pid));
if let Some(cwd) = cwd {
- if self.terminal_cwds.get(&terminal_id) != Some(cwd) {
- let pane_id = PaneId::Terminal(terminal_id);
+ if self.terminal_cwds.get(terminal_id) != Some(cwd) {
+ let pane_id = PaneId::Terminal(*terminal_id);
let focused_client_ids: Vec<ClientId> = self
.active_panes
.iter()
@@ -2060,7 +2117,62 @@ impl Pty {
Event::CwdChanged(pane_id.into(), cwd.clone(), focused_client_ids),
)]));
}
- self.terminal_cwds.insert(terminal_id, cwd.clone());
+ self.terminal_cwds.insert(*terminal_id, cwd.clone());
+ }
+
+ let cmd = process_id.and_then(|pid| pids_to_cmds.get(pid));
+ if let Some(cmd) = cmd {
+ self.terminal_cmds.insert(*terminal_id, cmd.clone());
+ }
+ }
+
+ let ppids_to_cmds = self
+ .bus
+ .os_input
+ .as_ref()
+ .map(|os_input| os_input.get_all_cmds_by_ppid(&self.post_command_discovery_hook))
+ .unwrap_or_default();
+
+ for terminal_id in &active_terminal_ids {
+ let process_id = self.id_to_child_pid.get(terminal_id);
+ let foreground_cmd: Vec<String> = process_id
+ .and_then(|pid| ppids_to_cmds.get(&pid.to_string()))
+ .cloned()
+ .unwrap_or_default();
+
+ if self.terminal_foreground_cmds.get(terminal_id) != Some(&foreground_cmd) {
+ let pane_id = PaneId::Terminal(*terminal_id);
+ let focused_client_ids: Vec<ClientId> = self
+ .active_panes
+ .iter()
+ .filter(|(_, active_pane)| *active_pane == &pane_id)
+ .map(|(client_id, _)| *client_id)
+ .collect();
+ let (command, is_foreground) = if foreground_cmd.is_empty() {
+ let shell_cmd = self
+ .terminal_cmds
+ .get(terminal_id)
+ .cloned()
+ .unwrap_or_default();
+ (shell_cmd, false)
+ } else {
+ (foreground_cmd.clone(), true)
+ };
+ let _ = self
+ .bus
+ .senders
+ .send_to_plugin(PluginInstruction::Update(vec![(
+ None,
+ None,
+ Event::CommandChanged(
+ pane_id.into(),
+ command,
+ is_foreground,
+ focused_client_ids,
+ ),
+ )]));
+ self.terminal_foreground_cmds
+ .insert(*terminal_id, foreground_cmd);
}
}
}
@@ -2074,6 +2186,32 @@ impl Pty {
self.post_command_discovery_hook = post_command_discovery_hook;
}
+ pub fn notify_cwd_from_osc7(&mut self, terminal_id: u32, path: PathBuf) {
+ use std::sync::atomic::Ordering;
+
+ if self.terminal_cwds.get(&terminal_id) != Some(&path) {
+ let pane_id = PaneId::Terminal(terminal_id);
+ let focused_client_ids: Vec<ClientId> = self
+ .active_panes
+ .iter()
+ .filter(|(_, active_pane)| *active_pane == &pane_id)
+ .map(|(client_id, _)| *client_id)
+ .collect();
+ let _ = self
+ .bus
+ .senders
+ .send_to_plugin(PluginInstruction::Update(vec![(
+ None,
+ None,
+ Event::CwdChanged(pane_id.into(), path.clone(), focused_client_ids),
+ )]));
+ self.terminal_cwds.insert(terminal_id, path);
+ }
+ if let Some(flag) = self.pane_activity_flags.get(&terminal_id) {
+ flag.store(false, Ordering::Relaxed);
+ }
+ }
+
pub fn send_sigint_to_pane(&self, pane_id: PaneId) {
let err_context = || format!("failed to send SIGINT to pane {:?}", pane_id);
@@ -2263,3 +2401,7 @@ pub fn get_default_shell() -> PathBuf {
"cmd.exe".to_string()
}))
}
+
+#[cfg(test)]
+#[path = "./unit/pty_tests.rs"]
+mod pty_tests;
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index b060511a..155997f9 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -203,9 +203,9 @@ pub(crate) fn route_action(
capabilities: PluginCapabilities,
client_attributes: ClientAttributes,
default_shell: Option<TerminalAction>,
- default_layout: Box<Layout>,
+ default_layout: &Layout,
mut seen_cli_pipes: Option<&mut HashSet<String>>,
- client_keybinds: Keybinds,
+ client_keybinds: &Keybinds,
default_mode: InputMode,
os_input: Option<Box<dyn ServerOsApi>>,
) -> Result<(bool, Option<ActionCompletionResult>)> {
@@ -1517,7 +1517,7 @@ pub(crate) fn route_action(
Action::BreakPane => {
senders
.send_to_screen(ScreenInstruction::BreakPane(
- default_layout.clone(),
+ Box::new(default_layout.clone()),
default_shell.clone(),
client_id,
Some(NotificationEnd::new(completion_tx)),
@@ -2230,27 +2230,22 @@ pub(crate) fn route_thread_main(
.unwrap()
.set_last_active_client(client_id);
- let session_data_assets =
- session_data.read().as_ref().unwrap().as_ref().map(|s| {
- (
- s.senders.clone(),
- s.capabilities.clone(),
- s.client_attributes.clone(),
- s.default_shell.clone(),
- s.layout.clone(),
- s.session_configuration
- .get_client_configuration(&client_id)
- .options
- .default_mode
- .unwrap_or(InputMode::Normal)
- .clone(),
- )
- });
- if let Some((keybinds, input_mode, default_input_mode)) = session_data
- .read()
- .unwrap()
- .as_ref()
- .and_then(|s| s.get_client_keybinds_and_mode(&client_id))
+ let session_data_guard = session_data.read().unwrap();
+ let session_data_assets = session_data_guard.as_ref().map(|s| {
+ (
+ s.senders.clone(),
+ s.capabilities.clone(),
+ s.client_attributes.clone(),
+ s.default_shell.clone(),
+ &*s.layout,
+ s.session_configuration
+ .get_client_default_input_mode(&client_id),
+ )
+ });
+ if let Some((keybinds, input_mode, default_input_mode)) =
+ session_data_guard
+ .as_ref()
+ .and_then(|s| s.get_client_keybinds_and_mode(&client_id))
{
if let Some((
senders,
@@ -2288,9 +2283,9 @@ pub(crate) fn route_thread_main(
capabilities,
client_attributes.clone(),
default_shell.clone(),
- layout.clone(),
+ layout,
Some(&mut seen_cli_pipes),
- keybinds.clone(),
+ keybinds,
client_input_mode,
Some(os_input.clone()),
) {
@@ -2345,25 +2340,19 @@ pub(crate) fn route_thread_main(
});
}
- let session_data_assets =
- session_data.read().unwrap().as_ref().map(|s| {
- (
- s.senders.clone(),
- s.capabilities.clone(),
- s.client_attributes.clone(),
- s.default_shell.clone(),
- s.layout.clone(),
- s.session_configuration
- .get_client_configuration(&client_id)
- .options
- .default_mode
- .unwrap_or(InputMode::Normal)
- .clone(),
- s.session_configuration
- .get_client_keybinds(&client_id)
- .clone(),
- )
- });
+ let session_data_guard = session_data.read().unwrap();
+ let session_data_assets = session_data_guard.as_ref().map(|s| {
+ (
+ s.senders.clone(),
+ s.capabilities.clone(),
+ s.client_attributes.clone(),
+ s.default_shell.clone(),
+ &*s.layout,
+ s.session_configuration
+ .get_client_default_input_mode(&client_id),
+ s.session_configuration.get_client_keybinds(&client_id),
+ )
+ });
if let Some((
senders,
capabilities,
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 81ee975b..f05b0823 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -50,7 +50,7 @@ use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
use zellij_utils::input::config::Config;
use zellij_utils::input::keybinds::Keybinds;
-use zellij_utils::input::mouse::MouseEvent;
+use zellij_utils::input::mouse::{MouseEvent, MouseEventType};
use zellij_utils::input::options::Clipboard;
use zellij_utils::ipc::{ExitReason, ServerToClientMsg};
use zellij_utils::pane_size::{PaneGeom, Size, SizeInPixels};
@@ -1320,10 +1320,10 @@ pub(crate) struct Screen {
copy_options: CopyOptions,
debug: bool,
session_name: String,
- session_infos_on_machine: BTreeMap<String, SessionInfo>, // String is the session name, can
+ peer_sessions_cache: BTreeMap<String, SessionInfo>, // String is the session name, can
// also be this session
- resurrectable_sessions: BTreeMap<String, Duration>, // String is the session name, duration is
- // its creation time
+ resurrectable_sessions_cache: BTreeMap<String, Duration>, // String is the session name,
+ // duration is its creation time
default_layout: Box<Layout>,
default_shell: PathBuf,
styled_underlines: bool,
@@ -1395,9 +1395,9 @@ impl Screen {
) -> Self {
let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone());
- let mut session_infos_on_machine = BTreeMap::new();
- let resurrectable_sessions = BTreeMap::new();
- session_infos_on_machine.insert(session_name.clone(), session_info);
+ let mut peer_sessions_cache = BTreeMap::new();
+ let resurrectable_sessions_cache = BTreeMap::new();
+ peer_sessions_cache.insert(session_name.clone(), session_info);
let current_pane_group = PaneGroups::new(bus.senders.clone());
Screen {
bus,
@@ -1424,7 +1424,7 @@ impl Screen {
copy_options,
debug,
session_name,
- session_infos_on_machine,
+ peer_sessions_cache,
default_layout,
default_layout_name,
default_shell,
@@ -1434,7 +1434,7 @@ impl Screen {
styled_underlines,
osc8_hyperlinks,
arrow_fonts,
- resurrectable_sessions,
+ resurrectable_sessions_cache,
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
default_editor,
@@ -2939,16 +2939,31 @@ impl Screen {
.senders
.send_to_background_jobs(BackgroundJob::ReportSessionInfo(
self.session_name.to_owned(),
- session_info,
+ session_info.clone(),
))
.with_context(err_context)?;
+ self.peer_sessions_cache
+ .insert(self.session_name.clone(), session_info);
+ let mut live_sessions: Vec<SessionInfo> =
+ self.peer_sessions_cache.values().cloned().collect();
+ for info in live_sessions.iter_mut() {
+ info.is_current_session = info.name == self.session_name;
+ }
+ let resurrectable_sessions: Vec<(String, Duration)> = self
+ .resurrectable_sessions_cache
+ .iter()
+ .map(|(n, d)| (n.clone(), *d))
+ .collect();
self.bus
.senders
- .send_to_background_jobs(BackgroundJob::ReadAllSessionInfosOnMachine)
+ .send_to_plugin(PluginInstruction::Update(vec![(
+ None,
+ None,
+ Event::SessionUpdate(live_sessions, resurrectable_sessions),
+ )]))
.with_context(err_context)?;
- // TODO: consider moving this elsewhere
self.bus
.senders
.send_to_background_jobs(BackgroundJob::QueryZellijWebServerStatus)
@@ -2971,16 +2986,16 @@ impl Screen {
new_session_infos: BTreeMap<String, SessionInfo>,
resurrectable_sessions: BTreeMap<String, Duration>,
) -> Result<()> {
- self.session_infos_on_machine = new_session_infos;
- self.resurrectable_sessions = resurrectable_sessions;
+ self.peer_sessions_cache = new_session_infos;
+ self.resurrectable_sessions_cache = resurrectable_sessions;
self.bus
.senders
.send_to_plugin(PluginInstruction::Update(vec![(
None,
None,
Event::SessionUpdate(
- self.session_infos_on_machine.values().cloned().collect(),
- self.resurrectable_sessions
+ self.peer_sessions_cache.values().cloned().collect(),
+ self.resurrectable_sessions_cache
.iter()
.map(|(n, c)| (n.clone(), c.clone()))
.collect(),
@@ -4215,6 +4230,12 @@ impl Screen {
}
}
pub fn handle_mouse_event(&mut self, event: MouseEvent, client_id: ClientId) {
+ let is_bare_motion = event.event_type == MouseEventType::Motion
+ && !event.left
+ && !event.right
+ && !event.middle
+ && !event.wheel_up
+ && !event.wheel_down;
match self
.get_active_tab_mut(client_id)
.and_then(|tab| tab.handle_mouse_event(&event, client_id))
@@ -4240,10 +4261,12 @@ impl Screen {
}
}
if mouse_effect.state_changed {
- let _ = self.log_and_report_session_state();
+ if !is_bare_motion {
+ let _ = self.log_and_report_session_state();
+ }
should_render = true;
}
- if !mouse_effect.leave_clipboard_message {
+ if !mouse_effect.leave_clipboard_message && !is_bare_motion {
let target_plugin_ids =
self.targeted_plugin_ids(client_id, EventType::InputReceived);
let plugin_updates: Vec<_> = target_plugin_ids
@@ -4256,7 +4279,7 @@ impl Screen {
.senders
.send_to_plugin(PluginInstruction::Update(plugin_updates));
}
- should_render = true; // TODO: do we need this?
+ should_render = true;
}
if should_render {
self.render(None).non_fatal();
@@ -7923,7 +7946,7 @@ pub(crate) fn screen_thread_main(
_completion_tx, // the action ends here, dropping this will release anything
// waiting for it
) => {
- if screen.session_infos_on_machine.contains_key(&name) {
+ if screen.peer_sessions_cache.contains_key(&name) {
let error_text = "A session by this name already exists.";
log::error!("{}", error_text);
if let Some(os_input) = &mut screen.bus.os_input {
@@ -7934,7 +7957,7 @@ pub(crate) fn screen_thread_main(
},
);
}
- } else if screen.resurrectable_sessions.contains_key(&name) {
+ } else if screen.resurrectable_sessions_cache.contains_key(&name) {
let error_text =
"A resurrectable session by this name exists, cannot use this name.";
log::error!("{}", error_text);
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 0eb953c7..928f7c79 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -415,6 +415,9 @@ pub trait Pane {
fn drain_desktop_notifications(&mut self) -> Vec<(String, String)> {
vec![]
}
+ fn drain_osc7_cwd(&mut self) -> Option<std::path::PathBuf> {
+ None
+ }
fn render_full_viewport(&mut self) {}
fn relative_position(&self, position_on_screen: &Position) -> Position {
position_on_screen.relative_to(self.get_content_y(), self.get_content_x())
@@ -634,7 +637,9 @@ pub trait Pane {
) {
}
fn clear_plugin_highlights(&mut self, _plugin_id: u32) {}
- fn set_hover_position(&mut self, _position: Option<Position>) {}
+ fn set_hover_position(&mut self, _position: Option<Position>) -> bool {
+ false
+ }
fn cached_hover_tooltip(&self) -> Option<String> {
None
}
@@ -2720,6 +2725,7 @@ impl Tab {
let messages_to_pty = terminal_output.drain_messages_to_pty();
let clipboard_update = terminal_output.drain_clipboard_update();
let desktop_notifications = terminal_output.drain_desktop_notifications();
+ let osc7_cwd = terminal_output.drain_osc7_cwd();
for message in messages_to_pty {
self.write_to_pane_id_without_preprocessing(message, PaneId::Terminal(pid))
.with_context(err_context)?;
@@ -2732,6 +2738,11 @@ impl Tab {
self.forward_desktop_notifications(desktop_notifications, pid)
.with_context(err_context)?;
}
+ if let Some(path) = osc7_cwd {
+ let _ = self
+ .senders
+ .send_to_pty(PtyInstruction::NotifyCwdFromOsc7(pid, path));
+ }
}
Ok(())
}
diff --git a/zellij-server/src/tab/mouse_handler.rs b/zellij-server/src/tab/mouse_handler.rs
index b14cbfca..97f4ce27 100644
--- a/zellij-server/src/tab/mouse_handler.rs
+++ b/zellij-server/src/tab/mouse_handler.rs
@@ -651,7 +651,13 @@ impl MouseHandler {
let err_context =
|| format!("failed to execute mouse action {action:?} for client {client_id}");
- if !matches!(action, MouseAction::UpdateHover { .. }) {
+ let preserves_help_text = matches!(&action, MouseAction::UpdateHover { .. })
+ || matches!(
+ &action,
+ MouseAction::SendToTerminal { event, .. }
+ if event.event_type == MouseEventType::Motion
+ );
+ if !preserves_help_text {
tab.mouse_help_text_visible.remove(&client_id);
}
@@ -1154,8 +1160,9 @@ impl MouseHandler {
if let Some(pane) = tab.get_pane_with_id_mut(pane_id) {
if !pane.terminal_emulator_wants_mouse() {
let relative = pane.relative_position(&event.position);
- pane.set_hover_position(Some(relative));
- should_render = true;
+ if pane.set_hover_position(Some(relative)) {
+ should_render = true;
+ }
}
}
}
diff --git a/zellij-server/src/terminal_bytes.rs b/zellij-server/src/terminal_bytes.rs
index 809163a5..0fcdc4ce 100644
--- a/zellij-server/src/terminal_bytes.rs
+++ b/zellij-server/src/terminal_bytes.rs
@@ -1,4 +1,8 @@
use crate::{os_input_output::AsyncReader, screen::ScreenInstruction, thread_bus::ThreadSenders};
+use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc,
+};
use std::time::{Duration, Instant};
use tokio::task;
use zellij_utils::{
@@ -11,6 +15,7 @@ pub(crate) struct TerminalBytes {
senders: ThreadSenders,
async_reader: Box<dyn AsyncReader>,
debug: bool,
+ activity_flag: Arc<AtomicBool>,
}
impl TerminalBytes {
@@ -19,12 +24,14 @@ impl TerminalBytes {
async_reader: Box<dyn AsyncReader>,
senders: ThreadSenders,
debug: bool,
+ activity_flag: Arc<AtomicBool>,
) -> Self {
TerminalBytes {
terminal_id,
senders,
debug,
async_reader,
+ activity_flag,
}
}
pub async fn listen(&mut self) -> Result<()> {
@@ -52,6 +59,7 @@ impl TerminalBytes {
break;
},
Ok(n_bytes) => {
+ self.activity_flag.store(true, Ordering::Relaxed);
let bytes = &buf[..n_bytes];
if self.debug {
let _ = debug_to_file(bytes, self.terminal_id as i32);
diff --git a/zellij-server/src/unit/pty_tests.rs b/zellij-server/src/unit/pty_tests.rs
new file mode 100644
index 00000000..1080b6ca
--- /dev/null
+++ b/zellij-server/src/unit/pty_tests.rs
@@ -0,0 +1,479 @@
+use super::*;
+use crate::os_input_output::ServerOsApi;
+use crate::plugins::PluginInstruction;
+use crate::thread_bus::Bus;
+use interprocess::local_socket::Stream as LocalSocketStream;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Mutex};
+use zellij_utils::channels::{self, SenderWithContext};
+use zellij_utils::data::{Event, Palette};
+use zellij_utils::errors::ErrorContext;
+use zellij_utils::input::command::RunCommand;
+use zellij_utils::ipc::{ClientToServerMsg, IpcReceiverWithContext, ServerToClientMsg};
+
+#[derive(Clone)]
+struct MockOsApi {
+ cwds: Arc<Mutex<HashMap<u32, PathBuf>>>,
+ cmds: Arc<Mutex<HashMap<u32, Vec<String>>>>,
+ cmds_by_ppid: Arc<Mutex<HashMap<String, Vec<String>>>>,
+}
+
+impl MockOsApi {
+ fn new() -> Self {
+ MockOsApi {
+ cwds: Arc::new(Mutex::new(HashMap::new())),
+ cmds: Arc::new(Mutex::new(HashMap::new())),
+ cmds_by_ppid: Arc::new(Mutex::new(HashMap::new())),
+ }
+ }
+ fn set_cwd(&self, pid: u32, path: PathBuf) {
+ self.cwds.lock().unwrap().insert(pid, path);
+ }
+ fn set_cmd(&self, pid: u32, cmd: Vec<String>) {
+ self.cmds.lock().unwrap().insert(pid, cmd);
+ }
+ fn set_foreground_cmd(&self, ppid: u32, cmd: Vec<String>) {
+ self.cmds_by_ppid
+ .lock()
+ .unwrap()
+ .insert(ppid.to_string(), cmd);
+ }
+ fn clear_foreground_cmd(&self, ppid: u32) {
+ self.cmds_by_ppid.lock().unwrap().remove(&ppid.to_string());
+ }
+}
+
+impl ServerOsApi for MockOsApi {
+ fn set_terminal_size_using_terminal_id(
+ &self,
+ _: u32,
+ _: u16,
+ _: u16,
+ _: Option<u16>,
+ _: Option<u16>,
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn spawn_terminal(
+ &self,
+ _: TerminalAction,
+ _: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
+ _: Option<PathBuf>,
+ ) -> anyhow::Result<(u32, Box<dyn AsyncReader>, Option<u32>)> {
+ unimplemented!()
+ }
+ fn write_to_tty_stdin(&self, _: u32, buf: &[u8]) -> anyhow::Result<usize> {
+ Ok(buf.len())
+ }
+ fn tcdrain(&self, _: u32) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn kill(&self, _: u32) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn force_kill(&self, _: u32) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn send_sigint(&self, _: u32) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn box_clone(&self) -> Box<dyn ServerOsApi> {
+ Box::new((*self).clone())
+ }
+ fn send_to_client(&self, _: ClientId, _: ServerToClientMsg) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn new_client(
+ &mut self,
+ _: ClientId,
+ _: LocalSocketStream,
+ ) -> anyhow::Result<IpcReceiverWithContext<ClientToServerMsg>> {
+ unimplemented!()
+ }
+ fn new_client_with_reply(
+ &mut self,
+ _: ClientId,
+ _: LocalSocketStream,
+ _: LocalSocketStream,
+ ) -> anyhow::Result<IpcReceiverWithContext<ClientToServerMsg>> {
+ unimplemented!()
+ }
+ fn remove_client(&mut self, _: ClientId) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn load_palette(&self) -> Palette {
+ Palette::default()
+ }
+ fn get_cwd(&self, pid: u32) -> Option<PathBuf> {
+ self.cwds.lock().unwrap().get(&pid).cloned()
+ }
+ fn get_cwds(&self, pids: Vec<u32>) -> (HashMap<u32, PathBuf>, HashMap<u32, Vec<String>>) {
+ let cwds_lock = self.cwds.lock().unwrap();
+ let cmds_lock = self.cmds.lock().unwrap();
+ let cwds = pids
+ .iter()
+ .filter_map(|pid| cwds_lock.get(pid).map(|cwd| (*pid, cwd.clone())))
+ .collect();
+ let cmds = pids
+ .iter()
+ .filter_map(|pid| cmds_lock.get(pid).map(|cmd| (*pid, cmd.clone())))
+ .collect();
+ (cwds, cmds)
+ }
+ fn get_all_cmds_by_ppid(&self, _: &Option<String>) -> HashMap<String, Vec<String>> {
+ self.cmds_by_ppid.lock().unwrap().clone()
+ }
+ fn write_to_file(&mut self, _: String, _: Option<String>) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn re_run_command_in_terminal(
+ &self,
+ _: u32,
+ _: RunCommand,
+ _: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
+ ) -> anyhow::Result<(Box<dyn AsyncReader>, Option<u32>)> {
+ unimplemented!()
+ }
+ fn clear_terminal_id(&self, _: u32) -> anyhow::Result<()> {
+ Ok(())
+ }
+}
+
+fn make_pty_with_plugin_receiver(
+ mock: MockOsApi,
+) -> (Pty, channels::Receiver<(PluginInstruction, ErrorContext)>) {
+ let (plugin_tx, plugin_rx) = channels::unbounded();
+ let plugin_sender = SenderWithContext::new(plugin_tx);
+ let mut bus: Bus<PtyInstruction> = Bus::empty().should_silently_fail();
+ bus.os_input = Some(Box::new(mock));
+ bus.senders.to_plugin = Some(plugin_sender);
+ let pty = Pty::new(bus, false, None, None);
+ (pty, plugin_rx)
+}
+
+fn set_active_terminal(pty: &mut Pty, terminal_id: u32, child_pid: u32) {
+ let flag = Arc::new(AtomicBool::new(true));
+ pty.id_to_child_pid.insert(terminal_id, child_pid);
+ pty.pane_activity_flags.insert(terminal_id, flag);
+}
+
+fn collect_cwd_changed_events(
+ rx: &channels::Receiver<(PluginInstruction, ErrorContext)>,
+) -> Vec<(PaneId, PathBuf)> {
+ let mut events = Vec::new();
+ while let Ok((instruction, _)) = rx.try_recv() {
+ if let PluginInstruction::Update(updates) = instruction {
+ for (_, _, event) in updates {
+ if let Event::CwdChanged(pane_id, cwd, _) = event {
+ events.push((pane_id.into(), cwd));
+ }
+ }
+ }
+ }
+ events
+}
+
+fn collect_command_changed_events(
+ rx: &channels::Receiver<(PluginInstruction, ErrorContext)>,
+) -> Vec<(PaneId, Vec<String>, bool)> {
+ let mut events = Vec::new();
+ while let Ok((instruction, _)) = rx.try_recv() {
+ if let PluginInstruction::Update(updates) = instruction {
+ for (_, _, event) in updates {
+ if let Event::CommandChanged(pane_id, cmd, is_foreground, _) = event {
+ events.push((pane_id.into(), cmd, is_foreground));
+ }
+ }
+ }
+ }
+ events
+}
+
+#[test]
+fn foreground_command_emitted_with_is_foreground_true() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_foreground_cmd(child_pid, vec!["vim".into(), "file.rs".into()]);
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+
+ pty.update_and_report_cwds();
+
+ let events = collect_command_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert_eq!(events[0].0, PaneId::Terminal(1));
+ assert_eq!(events[0].1, vec!["vim", "file.rs"]);
+ assert!(events[0].2, "expected is_foreground=true");
+}
+
+#[test]
+fn empty_foreground_falls_back_to_shell_command() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_cmd(child_pid, vec!["/bin/bash".into()]);
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+
+ pty.update_and_report_cwds();
+
+ let events = collect_command_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert_eq!(events[0].0, PaneId::Terminal(1));
+ assert_eq!(events[0].1, vec!["/bin/bash"]);
+ assert!(!events[0].2, "expected is_foreground=false");
+}
+
+#[test]
+fn foreground_clearing_emits_shell_fallback() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_cmd(child_pid, vec!["/bin/zsh".into()]);
+ mock.set_foreground_cmd(child_pid, vec!["cargo".into(), "build".into()]);
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock.clone());
+ set_active_terminal(&mut pty, 1, child_pid);
+
+ pty.update_and_report_cwds();
+ let events = collect_command_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert!(events[0].2, "first event should be foreground");
+ assert_eq!(events[0].1, vec!["cargo", "build"]);
+
+ mock.clear_foreground_cmd(child_pid);
+ pty.pane_activity_flags
+ .get(&1)
+ .unwrap()
+ .store(true, Ordering::Relaxed);
+
+ pty.update_and_report_cwds();
+ let events = collect_command_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert_eq!(events[0].1, vec!["/bin/zsh"]);
+ assert!(
+ !events[0].2,
+ "after clearing foreground, should fall back to shell"
+ );
+}
+
+#[test]
+fn no_event_when_foreground_unchanged() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_foreground_cmd(child_pid, vec!["htop".into()]);
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+
+ pty.update_and_report_cwds();
+ let _ = collect_command_changed_events(&rx);
+
+ pty.pane_activity_flags
+ .get(&1)
+ .unwrap()
+ .store(true, Ordering::Relaxed);
+ pty.update_and_report_cwds();
+ let events = collect_command_changed_events(&rx);
+ assert!(
+ events.is_empty(),
+ "no event expected when command unchanged"
+ );
+}
+
+#[test]
+fn no_event_for_inactive_terminal() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_foreground_cmd(child_pid, vec!["vim".into()]);
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+ pty.pane_activity_flags
+ .get(&1)
+ .unwrap()
+ .store(false, Ordering::Relaxed);
+
+ pty.update_and_report_cwds();
+ let events = collect_command_changed_events(&rx);
+ assert!(
+ events.is_empty(),
+ "inactive terminal should produce no events"
+ );
+}
+
+#[test]
+fn foreground_change_between_two_commands() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_foreground_cmd(child_pid, vec!["vim".into()]);
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock.clone());
+ set_active_terminal(&mut pty, 1, child_pid);
+
+ pty.update_and_report_cwds();
+ let events = collect_command_changed_events(&rx);
+ assert_eq!(events[0].1, vec!["vim"]);
+ assert!(events[0].2);
+
+ mock.set_foreground_cmd(child_pid, vec!["cargo".into(), "test".into()]);
+ pty.pane_activity_flags
+ .get(&1)
+ .unwrap()
+ .store(true, Ordering::Relaxed);
+
+ pty.update_and_report_cwds();
+ let events = collect_command_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert_eq!(events[0].1, vec!["cargo", "test"]);
+ assert!(events[0].2);
+}
+
+// --- Activity flag gating ---
+
+#[test]
+fn activity_flag_reset_after_poll() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ let (mut pty, _rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+ assert!(pty
+ .pane_activity_flags
+ .get(&1)
+ .unwrap()
+ .load(Ordering::Relaxed));
+
+ pty.update_and_report_cwds();
+
+ assert!(
+ !pty.pane_activity_flags
+ .get(&1)
+ .unwrap()
+ .load(Ordering::Relaxed),
+ "activity flag should be reset to false after poll"
+ );
+}
+
+#[test]
+fn multiple_terminals_only_active_ones_polled() {
+ let mock = MockOsApi::new();
+ let pid_active = 100;
+ let pid_inactive = 200;
+ mock.set_cwd(pid_active, PathBuf::from("/active"));
+ mock.set_cwd(pid_inactive, PathBuf::from("/inactive"));
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, pid_active);
+ set_active_terminal(&mut pty, 2, pid_inactive);
+ pty.pane_activity_flags
+ .get(&2)
+ .unwrap()
+ .store(false, Ordering::Relaxed);
+
+ pty.update_and_report_cwds();
+
+ let events = collect_cwd_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert_eq!(events[0].0, PaneId::Terminal(1));
+ assert_eq!(events[0].1, PathBuf::from("/active"));
+}
+
+// --- CWD change events ---
+
+#[test]
+fn cwd_changed_event_emitted_on_change() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_cwd(child_pid, PathBuf::from("/home/user"));
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+
+ pty.update_and_report_cwds();
+
+ let events = collect_cwd_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert_eq!(events[0].0, PaneId::Terminal(1));
+ assert_eq!(events[0].1, PathBuf::from("/home/user"));
+}
+
+#[test]
+fn no_cwd_event_when_unchanged() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_cwd(child_pid, PathBuf::from("/home/user"));
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+ pty.terminal_cwds.insert(1, PathBuf::from("/home/user"));
+
+ pty.update_and_report_cwds();
+
+ let events = collect_cwd_changed_events(&rx);
+ assert!(events.is_empty(), "no event expected when cwd unchanged");
+}
+
+// --- OSC7 CWD notification ---
+
+#[test]
+fn osc7_emits_cwd_changed() {
+ let mock = MockOsApi::new();
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ pty.id_to_child_pid.insert(1, 100);
+
+ pty.notify_cwd_from_osc7(1, PathBuf::from("/tmp/new"));
+
+ let events = collect_cwd_changed_events(&rx);
+ assert_eq!(events.len(), 1);
+ assert_eq!(events[0].0, PaneId::Terminal(1));
+ assert_eq!(events[0].1, PathBuf::from("/tmp/new"));
+ assert_eq!(
+ pty.terminal_cwds.get(&1),
+ Some(&PathBuf::from("/tmp/new")),
+ "cache should be updated"
+ );
+}
+
+#[test]
+fn osc7_no_event_when_unchanged() {
+ let mock = MockOsApi::new();
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ pty.id_to_child_pid.insert(1, 100);
+ pty.terminal_cwds.insert(1, PathBuf::from("/same"));
+
+ pty.notify_cwd_from_osc7(1, PathBuf::from("/same"));
+
+ let events = collect_cwd_changed_events(&rx);
+ assert!(events.is_empty(), "no event when osc7 path matches cache");
+}
+
+#[test]
+fn osc7_clears_activity_flag() {
+ let mock = MockOsApi::new();
+ let (mut pty, _rx) = make_pty_with_plugin_receiver(mock);
+ let flag = Arc::new(AtomicBool::new(true));
+ pty.id_to_child_pid.insert(1, 100);
+ pty.pane_activity_flags.insert(1, flag.clone());
+
+ pty.notify_cwd_from_osc7(1, PathBuf::from("/new"));
+
+ assert!(
+ !flag.load(Ordering::Relaxed),
+ "osc7 should clear the activity flag"
+ );
+}
+
+#[test]
+fn osc7_then_poll_skips_terminal() {
+ let mock = MockOsApi::new();
+ let child_pid = 100;
+ mock.set_cwd(child_pid, PathBuf::from("/from-proc"));
+ mock.set_foreground_cmd(child_pid, vec!["vim".into()]);
+ let (mut pty, rx) = make_pty_with_plugin_receiver(mock);
+ set_active_terminal(&mut pty, 1, child_pid);
+
+ pty.notify_cwd_from_osc7(1, PathBuf::from("/from-osc7"));
+ let osc7_events = collect_cwd_changed_events(&rx);
+ assert_eq!(osc7_events.len(), 1);
+
+ pty.update_and_report_cwds();
+ let cwd_events = collect_cwd_changed_events(&rx);
+ let cmd_events = collect_command_changed_events(&rx);
+ assert!(
+ cwd_events.is_empty() && cmd_events.is_empty(),
+ "poll after osc7 should skip terminal since flag was cleared"
+ );
+}
diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs
index 44f0d31a..27304131 100644
--- a/zellij-server/src/unit/screen_tests.rs
+++ b/zellij-server/src/unit/screen_tests.rs
@@ -152,9 +152,9 @@ fn send_cli_action_to_server(
capabilities,
client_attributes.clone(),
default_shell.clone(),
- default_layout.clone(),
+ &default_layout,
None,
- client_keybinds.clone(),
+ &client_keybinds,
default_mode,
None,
)
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index 5382dde4..92c37099 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -14,31 +14,32 @@ use zellij_utils::plugin_api::generated_api::api::plugin_command::{
};
use zellij_utils::plugin_api::plugin_command::{
dump_layout_response, dump_session_layout_response, get_focused_pane_info_response,
- get_pane_cwd_response, get_pane_running_command_response, parse_layout_response,
- CreateTokenResponse, ListTokensResponse, ProtobufBreakPanesToNewTabResponse,
- ProtobufBreakPanesToTabWithIdResponse, ProtobufBreakPanesToTabWithIndexResponse,
- ProtobufCurrentSessionLastSavedTimeResponse, ProtobufDeleteLayoutResponse,
- ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse,
- ProtobufFocusOrCreateTabResponse, ProtobufGenerateRandomNameResponse,
- ProtobufGetFocusedPaneInfoResponse, ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse,
- ProtobufGetPaneInfoResponse, ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse,
- ProtobufGetSessionEnvironmentVariablesResponse, ProtobufGetTabInfoResponse,
- ProtobufHideFloatingPanesResponse, ProtobufNewTabResponse, ProtobufNewTabsResponse,
- ProtobufOpenCommandPaneBackgroundResponse, ProtobufOpenCommandPaneFloatingNearPluginResponse,
- ProtobufOpenCommandPaneFloatingResponse, ProtobufOpenCommandPaneInPlaceOfPaneIdResponse,
- ProtobufOpenCommandPaneInPlaceOfPluginResponse, ProtobufOpenCommandPaneInPlaceResponse,
- ProtobufOpenCommandPaneNearPluginResponse, ProtobufOpenCommandPaneResponse,
- ProtobufOpenEditPaneInPlaceOfPaneIdResponse, ProtobufOpenFileFloatingNearPluginResponse,
- ProtobufOpenFileFloatingResponse, ProtobufOpenFileInPlaceOfPluginResponse,
- ProtobufOpenFileInPlaceResponse, ProtobufOpenFileNearPluginResponse, ProtobufOpenFileResponse,
- ProtobufOpenPaneInNewTabResponse, ProtobufOpenPluginPaneFloatingResponse,
- ProtobufOpenTerminalFloatingNearPluginResponse, ProtobufOpenTerminalFloatingResponse,
- ProtobufOpenTerminalInPlaceOfPluginResponse, ProtobufOpenTerminalInPlaceResponse,
- ProtobufOpenTerminalNearPluginResponse, ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse,
- ProtobufOpenTerminalResponse, ProtobufParseLayoutResponse, ProtobufPluginCommand,
- ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse,
- ProtobufShowFloatingPanesResponse, RenameWebTokenResponse, RevokeAllWebTokensResponse,
- RevokeTokenResponse,
+ get_pane_cwd_response, get_pane_running_command_response, get_session_list_response,
+ parse_layout_response, CreateTokenResponse, ListTokensResponse,
+ ProtobufBreakPanesToNewTabResponse, ProtobufBreakPanesToTabWithIdResponse,
+ ProtobufBreakPanesToTabWithIndexResponse, ProtobufCurrentSessionLastSavedTimeResponse,
+ ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse,
+ ProtobufEditLayoutResponse, ProtobufFocusOrCreateTabResponse,
+ ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
+ ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse, ProtobufGetPaneInfoResponse,
+ ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse,
+ ProtobufGetSessionEnvironmentVariablesResponse, ProtobufGetSessionListResponse,
+ ProtobufGetTabInfoResponse, ProtobufHideFloatingPanesResponse, ProtobufNewTabResponse,
+ ProtobufNewTabsResponse, ProtobufOpenCommandPaneBackgroundResponse,
+ ProtobufOpenCommandPaneFloatingNearPluginResponse, ProtobufOpenCommandPaneFloatingResponse,
+ ProtobufOpenCommandPaneInPlaceOfPaneIdResponse, ProtobufOpenCommandPaneInPlaceOfPluginResponse,
+ ProtobufOpenCommandPaneInPlaceResponse, ProtobufOpenCommandPaneNearPluginResponse,
+ ProtobufOpenCommandPaneResponse, ProtobufOpenEditPaneInPlaceOfPaneIdResponse,
+ ProtobufOpenFileFloatingNearPluginResponse, ProtobufOpenFileFloatingResponse,
+ ProtobufOpenFileInPlaceOfPluginResponse, ProtobufOpenFileInPlaceResponse,
+ ProtobufOpenFileNearPluginResponse, ProtobufOpenFileResponse, ProtobufOpenPaneInNewTabResponse,
+ ProtobufOpenPluginPaneFloatingResponse, ProtobufOpenTerminalFloatingNearPluginResponse,
+ ProtobufOpenTerminalFloatingResponse, ProtobufOpenTerminalInPlaceOfPluginResponse,
+ ProtobufOpenTerminalInPlaceResponse, ProtobufOpenTerminalNearPluginResponse,
+ ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse, ProtobufOpenTerminalResponse,
+ ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse,
+ ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse, ProtobufShowFloatingPanesResponse,
+ RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse,
};
use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion};
@@ -1902,6 +1903,43 @@ pub fn get_pane_running_command(pane_id: PaneId) -> Result<Vec<String>, String>
}
}
+/// Fetches a fresh snapshot of all live and resurrectable sessions on this machine.
+///
+/// # Permissions Required
+/// * `ReadApplicationState`
+pub fn get_session_list() -> Result<SessionListSnapshot, String> {
+ let plugin_command = PluginCommand::GetSessionList;
+ 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 =
+ ProtobufGetSessionListResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
+
+ match protobuf_response.result {
+ Some(get_session_list_response::Result::Snapshot(snapshot)) => {
+ let mut live_sessions = Vec::new();
+ for manifest in snapshot.live_sessions {
+ match SessionInfo::try_from(manifest) {
+ Ok(si) => live_sessions.push(si),
+ Err(e) => return Err(format!("Malformed session manifest: {}", e)),
+ }
+ }
+ let resurrectable_sessions = snapshot
+ .resurrectable_sessions
+ .into_iter()
+ .map(<(String, std::time::Duration)>::from)
+ .collect();
+ Ok(SessionListSnapshot {
+ live_sessions,
+ resurrectable_sessions,
+ })
+ },
+ Some(get_session_list_response::Result::Error(err)) => Err(err),
+ None => Err("Empty response from server".to_string()),
+ }
+}
+
/// Gets the current working directory for a specific pane by its ID.
///
/// This queries the operating system for the **current** working directory
diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs
index 30ace492..2ec490b0 100644
--- a/zellij-utils/assets/prost/api.event.rs
+++ b/zellij-utils/assets/prost/api.event.rs
@@ -9,7 +9,7 @@ pub struct EventNameList {
pub struct Event {
#[prost(enumeration="EventType", tag="1")]
pub name: i32,
- #[prost(oneof="event::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")]
+ #[prost(oneof="event::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")]
pub payload: ::core::option::Option<event::Payload>,
}
/// Nested message and enum types in `Event`.
@@ -91,6 +91,8 @@ pub mod event {
PaneRenderReportWithAnsiPayload(super::PaneRenderReportPayload),
#[prost(message, tag="38")]
InitialKeybindsPayload(super::InitialKeybindsPayload),
+ #[prost(message, tag="39")]
+ CommandChangedPayload(super::CommandChangedPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -105,6 +107,18 @@ pub struct CwdChangedPayload {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct CommandChangedPayload {
+ #[prost(message, optional, tag="1")]
+ pub pane_id: ::core::option::Option<PaneId>,
+ #[prost(string, repeated, tag="2")]
+ pub command: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+ #[prost(bool, tag="3")]
+ pub is_foreground: bool,
+ #[prost(uint32, repeated, tag="4")]
+ pub focused_client_ids: ::prost::alloc::vec::Vec<u32>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FailedToStartWebServerPayload {
#[prost(string, tag="1")]
pub error: ::prost::alloc::string::String,
@@ -815,6 +829,7 @@ pub enum EventType {
HighlightClicked = 42,
PaneRenderReportWithAnsi = 43,
InitialKeybinds = 44,
+ CommandChanged = 45,
}
impl EventType {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -867,6 +882,7 @@ impl EventType {
EventType::HighlightClicked => "HighlightClicked",
EventType::PaneRenderReportWithAnsi => "PaneRenderReportWithAnsi",
EventType::InitialKeybinds => "InitialKeybinds",
+ EventType::CommandChanged => "CommandChanged",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -916,6 +932,7 @@ impl EventType {
"HighlightClicked" => Some(Self::HighlightClicked),
"PaneRenderReportWithAnsi" => Some(Self::PaneRenderReportWithAnsi),
"InitialKeybinds" => Some(Self::InitialKeybinds),
+ "CommandChanged" => Some(Self::CommandChanged),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs
index 71a843dd..e4c5a03a 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, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160")]
+ #[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, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161")]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
/// Nested message and enum types in `PluginCommand`.
@@ -303,6 +303,8 @@ pub mod plugin_command {
ClearPaneHighlightsPayload(super::ClearPaneHighlightsPayload),
#[prost(message, tag="160")]
OpenPluginPaneFloatingPayload(super::OpenPluginPaneFloatingPayload),
+ #[prost(message, tag="161")]
+ GetSessionListPayload(super::GetSessionListPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -1899,6 +1901,35 @@ pub struct OpenPluginPaneFloatingResponse {
#[prost(message, optional, tag="1")]
pub pane_id: ::core::option::Option<PaneId>,
}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GetSessionListPayload {
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct SessionListSnapshot {
+ #[prost(message, repeated, tag="1")]
+ pub live_sessions: ::prost::alloc::vec::Vec<super::event::SessionManifest>,
+ #[prost(message, repeated, tag="2")]
+ pub resurrectable_sessions: ::prost::alloc::vec::Vec<super::event::ResurrectableSession>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GetSessionListResponse {
+ #[prost(oneof="get_session_list_response::Result", tags="1, 2")]
+ pub result: ::core::option::Option<get_session_list_response::Result>,
+}
+/// Nested message and enum types in `GetSessionListResponse`.
+pub mod get_session_list_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Result {
+ #[prost(message, tag="1")]
+ Snapshot(super::SessionListSnapshot),
+ #[prost(string, tag="2")]
+ Error(::prost::alloc::string::String),
+ }
+}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum CommandName {
@@ -2092,6 +2123,7 @@ pub enum CommandName {
ClearPaneHighlights = 208,
OpenPluginPaneFloating = 209,
ListWindowsVolumes = 210,
+ GetSessionList = 211,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -2290,6 +2322,7 @@ impl CommandName {
CommandName::ClearPaneHighlights => "ClearPaneHighlights",
CommandName::OpenPluginPaneFloating => "OpenPluginPaneFloating",
CommandName::ListWindowsVolumes => "ListWindowsVolumes",
+ CommandName::GetSessionList => "GetSessionList",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -2485,6 +2518,7 @@ impl CommandName {
"ClearPaneHighlights" => Some(Self::ClearPaneHighlights),
"OpenPluginPaneFloating" => Some(Self::OpenPluginPaneFloating),
"ListWindowsVolumes" => Some(Self::ListWindowsVolumes),
+ "GetSessionList" => Some(Self::GetSessionList),
_ => None,
}
}
diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs
index 75b9d4eb..32bb8153 100644
--- a/zellij-utils/src/consts.rs
+++ b/zellij-utils/src/consts.rs
@@ -51,6 +51,29 @@ pub fn create_config_and_cache_folders() {
if let Err(e) = std::fs::create_dir_all(&ZELLIJ_SESSION_INFO_CACHE_DIR.as_path()) {
log::error!("Failed to create session_info cache dir: {:?}", e);
}
+ prune_empty_session_info_folders();
+}
+
+fn prune_empty_session_info_folders() {
+ let Ok(entries) = std::fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) else {
+ return;
+ };
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+ let is_empty = std::fs::read_dir(&path)
+ .ok()
+ .map_or(false, |mut iter| iter.next().is_none());
+ if is_empty {
+ if let Err(e) = std::fs::remove_dir(&path) {
+ if e.kind() != std::io::ErrorKind::NotFound {
+ log::debug!("Failed to prune empty session folder {:?}: {:?}", path, e);
+ }
+ }
+ }
+ }
}
const fn system_default_data_dir() -> &'static str {
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 9e756fb1..cfcc3692 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -1012,6 +1012,7 @@ pub enum Event {
PaneRenderReportWithAnsi(HashMap<PaneId, PaneContents>),
ActionComplete(Action, Option<PaneId>, BTreeMap<String, String>), // Action, pane_id, context
CwdChanged(PaneId, PathBuf, Vec<ClientId>), // pane_id, cwd, focused_client_ids
+ CommandChanged(PaneId, Vec<String>, bool, Vec<ClientId>), // pane_id, command, is_foreground, focused_client_ids
AvailableLayoutInfo(Vec<LayoutInfo>, Vec<LayoutWithError>),
PluginConfigurationChanged(BTreeMap<String, String>),
HighlightClicked {
@@ -2580,6 +2581,18 @@ pub enum GetPaneRunningCommandResponse {
Err(String),
}
+#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
+pub struct SessionListSnapshot {
+ pub live_sessions: Vec<SessionInfo>,
+ pub resurrectable_sessions: Vec<(String, Duration)>,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub enum GetSessionListResponse {
+ Ok(SessionListSnapshot),
+ Err(String),
+}
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GetPaneCwdResponse {
Ok(PathBuf),
@@ -3566,6 +3579,7 @@ pub enum PluginCommand {
context: BTreeMap<String, String>,
},
ListWindowsVolumes,
+ GetSessionList,
}
// Response type for plugin API methods that open a pane in a new tab
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index d44a348a..5bb8e955 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -487,6 +487,7 @@ pub enum PtyContext {
GetPaneRunningCommand,
GetPaneCwd,
UpdateAndReportCwds,
+ NotifyCwdFromOsc7,
Exit,
}
@@ -614,7 +615,6 @@ pub enum BackgroundJobContext {
DisplayPaneError,
AnimatePluginLoading,
StopPluginLoadingAnimation,
- ReadAllSessionInfosOnMachine,
ReportSessionInfo,
ReportLayoutInfo,
RunCommand,
diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto
index ce9eb8ff..3c3a8490 100644
--- a/zellij-utils/src/plugin_api/event.proto
+++ b/zellij-utils/src/plugin_api/event.proto
@@ -67,6 +67,7 @@ enum EventType {
HighlightClicked = 42;
PaneRenderReportWithAnsi = 43;
InitialKeybinds = 44;
+ CommandChanged = 45;
}
message EventNameList {
@@ -113,6 +114,7 @@ message Event {
HighlightClickedPayload highlight_clicked_payload = 36;
PaneRenderReportPayload pane_render_report_with_ansi_payload = 37;
InitialKeybindsPayload initial_keybinds_payload = 38;
+ CommandChangedPayload command_changed_payload = 39;
}
}
@@ -122,6 +124,13 @@ message CwdChangedPayload {
repeated uint32 focused_client_ids = 3;
}
+message CommandChangedPayload {
+ PaneId pane_id = 1;
+ repeated string command = 2;
+ bool is_foreground = 3;
+ repeated uint32 focused_client_ids = 4;
+}
+
message FailedToStartWebServerPayload {
string error = 1;
}
diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs
index 93b28365..6f138742 100644
--- a/zellij-utils/src/plugin_api/event.rs
+++ b/zellij-utils/src/plugin_api/event.rs
@@ -6,7 +6,8 @@ pub use super::generated_api::api::{
pane_scrollback_response, ActionCompletePayload as ProtobufActionCompletePayload,
AvailableLayoutInfoPayload as ProtobufAvailableLayoutInfoPayload,
ClientInfo as ProtobufClientInfo, ClientPaneHistory as ProtobufClientPaneHistory,
- ClientTabHistory as ProtobufClientTabHistory, ContextItem as ProtobufContextItem,
+ ClientTabHistory as ProtobufClientTabHistory,
+ CommandChangedPayload as ProtobufCommandChangedPayload, ContextItem as ProtobufContextItem,
CopyDestination as ProtobufCopyDestination, CwdChangedPayload as ProtobufCwdChangedPayload,
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata,
@@ -474,6 +475,27 @@ impl TryFrom<ProtobufEvent> for Event {
},
_ => Err("Malformed payload for the CwdChanged Event"),
},
+ Some(ProtobufEventType::CommandChanged) => match protobuf_event.payload {
+ Some(ProtobufEventPayload::CommandChangedPayload(p)) => {
+ let pane_id: PaneId = p
+ .pane_id
+ .ok_or("Missing pane_id in CommandChanged payload")?
+ .try_into()
+ .map_err(|_| "Failed to convert PaneId in CommandChanged payload")?;
+ let focused_client_ids: Vec<ClientId> = p
+ .focused_client_ids
+ .into_iter()
+ .map(|id| id as u16)
+ .collect();
+ Ok(Event::CommandChanged(
+ pane_id,
+ p.command,
+ p.is_foreground,
+ focused_client_ids,
+ ))
+ },
+ _ => Err("Malformed payload for the CommandChanged Event"),
+ },
Some(ProtobufEventType::AvailableLayoutInfo) => match protobuf_event.payload {
Some(ProtobufEventPayload::AvailableLayoutInfoPayload(
available_layout_info_payload,
@@ -1014,6 +1036,21 @@ impl TryFrom<Event> for ProtobufEvent {
payload: Some(event::Payload::CwdChangedPayload(cwd_changed_payload)),
})
},
+ Event::CommandChanged(pane_id, command, is_foreground, focused_client_ids) => {
+ let protobuf_pane_id: ProtobufPaneId = pane_id.try_into()?;
+ let focused_client_ids_u32: Vec<u32> =
+ focused_client_ids.into_iter().map(|id| id as u32).collect();
+ let payload = ProtobufCommandChangedPayload {
+ pane_id: Some(protobuf_pane_id),
+ command,
+ is_foreground,
+ focused_client_ids: focused_client_ids_u32,
+ };
+ Ok(ProtobufEvent {
+ name: ProtobufEventType::CommandChanged as i32,
+ payload: Some(event::Payload::CommandChangedPayload(payload)),
+ })
+ },
Event::AvailableLayoutInfo(available_layouts, layouts_with_errors) => {
let mut protobuf_available_layouts = vec![];
let mut protobuf_layouts_with_errors = vec![];
@@ -2015,6 +2052,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::UserAction => EventType::UserAction,
ProtobufEventType::ActionComplete => EventType::ActionComplete,
ProtobufEventType::CwdChanged => EventType::CwdChanged,
+ ProtobufEventType::CommandChanged => EventType::CommandChanged,
ProtobufEventType::AvailableLayoutInfo => EventType::AvailableLayoutInfo,
ProtobufEventType::PluginConfigurationChanged => EventType::PluginConfigurationChanged,
ProtobufEventType::HighlightClicked => EventType::HighlightClicked,
@@ -2067,6 +2105,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::UserAction => ProtobufEventType::UserAction,
EventType::ActionComplete => ProtobufEventType::ActionComplete,
EventType::CwdChanged => ProtobufEventType::CwdChanged,
+ EventType::CommandChanged => ProtobufEventType::CommandChanged,
EventType::AvailableLayoutInfo => ProtobufEventType::AvailableLayoutInfo,
EventType::PluginConfigurationChanged => ProtobufEventType::PluginConfigurationChanged,
EventType::HighlightClicked => ProtobufEventType::HighlightClicked,
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index 63baa443..0b768838 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -203,6 +203,7 @@ enum CommandName {
ClearPaneHighlights = 208;
OpenPluginPaneFloating = 209;
ListWindowsVolumes = 210;
+ GetSessionList = 211;
}
message PluginCommand {
@@ -354,6 +355,7 @@ message PluginCommand {
SetPaneRegexHighlightsPayload set_pane_regex_highlights_payload = 158;
ClearPaneHighlightsPayload clear_pane_highlights_payload = 159;
OpenPluginPaneFloatingPayload open_plugin_pane_floating_payload = 160;
+ GetSessionListPayload get_session_list_payload = 161;
}
}
@@ -1295,3 +1297,17 @@ message OpenPluginPaneFloatingPayload {
message OpenPluginPaneFloatingResponse {
optional PaneId pane_id = 1;
}
+
+message GetSessionListPayload {}
+
+message SessionListSnapshot {
+ repeated event.SessionManifest live_sessions = 1;
+ repeated event.ResurrectableSession resurrectable_sessions = 2;
+}
+
+message GetSessionListResponse {
+ oneof result {
+ SessionListSnapshot snapshot = 1;
+ string error = 2;
+ }
+}
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index a457f720..5bfb2b42 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -1,13 +1,17 @@
pub use super::generated_api::api::{
action::{Action as ProtobufAction, PaneIdAndShouldFloat, SwitchToModePayload},
- event::{EventNameList as ProtobufEventNameList, Header},
+ event::{
+ EventNameList as ProtobufEventNameList, Header,
+ ResurrectableSession as ProtobufResurrectableSession,
+ SessionManifest as ProtobufSessionManifest,
+ },
input_mode::InputMode as ProtobufInputMode,
plugin_command::{
break_panes_to_new_tab_response, break_panes_to_tab_with_id_response,
break_panes_to_tab_with_index_response, delete_layout_response, dump_layout_response,
dump_session_layout_response, edit_layout_response, focus_or_create_tab_response,
get_focused_pane_info_response, get_pane_cwd_response, get_pane_pid_response,
- get_pane_running_command_response, hide_floating_panes_response,
+ get_pane_running_command_response, get_session_list_response, hide_floating_panes_response,
highlight_style::Style as ProtobufHighlightStyleVariant, new_tab_response,
parse_layout_response, plugin_command::Payload, rename_layout_response,
save_layout_response, save_session_response, show_floating_panes_response,
@@ -48,7 +52,9 @@ pub use super::generated_api::api::{
GetPaneScrollbackPayload,
GetSessionEnvironmentVariablesPayload as ProtobufGetSessionEnvironmentVariablesPayload,
GetSessionEnvironmentVariablesResponse as ProtobufGetSessionEnvironmentVariablesResponse,
- GetTabInfoPayload, GetTabInfoResponse as ProtobufGetTabInfoResponse, GoToTabWithIdPayload,
+ GetSessionListPayload as ProtobufGetSessionListPayload,
+ GetSessionListResponse as ProtobufGetSessionListResponse, GetTabInfoPayload,
+ GetTabInfoResponse as ProtobufGetTabInfoResponse, GoToTabWithIdPayload,
GroupAndUngroupPanesPayload, HideFloatingPanesPayload as ProtobufHideFloatingPanesPayload,
HideFloatingPanesResponse as ProtobufHideFloatingPanesResponse, HidePaneWithIdPayload,
HighlightAndUnhighlightPanesPayload, HighlightLayer as ProtobufHighlightLayer,
@@ -108,9 +114,10 @@ pub use super::generated_api::api::{
SaveLayoutPayload, SaveLayoutResponse as ProtobufSaveLayoutResponse, SaveSessionPayload,
SaveSessionResponse as ProtobufSaveSessionResponse, ScrollDownInPaneIdPayload,
ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload,
- SetFloatingPanePinnedPayload, SetPaneBorderlessPayload, SetPaneColorPayload,
- SetPaneRegexHighlightsPayload, SetSelfMouseSelectionSupportPayload, SetTimeoutPayload,
- ShowCursorPayload, ShowFloatingPanesPayload as ProtobufShowFloatingPanesPayload,
+ SessionListSnapshot as ProtobufSessionListSnapshot, SetFloatingPanePinnedPayload,
+ SetPaneBorderlessPayload, SetPaneColorPayload, SetPaneRegexHighlightsPayload,
+ SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowCursorPayload,
+ ShowFloatingPanesPayload as ProtobufShowFloatingPanesPayload,
ShowFloatingPanesResponse as ProtobufShowFloatingPanesResponse, ShowPaneWithIdPayload,
StackPanesPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToIdPayload,
SwitchTabToPayload, TogglePaneBorderlessPayload, TogglePaneEmbedOrEjectForPaneIdPayload,
@@ -124,9 +131,10 @@ pub use super::generated_api::api::{
use crate::data::{
ConnectToSession, DeleteLayoutResponse, EditLayoutResponse, FloatingPaneCoordinates,
GetFocusedPaneInfoResponse, GetPaneCwdResponse, GetPanePidResponse,
- GetPaneRunningCommandResponse, HighlightLayer, HighlightStyle, HttpVerb, InputMode,
- KeyWithModifier, MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand,
- RegexHighlight, RenameLayoutResponse, SaveLayoutResponse,
+ GetPaneRunningCommandResponse, GetSessionListResponse, HighlightLayer, HighlightStyle,
+ HttpVerb, InputMode, KeyWithModifier, MessageToPlugin, NewPluginArgs, PaneId, PermissionType,
+ PluginCommand, RegexHighlight, RenameLayoutResponse, SaveLayoutResponse, SessionInfo,
+ SessionListSnapshot,
};
use crate::input::actions::Action;
use crate::input::layout::PercentOrFixed;
@@ -329,6 +337,63 @@ impl From<GetPaneRunningCommandResponse> for ProtobufGetPaneRunningCommandRespon
}
}
+impl From<GetSessionListResponse> for ProtobufGetSessionListResponse {
+ fn from(response: GetSessionListResponse) -> Self {
+ match response {
+ GetSessionListResponse::Ok(snapshot) => {
+ let live_sessions = snapshot
+ .live_sessions
+ .into_iter()
+ .filter_map(|si| ProtobufSessionManifest::try_from(si).ok())
+ .collect();
+ let resurrectable_sessions = snapshot
+ .resurrectable_sessions
+ .into_iter()
+ .map(|entry| ProtobufResurrectableSession::from(entry))
+ .collect();
+ ProtobufGetSessionListResponse {
+ result: Some(get_session_list_response::Result::Snapshot(
+ ProtobufSessionListSnapshot {
+ live_sessions,
+ resurrectable_sessions,
+ },
+ )),
+ }
+ },
+ GetSessionListResponse::Err(err) => ProtobufGetSessionListResponse {
+ result: Some(get_session_list_response::Result::Error(err)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufGetSessionListResponse> for GetSessionListResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufGetSessionListResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(get_session_list_response::Result::Snapshot(snapshot)) => {
+ let mut live_sessions: Vec<SessionInfo> = Vec::new();
+ for manifest in snapshot.live_sessions {
+ live_sessions.push(SessionInfo::try_from(manifest)?);
+ }
+ let resurrectable_sessions = snapshot
+ .resurrectable_sessions
+ .into_iter()
+ .map(<(String, std::time::Duration)>::from)
+ .collect();
+ Ok(GetSessionListResponse::Ok(SessionListSnapshot {
+ live_sessions,
+ resurrectable_sessions,
+ }))
+ },
+ Some(get_session_list_response::Result::Error(err)) => {
+ Ok(GetSessionListResponse::Err(err))
+ },
+ None => Err("Empty GetSessionListResponse"),
+ }
+ }
+}
+
impl From<GetPaneCwdResponse> for ProtobufGetPaneCwdResponse {
fn from(response: GetPaneCwdResponse) -> Self {
match response {
@@ -1300,6 +1365,11 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
Some(_) => Err("ListWindowsVolumes should have no payload"),
None => Ok(PluginCommand::ListWindowsVolumes),
},
+ Some(CommandName::GetSessionList) => match protobuf_plugin_command.payload {
+ Some(Payload::GetSessionListPayload(_)) => Ok(PluginCommand::GetSessionList),
+ None => Ok(PluginCommand::GetSessionList),
+ _ => Err("Mismatched payload for GetSessionList"),
+ },
Some(CommandName::DumpSessionLayout) => match protobuf_plugin_command.payload {
Some(Payload::DumpSessionLayoutPayload(payload)) => {
Ok(PluginCommand::DumpSessionLayout {
@@ -3145,6 +3215,12 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
name: CommandName::ListWindowsVolumes as i32,
payload: None,
}),
+ PluginCommand::GetSessionList => Ok(ProtobufPluginCommand {
+ name: CommandName::GetSessionList as i32,
+ payload: Some(Payload::GetSessionListPayload(
+ ProtobufGetSessionListPayload {},
+ )),
+ }),
PluginCommand::DumpSessionLayout { tab_index } => Ok(ProtobufPluginCommand {
name: CommandName::DumpSessionLayout as i32,
payload: tab_index.map(|idx| {