mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-12 02:27:24 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¤t_session_name);
|
||||
for (session_name, session_info) in session_infos_on_machine.iter_mut()
|
||||
{
|
||||
if session_name == ¤t_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(¤t_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(¤t_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(¤t_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(¤t_session_name);
|
||||
let _wrote_layout_file = std::fs::create_dir_all(
|
||||
session_info_folder_for_session(¤t_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(¤t_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(¤t_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(¤t_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, ¤t_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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -2151,6 +2151,7 @@ fn check_event_permission(
|
||||
| Event::FailedToWriteConfigToDisk(..)
|
||||
| Event::CommandPaneReRun(..)
|
||||
| Event::CwdChanged(..)
|
||||
| Event::CommandChanged(..)
|
||||
| Event::AvailableLayoutInfo(..)
|
||||
| Event::PluginConfigurationChanged(..)
|
||||
| Event::HighlightClicked { .. }
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user