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
This commit is contained in:
Aram Drevekenin
2026-04-17 14:08:48 +02:00
committed by GitHub
parent 0d8ebaa234
commit aa29cbaf66
29 changed files with 1548 additions and 271 deletions
+1
View File
@@ -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)
+67 -4
View File
@@ -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()
@@ -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();
+15 -3
View File
@@ -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 {
-2
View File
@@ -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,
+253 -109
View File
@@ -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(())
});
}
}
}
fn read_other_live_session_states(current_session_name: &str) -> BTreeMap<String, SessionInfo> {
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,
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));
}
}
}
+5 -6
View File
@@ -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);
+89 -2
View File
@@ -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);
}
}
+10 -3
View File
@@ -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()
+1
View File
@@ -2151,6 +2151,7 @@ fn check_event_permission(
| Event::FailedToWriteConfigToDisk(..)
| Event::CommandPaneReRun(..)
| Event::CwdChanged(..)
| Event::CommandChanged(..)
| Event::AvailableLayoutInfo(..)
| Event::PluginConfigurationChanged(..)
| Event::HighlightClicked { .. }
+50 -10
View File
@@ -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
},
+163 -21
View File
@@ -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;
+34 -45
View File
@@ -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,
+44 -21
View File
@@ -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);
+12 -1
View File
@@ -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(())
}
+10 -3
View File
@@ -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;
}
}
}
}
+8
View File
@@ -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);
+479
View File
@@ -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"
);
}
+2 -2
View File
@@ -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,
)
+63 -25
View File
@@ -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
+18 -1
View File
@@ -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,
}
}
@@ -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,
}
}
+23
View File
@@ -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 {
+14
View File
@@ -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
+1 -1
View File
@@ -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,
+9
View File
@@ -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;
}
+40 -1
View File
@@ -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,
@@ -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;
}
}
+85 -9
View File
@@ -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| {