diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-12-11 14:25:42 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-11 14:25:42 +0100 |
| commit | 693fa082def5ef0fe030cd7fdc3b1df051b35dc6 (patch) | |
| tree | b3f651462bbc1cd2fa5b2d79207343413ef6b83c /zellij-server | |
| parent | fix(zellij-server): unblock building on 32bit targets (#4484) (diff) | |
| download | sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.gz sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.xz sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.zip | |
Command Sequences, Blocking CLI Commands, New Plugin APIs (#4546)
* fix deadlocks
* initial logging to plugin implementation
* only log actions, include keys as Action::Write
* send actions to plugins as a new Event::UserAction
* add Action::Write metadata to plugin API
* run_action plugin command and permission
* block-until-exit/success/failure in the cli
* fix tests
* add NewBlockingPane to the plugin API
* run actions in a different thread so as not to block plugin execution
* add custom context to run_action flow
* fixes for floating pane z_index sorting and unknown protobuf events not crashing plugins
* always render the frame of pinned panes
* macros module
* add macros to config
* KDL macro parsing
* thread macros to the plugin thread
* get/update/delete macros from plugins
* add macros updated event
* add ability to rename macros
* fix deadlock
* fix z_indices with pinned unselectable panes bug
* allow plugins to opt-in to show_cursor
* change show_cursor(bool) to show_cursor(Option<coordinates>)
* test
* upgrade strum to allow proper enum iteration and case insensitive fromstr implementation for Actions
* better cwd tracking for exited command panes
* add initial panes to NewTab Action (no-op for now)
* add initial panes to cli newtab action
* make initial panes work
* add close_on_exit and start_suspended to the cli
* properly convert newtab action (potentially breaking for plugins)
* allow first pane in tab to block
* make distinction between opening panes with the last active client and the current pane in the cli
* improve some apis
* fix fixed size closing pane bug
* fix plugin cursor coordinates
* fix issues with launch-or-focus-plugin having to do with in-place and suppressed panes
* fix: set affected pane in replace pane
* allow setting a pane in place when it is suppressed and being focused (tests failing)
* fix tests
* some fixes for unsuppression of panes
* some fixes for completion_tx in new tab action
* allow replace/suppressed rather than replace/close only
* fixes for inserting suppressed panes without conflicts
* pane focus history and explicit unsuppress plugin api
* fix: prefer explicitly provided pane id in placement in actions that provide it
* fix floating pane coordinate change and z_index
* fix plugin cache dir to persist between sessions
* fix some stack focusing issues when adding panes without a client id
* make sure pinned panes are always above non-pinned floating panes
* allow plugins to copy to clipboard
* initial text styling additions implementations - buggy
* fix dimming issues
* remove extras, stay with just dim and unbold
* unbold and dim as indices
* allow ui components to render success/error colors
* allow plugins to send sigint/sigkill to processes, as well as receive their pid
* ui component fixes and send originating plugin context with run_action
* originating plugin for new tab too
* make plugn show pane command also expand stacked panes
* add sequence plugin
* remove macros
* remove snapshots
* add shortcut for sequence plugin
* fix plugin tests
* plugin tests for new apis
* update snapshots for sequence shortcut
* cleanups and formatting
* Event::CwdChanged
* moar cleanups
* remove some warnings
* remove mroe warnings
* remove dead code
* remove more dead code
* fix minor bugs in sequence plugin
* update sequence help and format codebase
* fix conditional blocking cli commands
* fix initial-command new-tab cli flag
* add client_ids to CwdChanged Event
* last fixes
* add pr
Diffstat (limited to 'zellij-server')
110 files changed, 2589 insertions, 603 deletions
diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index 9bc8a938..62907048 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -24,6 +24,7 @@ use std::time::{Duration, Instant}; use crate::panes::PaneId; use crate::plugins::{PluginId, PluginInstruction}; +use crate::pty::PtyInstruction; use crate::screen::ScreenInstruction; use crate::thread_bus::Bus; use crate::ClientId; @@ -228,6 +229,7 @@ pub(crate) fn background_jobs_main( session_infos_on_machine, resurrectable_sessions, )); + let _ = senders.send_to_pty(PtyInstruction::UpdateAndReportCwds); if last_serialization_time .lock() .unwrap() diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 1861fb68..dd093919 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -784,6 +784,8 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { floating_panes_layout, tab_name, swap_layouts, + None, // initial_panes + false, // block_on_first_terminal should_focus_tab, (client_id, is_web_client), None, diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index adcca974..46010493 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -500,6 +500,8 @@ pub trait ServerOsApi: Send + Sync { fn kill(&self, pid: Pid) -> Result<()>; /// Terminate the process with process ID `pid`. (SIGKILL) fn force_kill(&self, pid: Pid) -> Result<()>; + /// Send SIGINT to the process with process ID `pid` + fn send_sigint(&self, pid: Pid) -> Result<()>; /// Returns a [`Box`] pointer to this [`ServerOsApi`] struct. fn box_clone(&self) -> Box<dyn ServerOsApi>; fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()>; @@ -697,6 +699,10 @@ impl ServerOsApi for ServerOsInputOutput { let _ = kill(pid, Some(Signal::SIGKILL)); Ok(()) } + fn send_sigint(&self, pid: Pid) -> Result<()> { + let _ = kill(pid, Some(Signal::SIGINT)); + Ok(()) + } fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()> { let err_context = || format!("failed to send message to client {client_id}"); diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 6ae71b43..d347dcd0 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -18,6 +18,7 @@ use crate::{ ClientId, }; use std::cell::RefCell; +use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap, HashSet}; use std::rc::Rc; use std::time::Instant; @@ -129,6 +130,7 @@ impl FloatingPanes { .insert(pane_id, pane.position_and_size()); self.panes.insert(pane_id, pane); self.z_indices.push(pane_id); + self.make_sure_pinned_panes_are_on_top(); } pub fn replace_active_pane( &mut self, @@ -169,6 +171,7 @@ impl FloatingPanes { .with_context(err_context)?; self.z_indices.remove(z_index); self.z_indices.insert(z_index, with_pane_id); + self.make_sure_pinned_panes_are_on_top(); Ok(removed_pane) }); @@ -415,21 +418,10 @@ impl FloatingPanes { vec![] }; floating_panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| { - self.z_indices - .iter() - .position(|id| id == *a_id) - .with_context(err_context) - .fatal() - .cmp( - &self - .z_indices - .iter() - .position(|id| id == *b_id) - .with_context(err_context) - .fatal(), - ) + let a_pos = self.z_indices.iter().position(|id| id == *a_id); + let b_pos = self.z_indices.iter().position(|id| id == *b_id); + a_pos.cmp(&b_pos) }); - for (z_index, (kind, pane)) in floating_panes.iter_mut().enumerate() { let mut active_panes = active_panes.clone(); let multiple_users_exist_in_session = @@ -806,6 +798,16 @@ impl FloatingPanes { pane_geom.adjust_coordinates(new_coordinates, viewport); pane.set_geom(pane_geom); pane.set_should_render(true); + + // we do this in case this moves the pane under another pane so that the pane user's + // are focused on will always be on top + let is_focused = self.active_panes.pane_id_is_focused(&pane_id); + if is_focused { + self.z_indices.retain(|p_id| *p_id != pane_id); + self.z_indices.push(pane_id); + self.make_sure_pinned_panes_are_on_top(); + } + self.desired_pane_positions.insert(pane_id, pane_geom); } let _ = self.set_pane_frames(); @@ -840,7 +842,7 @@ impl FloatingPanes { self.focus_pane(next_active_pane_id, client_id); }, None => { - self.defocus_pane(pane_id, client_id); + self.defocus_pane(client_id); }, } } @@ -855,6 +857,7 @@ impl FloatingPanes { } self.z_indices.retain(|p_id| *p_id != pane_id); self.z_indices.push(pane_id); + self.make_sure_pinned_panes_are_on_top(); self.set_pane_active_at(pane_id); self.set_force_render(); } @@ -888,8 +891,7 @@ impl FloatingPanes { }, } } - pub fn defocus_pane(&mut self, pane_id: PaneId, client_id: ClientId) { - self.z_indices.retain(|p_id| *p_id != pane_id); + pub fn defocus_pane(&mut self, client_id: ClientId) { self.active_panes.remove(&client_id, &mut self.panes); self.set_force_render(); } @@ -1244,4 +1246,27 @@ impl FloatingPanes { ); floating_pane_grid.next_selectable_pane_id_to_the_right(&pane_id) } + fn make_sure_pinned_panes_are_on_top(&mut self) { + let pinned_status: std::collections::HashMap<_, _> = self + .z_indices + .iter() + .map(|id| (id.clone(), self.pane_id_is_pinned(id))) + .collect(); + + self.z_indices.sort_by(|a_id, b_id| { + let a_is_pinned = pinned_status.get(a_id).copied().unwrap_or(false); + let b_is_pinned = pinned_status.get(b_id).copied().unwrap_or(false); + match (a_is_pinned, b_is_pinned) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal, + } + }); + } + fn pane_id_is_pinned(&self, pane_id: &PaneId) -> bool { + self.panes + .get(pane_id) + .map(|p| p.position_and_size().is_pinned) + .unwrap_or(false) + } } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 0e74a9de..ffad3d84 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -90,6 +90,7 @@ pub(crate) struct PluginPane { character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, vte_parsers: HashMap<ClientId, vte::Parser>, grids: HashMap<ClientId, Grid>, + cursor_visibility: HashMap<ClientId, Option<(usize, usize)>>, prev_pane_name: String, frame: HashMap<ClientId, PaneFrame>, borderless: bool, @@ -149,6 +150,7 @@ impl PluginPane { sixel_image_store, vte_parsers: HashMap::new(), grids: HashMap::new(), + cursor_visibility: HashMap::new(), style, pane_frame_color_override: None, invoked_with, @@ -248,8 +250,25 @@ impl Pane for PluginPane { self.should_render.insert(client_id, true); } - fn cursor_coordinates(&self) -> Option<(usize, usize)> { - None + fn cursor_coordinates(&self, client_id: Option<ClientId>) -> Option<(usize, usize)> { + let own_content_columns = self.get_content_columns(); + let own_content_rows = self.get_content_rows(); + let Offset { top, left, .. } = self.content_offset; + if let Some(coordinates) = + client_id.and_then(|client_id| self.cursor_visibility.get(&client_id)) + { + coordinates + .map(|(x, y)| (x + left, y + top)) + .and_then(|(x, y)| { + if x >= own_content_columns || y >= own_content_rows { + None + } else { + Some((x, y)) // these are 0 indexed + } + }) + } else { + None + } } fn adjust_input_to_terminal( &mut self, @@ -360,6 +379,10 @@ impl Pane for PluginPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } + fn show_cursor(&mut self, client_id: ClientId, cursor_position: Option<(usize, usize)>) { + self.cursor_visibility.insert(client_id, cursor_position); + self.should_render.insert(client_id, true); + } fn request_permissions_from_user(&mut self, permissions: Option<PluginPermission>) { self.requesting_permissions = permissions; self.handle_plugin_bytes_for_all_clients(Default::default()); // to trigger the render of @@ -440,7 +463,7 @@ impl Pane for PluginPane { let res = match self.frame.get(&client_id) { // TODO: use and_then or something? Some(last_frame) => { - if &frame != last_frame { + if &frame != last_frame || is_pinned { if !self.borderless { let frame_output = frame.render().with_context(err_context)?; self.frame.insert(client_id, frame); diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 1c5667a5..97bf5dcc 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -197,7 +197,7 @@ impl Pane for TerminalPane { self.vte_parser.advance(&mut self.grid, byte); } } - fn cursor_coordinates(&self) -> Option<(usize, usize)> { + fn cursor_coordinates(&self, _client_id: Option<ClientId>) -> Option<(usize, usize)> { // (x, y) if self.get_content_rows() < 1 || self.get_content_columns() < 1 { // do not render cursor if there's no room for it @@ -740,6 +740,14 @@ impl Pane for TerminalPane { if let Some(notification_end) = self.notification_end.as_mut() { if let Some(exit_status) = exit_status { notification_end.set_exit_status(exit_status); + + // Check if unblock condition is met + if let Some(condition) = notification_end.unblock_condition() { + if condition.is_met(exit_status) { + // Condition met - drop the NotificationEnd now to unblock + drop(self.notification_end.take()); + } + } } } if is_first_run { @@ -899,6 +907,13 @@ impl Pane for TerminalPane { fn update_exit_status(&mut self, exit_status: i32) { if let Some(notification_end) = self.notification_end.as_mut() { notification_end.set_exit_status(exit_status); + // Check if unblock condition is met + if let Some(condition) = notification_end.unblock_condition() { + if condition.is_met(exit_status) { + // Condition met - drop the NotificationEnd now to unblock + drop(self.notification_end.take()); + } + } } } } @@ -922,7 +937,7 @@ impl TerminalPane { arrow_fonts: bool, styled_underlines: bool, explicitly_disable_keyboard_protocol: bool, - notification_end: Option<NotificationEnd>, + mut notification_end: Option<NotificationEnd>, ) -> TerminalPane { let initial_pane_title = initial_pane_title.unwrap_or_else(|| format!("Pane #{}", pane_index)); @@ -940,6 +955,9 @@ impl TerminalPane { styled_underlines, explicitly_disable_keyboard_protocol, ); + if let Some(notification_end) = notification_end.as_mut() { + notification_end.set_affected_pane_id(PaneId::Terminal(pid)); + } TerminalPane { frame: HashMap::new(), content_offset: Offset::default(), diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index fb73a90d..4df68db4 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -313,6 +313,7 @@ impl TiledPanes { Ok(new_pane_geom) => { pane.set_geom(new_pane_geom); self.panes.insert(pane_id, pane); // TODO: is set_geom the right one? + self.expand_pane_in_stack(pane_id); }, Err(e) => { log::error!("Failed to add pane to stack: {:?}", e); @@ -349,7 +350,8 @@ impl TiledPanes { Ok(new_pane_geom) => { pane.set_geom(new_pane_geom); self.panes.insert(pane_id, pane); - self.set_force_render(); // TODO: why do we need this? + + self.expand_pane_in_stack(pane_id); return; }, Err(_e) => { @@ -447,7 +449,9 @@ impl TiledPanes { .map(|p| p.is_stacked()) .unwrap_or(false); if !pane_id_is_stacked { - let _ = pane_grid.make_pane_stacked(&root_pane_id); + if let Err(e) = pane_grid.make_pane_stacked(&root_pane_id) { + log::error!("Failed to make pane stacked: {:?}", e); + } } match pane_grid.make_room_in_stack_of_pane_id_for_pane(&root_pane_id) { Ok(new_pane_geom) => { diff --git a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs index ddb225a4..0c989a9e 100644 --- a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs +++ b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs @@ -1344,6 +1344,14 @@ impl<'a> TiledPaneGrid<'a> { let _ = pane_resizer.layout(direction, side_length); return true; } + } else { + // best effort resize - we just remove the pane and relayout everything + // this might happen if we are closing a fixed pane + self.panes.borrow_mut().remove(&id); + let mut pane_resizer = PaneResizer::new(self.panes.clone()); + let _ = pane_resizer.layout(SplitDirection::Horizontal, self.display_area.cols); + let _ = pane_resizer.layout(SplitDirection::Vertical, self.display_area.rows); + return true; } false } diff --git a/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs b/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs index 6b4edf40..e4adfbe4 100644 --- a/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs +++ b/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs @@ -1085,7 +1085,7 @@ impl Pane for MockPane { fn handle_plugin_bytes(&mut self, _client_id: ClientId, _bytes: VteBytes) { unimplemented!() } - fn cursor_coordinates(&self) -> Option<(usize, usize)> { + fn cursor_coordinates(&self, _client_id: Option<ClientId>) -> Option<(usize, usize)> { unimplemented!() } diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap index 7ab7a56c..53059240 100644 --- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap +++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap @@ -2,13 +2,13 @@ source: zellij-server/src/panes/./unit/grid_tests.rs expression: "format!(\"{:?}\", grid)" --- -00 (C): title1 title2 title3 title4 title5 -01 (C): 1lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lkjdslkjsdf -02 (C): 2lksdjflksdjf lkjdsflksjdf lkj😳😳sdlkfjsfd lkjsdlkjfd lkjdslkjsdf -03 (C): 3lksdjflksdjf lkjdsflksjdf lkjsd😳lkfjsfd lkjsdlkjfd lkjdslkjsdf -04 (C): 4lksdjflks222djf lk😳222jdsflksjdf lk222jsdlkfjsfd lkwwwjsdlkjfd lkjdslkjsdf -05 (C): 5lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lksdjflksjfdlkjsdlkjfd lkjdrrrrslkjsdf -06 (C): 6lksdjflk😳sdjf l😳kjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lwwkwkjdslkjsdf +00 (C): title1 title2 title3 title4 title5 +01 (C): 1lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lkjdslkjsdf +02 (C): 2lksdjflksdjf lkjdsflksjdf lkj😳😳sdlkfjsfd lkjsdlkjfd lkjdslkjsdf +03 (C): 3lksdjflksdjf lkjdsflksjdf lkjsd😳lkfjsfd lkjsdlkjfd lkjdslkjsdf +04 (C): 4lksdjflks222djf lk😳222jdsflksjdf lk222jsdlkfjsfd lkwwwjsdlkjfd lkjdslkjsdf +05 (C): 5lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lksdjflksjfdlkjsdlkjfd lkjdrrrrslkjsdf +06 (C): 6lksdjflk😳sdjf l😳kjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lwwkwkjdslkjsdf 07 (C): 08 (C): 09 (C): diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index cede7901..24a1e811 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -30,12 +30,13 @@ use wasm_bridge::WasmBridge; use async_std::{channel, future::timeout, task}; use zellij_utils::{ data::{ - ClientInfo, Event, EventType, FloatingPaneCoordinates, InputMode, MessageToPlugin, - PermissionStatus, PermissionType, PipeMessage, PipeSource, PluginCapabilities, - WebServerStatus, + ClientInfo, CommandOrPlugin, Event, EventType, FloatingPaneCoordinates, InputMode, + MessageToPlugin, PermissionStatus, PermissionType, PipeMessage, PipeSource, + PluginCapabilities, WebServerStatus, }, errors::{prelude::*, ContextType, PluginContext}, input::{ + actions::Action, command::TerminalAction, keybinds::Keybinds, layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout}, @@ -86,10 +87,12 @@ pub enum PluginInstruction { Option<TerminalAction>, Option<TiledPaneLayout>, Vec<FloatingPaneLayout>, - usize, // tab_index - bool, // should change focus to new tab - (ClientId, bool), // bool -> is_web_client - Option<NotificationEnd>, // completion signal + usize, // tab_index + Option<Vec<CommandOrPlugin>>, // initial_panes + bool, // block_on_first_terminal + bool, // should change focus to new tab + (ClientId, bool), // bool -> is_web_client + Option<NotificationEnd>, // completion signal ), ApplyCachedEvents { plugin_ids: Vec<PluginId>, @@ -175,6 +178,12 @@ pub enum PluginInstruction { WebServerStarted(String), // String -> the base url of the web server FailedToStartWebServer(String), PaneRenderReport(PaneRenderReport), + UserInput { + client_id: ClientId, + action: Action, + terminal_id: Option<u32>, + cli_client_id: Option<ClientId>, + }, Exit, } @@ -225,6 +234,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::WebServerStarted(..) => PluginContext::WebServerStarted, PluginInstruction::FailedToStartWebServer(..) => PluginContext::FailedToStartWebServer, PluginInstruction::PaneRenderReport(..) => PluginContext::PaneRenderReport, + PluginInstruction::UserInput { .. } => PluginContext::UserInput, } } } @@ -453,6 +463,8 @@ pub(crate) fn plugin_thread_main( mut tab_layout, mut floating_panes_layout, tab_index, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -469,6 +481,26 @@ pub(crate) fn plugin_thread_main( let mut plugin_ids: HashMap<RunPluginOrAlias, Vec<PluginId>> = HashMap::new(); tab_layout = tab_layout.or_else(|| Some(layout.new_tab().0)); + + // Match initial_panes plugins to empty slots in the layout + if let Some(ref initial_panes_vec) = initial_panes { + if let Some(ref mut tiled_layout) = tab_layout { + for initial_pane in initial_panes_vec.iter() { + if let CommandOrPlugin::Plugin(run_plugin_or_alias) = initial_pane { + if !tiled_layout.replace_next_empty_slot_with_run(Run::Plugin( + run_plugin_or_alias.clone(), + )) { + log::warn!( + "More initial_panes provided than empty slots available" + ); + break; + } + } + // Skip CommandOrPlugin::Command entries (handled by pty thread) + } + } + } + tab_layout.as_mut().map(|t| { t.populate_plugin_aliases_in_layout(&plugin_aliases); if let Some(cwd) = cwd.as_ref() { @@ -481,7 +513,7 @@ pub(crate) fn plugin_thread_main( .as_mut() .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases)); }); - let mut extracted_run_instructions = tab_layout + let extracted_run_instructions = tab_layout .clone() .unwrap_or_else(|| layout.new_tab().0) .extract_run_instructions(); @@ -496,8 +528,10 @@ pub(crate) fn plugin_thread_main( .filter(|f| !f.already_running) .map(|f| f.run.clone()) .collect(); - extracted_run_instructions.append(&mut extracted_floating_plugins); - for run_instruction in extracted_run_instructions { + let mut all_run_instructions = extracted_run_instructions; + all_run_instructions.append(&mut extracted_floating_plugins); + + for run_instruction in all_run_instructions { if let Some(Run::Plugin(run_plugin_or_alias)) = run_instruction { let run_plugin = run_plugin_or_alias.get_run_plugin(); let cwd = run_plugin_or_alias @@ -525,6 +559,8 @@ pub(crate) fn plugin_thread_main( floating_panes_layout, tab_index, plugin_ids, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -989,6 +1025,20 @@ pub(crate) fn plugin_thread_main( .handle_pane_render_report(pane_render_report, shutdown_send.clone()) .non_fatal(); }, + PluginInstruction::UserInput { + client_id, + action, + terminal_id, + cli_client_id, + } => { + // Fire Event::UserAction to all subscribed plugins with InterceptInput permission + let updates = vec![( + None, + None, + Event::UserAction(action, client_id, terminal_id, cli_client_id), + )]; + wasm_bridge.update_plugins(updates, shutdown_send.clone())?; + }, PluginInstruction::Exit => { break; }, diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 5f90ebbe..c0368fbc 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -9137,3 +9137,670 @@ pub fn before_close_plugin_event() { .unwrap(); assert_snapshot!(format!("{:#?}", sent_instruction)); } + +#[test] +#[ignore] +pub fn show_cursor_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::ShowPluginCursor, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('a')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let show_cursor_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::ShowPluginCursor(plugin_id, client_id, cursor_position) = i { + Some((*plugin_id, *client_id, *cursor_position)) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", show_cursor_instruction)); +} + +#[test] +#[ignore] +pub fn hide_cursor_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::ShowPluginCursor, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('b')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let hide_cursor_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::ShowPluginCursor(plugin_id, client_id, cursor_position) = i { + Some((*plugin_id, *client_id, *cursor_position)) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", hide_cursor_instruction)); +} + +#[test] +#[ignore] +pub fn copy_to_clipboard_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::CopyTextToClipboard, + screen_receiver, + 1, + &PermissionType::WriteToClipboard, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('c')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let copy_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::CopyTextToClipboard(text, plugin_id) = i { + Some((text.clone(), *plugin_id)) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", copy_instruction)); +} + +#[test] +#[ignore] +pub fn run_action_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::MoveFocusLeft, + screen_receiver, + 1, + &PermissionType::RunActionsAsUser, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('d')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let move_focus_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::MoveFocusLeft(client_id, notification_end) = i { + Some((*client_id, notification_end.clone())) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", move_focus_instruction)); +} + +#[test] +#[ignore] +pub fn send_sigint_to_pane_id_plugin_command() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) = + create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); + + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::SendSigintToPaneId, + pty_receiver, + 1 + ); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('e')).with_super_modifier()), + )])); + + pty_thread.join().unwrap(); + teardown(); + + let sigint_instruction = received_pty_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let PtyInstruction::SendSigintToPaneId(pane_id) = i { + Some(*pane_id) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", sigint_instruction)); +} + +#[test] +#[ignore] +pub fn send_sigkill_to_pane_id_plugin_command() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) = + create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); + + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::SendSigkillToPaneId, + pty_receiver, + 1 + ); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('f')).with_super_modifier()), + )])); + + pty_thread.join().unwrap(); + teardown(); + + let sigkill_instruction = received_pty_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let PtyInstruction::SendSigkillToPaneId(pane_id) = i { + Some(*pane_id) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", sigkill_instruction)); +} + +#[test] +#[ignore] +pub fn copy_to_clipboard_without_permission() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + // This test denies the permission and expects no CopyTextToClipboard instruction + let screen_thread = deny_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::CopyTextToClipboard, + screen_receiver, + 1, + &PermissionType::WriteToClipboard, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('c')).with_super_modifier()), + )])); + + // Give it time to potentially (incorrectly) send the instruction + std::thread::sleep(std::time::Duration::from_millis(1000)); + + let _ = plugin_thread_sender.send(PluginInstruction::Exit); + screen_thread.join().unwrap(); + teardown(); + + let copy_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::CopyTextToClipboard(text, plugin_id) = i { + Some((text.clone(), *plugin_id)) + } else { + None + } + }); + + // Should be None because permission was denied + assert_snapshot!(format!("{:#?}", copy_instruction)); +} + +#[test] +#[ignore] +pub fn run_action_without_permission() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + let screen_thread = deny_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::MoveFocusLeft, + screen_receiver, + 1, + &PermissionType::RunActionsAsUser, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('d')).with_super_modifier()), + )])); + + std::thread::sleep(std::time::Duration::from_millis(1000)); + + let _ = plugin_thread_sender.send(PluginInstruction::Exit); + screen_thread.join().unwrap(); + teardown(); + + let move_focus_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::MoveFocusLeft(client_id, notification_end) = i { + Some((*client_id, notification_end.clone())) + } else { + None + } + }); + + // Should be None because permission was denied + assert_snapshot!(format!("{:#?}", move_focus_instruction)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap index 975e20a3..88023c4b 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap index 133b23ff..38179b85 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap index cdaa2362..d0312218 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap index cbf3c8c2..b795da46 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap @@ -12,6 +12,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), None, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap index 1041e20f..97e61a45 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap @@ -12,6 +12,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), None, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_plugin_command.snap new file mode 100644 index 00000000..cc4717e3 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_plugin_command.snap @@ -0,0 +1,10 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", copy_instruction)" +--- +Some( + ( + "test clipboard text", + 0, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_without_permission.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_without_permission.snap new file mode 100644 index 00000000..333c0389 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_without_permission.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", copy_instruction)" +--- +None diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap index ea71beac..1de9477d 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap @@ -11,6 +11,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap index cc572e0c..370a7bbf 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap index f8cf4623..3ef17f9d 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap index 305eb5a5..fbd45fd0 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap @@ -18,6 +18,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap index 6b98856a..31a63f73 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap @@ -8,11 +8,14 @@ Some( 1, ), false, + false, 1, Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap index 0b37d318..8073ceb8 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap index 345f0479..0a3a1d23 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap @@ -8,11 +8,14 @@ Some( 1, ), false, + false, 1, Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap index ca2edc5f..c76a5ecc 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap index 1451fa50..f4653b62 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap index 13bbdb4a..48cf0870 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap @@ -12,6 +12,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap index 3fd2e40f..7e45c1fc 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap @@ -18,6 +18,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap index 28c7dedc..91ec8a9b 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 5505 expression: "format!(\"{:#?}\", permissions)" --- Some( @@ -15,5 +14,7 @@ Some( ReadCliPipes, MessageAndLaunchOtherPlugins, Reconfigure, + RunActionsAsUser, + WriteToClipboard, ], ) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__hide_cursor_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__hide_cursor_plugin_command.snap new file mode 100644 index 00000000..31880c73 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__hide_cursor_plugin_command.snap @@ -0,0 +1,11 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", hide_cursor_instruction)" +--- +Some( + ( + 0, + 1, + None, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap index c3b1d5f2..94419199 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap index 72b7dc15..f901ba75 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap index b9306571..1c2a8fc6 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap index 9da82855..51f2cc15 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap index 1213308e..128c7abe 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap @@ -17,6 +17,8 @@ Some( [], [], ), + None, + false, true, ( 1, @@ -26,6 +28,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap index bc4faa99..1c74a94b 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap @@ -63,6 +63,8 @@ Some( [], [], ), + None, + false, false, ( 1, @@ -72,6 +74,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap index 2ba4de2a..889a6e9d 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap @@ -63,6 +63,8 @@ Some( [], [], ), + None, + false, true, ( 1, @@ -72,6 +74,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap index 21c4257c..87c3264c 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap index ac0838cc..3afcead6 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap index ca751a22..596700c5 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap index eb598cf0..be8503de 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap @@ -36,6 +36,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap index d235c416..b899e596 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap @@ -36,6 +36,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap index cde191a5..fc2087d6 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap index 89986fd2..4d2689db 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap index b2f0f9ae..b42943fc 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap @@ -31,6 +31,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap index 5cc2ce7c..dd197b76 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap @@ -31,6 +31,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap index d85ffc61..5292281b 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap index 014ac3d3..5709731c 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap index b35b3b0b..523e76fd 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap index d00a7663..5057701a 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap index 5816c283..119444e2 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap @@ -33,6 +33,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap index 9450f7c0..4846cd65 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap @@ -23,6 +23,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap index f23a792c..53b81db8 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap @@ -35,6 +35,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap index 75f47da7..1400f081 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 5414 expression: "format!(\"{:#?}\", new_tab_event)" --- Some( @@ -17,5 +16,7 @@ Some( ReadCliPipes, MessageAndLaunchOtherPlugins, Reconfigure, + WriteToClipboard, + RunActionsAsUser, ], ) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap index 37275f43..0184c9a6 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap @@ -14,6 +14,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap index d9fa542d..71766b1e 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap @@ -16,6 +16,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_plugin_command.snap new file mode 100644 index 00000000..dcbf7004 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_plugin_command.snap @@ -0,0 +1,17 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", move_focus_instruction)" +--- +Some( + ( + 1, + Some( + NotificationEnd { + channel: None, + exit_status: None, + unblock_condition: None, + affected_pane_id: None, + }, + ), + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_without_permission.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_without_permission.snap new file mode 100644 index 00000000..102abab0 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_without_permission.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", move_focus_instruction)" +--- +None diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap index c8605a1c..738a7625 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap index 410902d5..339ea8c4 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap index 9f2effac..5c0b9088 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap index acc0d6a6..d0250b34 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap index aa9592c2..14438cf8 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap @@ -18,6 +18,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigint_to_pane_id_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigint_to_pane_id_plugin_command.snap new file mode 100644 index 00000000..a7558487 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigint_to_pane_id_plugin_command.snap @@ -0,0 +1,9 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", sigint_instruction)" +--- +Some( + Terminal( + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigkill_to_pane_id_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigkill_to_pane_id_plugin_command.snap new file mode 100644 index 00000000..fb7dc0a1 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigkill_to_pane_id_plugin_command.snap @@ -0,0 +1,9 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", sigkill_instruction)" +--- +Some( + Terminal( + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_cursor_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_cursor_plugin_command.snap new file mode 100644 index 00000000..5b53f360 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_cursor_plugin_command.snap @@ -0,0 +1,16 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", show_cursor_instruction)" +--- +Some( + ( + 0, + 1, + Some( + ( + 5, + 10, + ), + ), + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap index 24f6c12f..457bebdc 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap @@ -8,6 +8,7 @@ Some( 1, ), true, + false, 1, None, ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap index a99b0230..41396ddb 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap @@ -8,11 +8,14 @@ Some( 0, ), false, + false, 1, Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap index 290247c0..845f37ba 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap @@ -21,6 +21,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap index 6df0b5df..b4ff3c00 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap @@ -330,6 +330,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap index f8959104..06cde256 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap index 34e96dae..44747b66 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap index 8f375b9c..8e24bff8 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap index e1bded4d..c91f66aa 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap @@ -8,6 +8,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap index e088770b..32965d58 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap index 6c500286..99df8475 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap index 54566d68..7836e7d2 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap index 8bb05ce1..abf3d1a2 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap @@ -16,6 +16,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap index 8bb05ce1..abf3d1a2 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap @@ -16,6 +16,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 5dd78d13..2453a0a1 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -113,7 +113,7 @@ impl LoadingContext { let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin_config.location).to_string()) .join(format!("{}-{}", plugin_id, client_id)); - let plugin_own_cache_dir = ZELLIJ_SESSION_CACHE_DIR + let plugin_own_cache_dir = ZELLIJ_CACHE_DIR .join(Url::from(&plugin_config.location).to_string()) .join(format!("plugin_cache")); let default_mode = wasm_bridge @@ -1901,9 +1901,11 @@ fn check_event_permission( | Event::EditPaneExited(..) | Event::FailedToWriteConfigToDisk(..) | Event::CommandPaneReRun(..) + | Event::CwdChanged(..) | Event::InputReceived => PermissionType::ReadApplicationState, Event::WebServerStatus(..) => PermissionType::StartWebServer, Event::PaneRenderReport(..) => PermissionType::ReadPaneContents, + Event::UserAction(..) => PermissionType::InterceptInput, _ => return (PermissionStatus::Granted, None), }; @@ -1943,54 +1945,63 @@ pub fn apply_event_to_plugin( mode_info.keybinds = running_plugin.store.data().keybinds.to_keybinds_vec(); mode_info.base_mode = Some(running_plugin.store.data().default_mode); } - let protobuf_event: ProtobufEvent = event - .clone() - .try_into() - .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?; - let update = instance - .get_typed_func::<(), i32>(&mut running_plugin.store, "update") - .with_context(err_context)?; - wasi_write_object(running_plugin.store.data(), &protobuf_event.encode_to_vec()) - .with_context(err_context)?; - let should_render = update - .call(&mut running_plugin.store, ()) - .with_context(err_context)?; - let mut should_render = should_render == 1; - if let Event::PermissionRequestResult(..) = event { - // we always render in this case, otherwise the request permission screen stays on - // screen - should_render = true; - } - if rows > 0 && columns > 0 && should_render { - let rendered_bytes = instance - .get_typed_func::<(i32, i32), ()>(&mut running_plugin.store, "render") - .and_then(|render| { - render.call(&mut running_plugin.store, (rows as i32, columns as i32)) - }) - .map_err(|e| anyhow!(e)) - .and_then(|_| { - wasi_read_string(running_plugin.store.data()).map_err(|e| anyhow!(e)) - }) - .with_context(err_context)?; - let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None); - let plugin_render_asset = PluginRenderAsset::new( - plugin_id, - client_id, - rendered_bytes.as_bytes().to_vec(), - ) - .with_pipes(pipes_to_block_or_unblock); - plugin_render_assets.push(plugin_render_asset); - } else { - // This is a bit of a hack to get around the fact that plugins are allowed not to - // render and still unblock CLI pipes - let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None); - let plugin_render_asset = PluginRenderAsset::new(plugin_id, client_id, vec![]) - .with_pipes(pipes_to_block_or_unblock); - let _ = senders - .send_to_plugin(PluginInstruction::UnblockCliPipes(vec![ - plugin_render_asset, - ])) - .context("failed to unblock input pipe"); + let protobuf_event: Result<ProtobufEvent, _> = event.clone().try_into(); + match protobuf_event { + Ok(protobuf_event) => { + let update = instance + .get_typed_func::<(), i32>(&mut running_plugin.store, "update") + .with_context(err_context)?; + wasi_write_object(running_plugin.store.data(), &protobuf_event.encode_to_vec()) + .with_context(err_context)?; + let should_render = update + .call(&mut running_plugin.store, ()) + .with_context(err_context)?; + let mut should_render = should_render == 1; + if let Event::PermissionRequestResult(..) = event { + // we always render in this case, otherwise the request permission screen stays on + // screen + should_render = true; + } + if rows > 0 && columns > 0 && should_render { + let rendered_bytes = instance + .get_typed_func::<(i32, i32), ()>(&mut running_plugin.store, "render") + .and_then(|render| { + render + .call(&mut running_plugin.store, (rows as i32, columns as i32)) + }) + .map_err(|e| anyhow!(e)) + .and_then(|_| { + wasi_read_string(running_plugin.store.data()) + .map_err(|e| anyhow!(e)) + }) + .with_context(err_context)?; + let pipes_to_block_or_unblock = + pipes_to_block_or_unblock(running_plugin, None); + let plugin_render_asset = PluginRenderAsset::new( + plugin_id, + client_id, + rendered_bytes.as_bytes().to_vec(), + ) + .with_pipes(pipes_to_block_or_unblock); + plugin_render_assets.push(plugin_render_asset); + } else { + // This is a bit of a hack to get around the fact that plugins are allowed not to + // render and still unblock CLI pipes + let pipes_to_block_or_unblock = + pipes_to_block_or_unblock(running_plugin, None); + let plugin_render_asset = + PluginRenderAsset::new(plugin_id, client_id, vec![]) + .with_pipes(pipes_to_block_or_unblock); + let _ = senders + .send_to_plugin(PluginInstruction::UnblockCliPipes(vec![ + plugin_render_asset, + ])) + .context("failed to unblock input pipe"); + } + }, + Err(e) => { + log::error!("Failed to convert to protobuf: {:?}", e); + }, } }, (PermissionStatus::Denied, permission) => { diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 526b4bcb..f64a9e18 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -20,9 +20,9 @@ use std::{ }; use wasmi::{Caller, Linker}; use zellij_utils::data::{ - CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, KeyWithModifier, LayoutInfo, - MessageToPlugin, NewPanePlacement, OriginatingPlugin, PaneScrollbackResponse, PermissionStatus, - PermissionType, PluginPermission, + CommandType, ConnectToSession, Event, FloatingPaneCoordinates, GetPanePidResponse, HttpVerb, + KeyWithModifier, LayoutInfo, MessageToPlugin, NewPanePlacement, OriginatingPlugin, + PaneScrollbackResponse, PermissionStatus, PermissionType, PluginPermission, }; use zellij_utils::input::permission::PermissionCache; use zellij_utils::ipc::{ClientToServerMsg, IpcSenderWithContext}; @@ -39,7 +39,7 @@ use prost::Message; use zellij_utils::{ consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR}, data::{ - CommandToRun, Direction, Event, EventType, FileToOpen, InputMode, PluginCommand, PluginIds, + CommandToRun, Direction, EventType, FileToOpen, InputMode, PluginCommand, PluginIds, PluginMessage, Resize, ResizeStrategy, }, errors::prelude::*, @@ -50,7 +50,7 @@ use zellij_utils::{ }, plugin_api::{ event::ProtobufPaneScrollbackResponse, - plugin_command::ProtobufPluginCommand, + plugin_command::{ProtobufGetPanePidResponse, ProtobufPluginCommand}, plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}, }, }; @@ -104,6 +104,7 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?, PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?, PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable), + PluginCommand::ShowCursor(cursor_position) => show_cursor(env, cursor_position), PluginCommand::GetPluginIds => get_plugin_ids(env), PluginCommand::GetZellijVersion => get_zellij_version(env), PluginCommand::OpenFile(file_to_open, context) => { @@ -198,6 +199,7 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::EditScrollback => edit_scrollback(env), PluginCommand::Write(bytes) => write(env, bytes), PluginCommand::WriteChars(chars) => write_chars(env, chars), + PluginCommand::CopyToClipboard(text) => copy_to_clipboard(env, text)?, PluginCommand::ToggleTab => toggle_tab(env), PluginCommand::MovePane => move_pane(env), PluginCommand::MovePaneWithDirection(direction) => { @@ -233,12 +235,26 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::ClosePluginPane(plugin_pane_id) => { close_plugin_pane(env, plugin_pane_id) }, - PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => { - focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden) - }, - PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => { - focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden) - }, + PluginCommand::FocusTerminalPane( + terminal_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => focus_terminal_pane( + env, + terminal_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ), + PluginCommand::FocusPluginPane( + plugin_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => focus_plugin_pane( + env, + plugin_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ), PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => { rename_terminal_pane(env, terminal_pane_id, &new_name) }, @@ -317,9 +333,16 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::HidePaneWithId(pane_id) => { hide_pane_with_id(env, pane_id.into())? }, - PluginCommand::ShowPaneWithId(pane_id, should_float_if_hidden) => { - show_pane_with_id(env, pane_id.into(), should_float_if_hidden) - }, + PluginCommand::ShowPaneWithId( + pane_id, + should_float_if_hidden, + should_focus_pane, + ) => show_pane_with_id( + env, + pane_id.into(), + should_float_if_hidden, + should_focus_pane, + ), PluginCommand::OpenCommandPaneBackground(command_to_run, context) => { open_command_pane_background(env, command_to_run, context) }, @@ -342,6 +365,13 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::WriteCharsToPaneId(chars, pane_id) => { write_chars_to_pane_id(env, chars, pane_id.into()) }, + PluginCommand::SendSigintToPaneId(pane_id) => { + send_sigint_to_pane_id(env, pane_id.into()) + }, + PluginCommand::SendSigkillToPaneId(pane_id) => { + send_sigkill_to_pane_id(env, pane_id.into()) + }, + PluginCommand::GetPanePid { pane_id } => get_pane_pid(env, pane_id.into()), PluginCommand::MovePaneWithPaneId(pane_id) => { move_pane_with_pane_id(env, pane_id.into()) }, @@ -509,11 +539,14 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::ReplacePaneWithExistingPane( pane_id_to_replace, existing_pane_id, + suppress_replaced_pane, ) => replace_pane_with_existing_pane( &mut env, pane_id_to_replace.into(), existing_pane_id.into(), + suppress_replaced_pane, ), + PluginCommand::RunAction(action, context) => run_action(&env, action, context), }, (PermissionStatus::Denied, permission) => { log::error!( @@ -595,6 +628,27 @@ fn set_selectable(env: &PluginEnv, selectable: bool) { .non_fatal(); } +fn show_cursor(env: &PluginEnv, cursor_position: Option<(usize, usize)>) { + env.senders + .send_to_screen(ScreenInstruction::ShowPluginCursor( + env.plugin_id, + env.client_id, + cursor_position, + )) + .with_context(|| { + format!( + "failed to {} plugin cursor from plugin {}", + if cursor_position.is_some() { + "show" + } else { + "hide" + }, + env.name() + ) + }) + .non_fatal(); +} + fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Result<()> { if PermissionCache::from_path_or_default(None) .check_permissions(env.plugin.location.to_string(), &permissions) @@ -680,10 +734,74 @@ fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap<String in_place, start_suppressed, coordinates: None, + near_current_pane: false, }; apply_action!(action, error_msg, env); } +fn run_action(env: &PluginEnv, mut action: Action, context: BTreeMap<String, String>) { + // Clone the necessary data to move into the thread + action.populate_originating_plugin(OriginatingPlugin::new( + env.plugin_id, + env.client_id, + context.clone(), + )); + let action_clone = action.clone(); + let client_id = env.client_id; + let plugin_id = env.plugin_id; + let senders = env.senders.clone(); + let capabilities = env.capabilities.clone(); + let client_attributes = env.client_attributes.clone(); + let default_shell = env.default_shell.clone(); + let default_layout = env.default_layout.clone(); + let keybinds = env.keybinds.clone(); + let default_mode = env.default_mode.clone(); + let plugin_name = env.name().to_string(); + + // Spawn a new thread to execute the action + thread::spawn(move || { + // Execute the action and capture the result + let pane_id = match route_action( + action, + client_id, + None, + Some(PaneId::Plugin(plugin_id)), + senders.clone(), + capabilities, + client_attributes, + default_shell, + default_layout, + None, + keybinds, + default_mode, + None, + ) { + Ok((_should_break, result)) => { + // Extract pane_id from ActionCompletionResult + result.and_then(|r| r.affected_pane_id) + }, + Err(e) => { + log::error!("failed to run action in plugin {}: {:?}", plugin_name, e); + None + }, + }; + + // After action completes, send ActionComplete event with context + // Convert server PaneId to zellij_utils PaneId + let pane_id_for_event = pane_id.map(|p| p.into()); + let updates = vec![( + Some(plugin_id), + Some(client_id), + Event::ActionComplete(action_clone, pane_id_for_event, context), + )]; + + // Send the ActionComplete event back to the plugin + if let Err(e) = senders.send_to_plugin(PluginInstruction::Update(updates)) { + log::error!("Failed to send ActionComplete event: {:?}", e); + } + }); +} + fn open_file_floating( env: &PluginEnv, file_to_open: FileToOpen, @@ -708,6 +826,7 @@ fn open_file_floating( in_place, start_suppressed, coordinates: floating_pane_coordinates, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -736,6 +855,7 @@ fn open_file_in_place( in_place, start_suppressed, coordinates: None, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -845,6 +965,7 @@ fn open_terminal(env: &PluginEnv, cwd: PathBuf) { direction: None, command: run_command_action, pane_name: None, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -894,6 +1015,7 @@ fn open_terminal_floating( command: run_command_action, pane_name: None, coordinates: floating_pane_coordinates, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -942,6 +1064,9 @@ fn open_terminal_in_place(env: &PluginEnv, cwd: PathBuf) { let action = Action::NewInPlacePane { command: run_command_action, pane_name: None, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }; apply_action!(action, error_msg, env); } @@ -1044,6 +1169,7 @@ fn open_command_pane( direction, command: Some(run_command_action), pane_name: name, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -1120,6 +1246,7 @@ fn open_command_pane_floating( command: Some(run_command_action), pane_name: name, coordinates: floating_pane_coordinates, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -1195,6 +1322,9 @@ fn open_command_pane_in_place( let action = Action::NewInPlacePane { command: Some(run_command_action), pane_name: name, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }; apply_action!(action, error_msg, env); } @@ -1418,20 +1548,36 @@ fn show_self(env: &PluginEnv, should_float_if_hidden: bool) { let action = Action::FocusPluginPaneWithId { pane_id: env.plugin_id, should_float_if_hidden, + should_be_in_place_if_hidden: false, }; let error_msg = || format!("Failed to show self for plugin"); apply_action!(action, error_msg, env); } -fn show_pane_with_id(env: &PluginEnv, pane_id: PaneId, should_float_if_hidden: bool) { - let _ = env - .senders - .send_to_screen(ScreenInstruction::FocusPaneWithId( - pane_id, - should_float_if_hidden, - env.client_id, - None, - )); +fn show_pane_with_id( + env: &PluginEnv, + pane_id: PaneId, + should_float_if_hidden: bool, + should_focus_pane: bool, +) { + if should_focus_pane { + let _ = env + .senders + .send_to_screen(ScreenInstruction::FocusPaneWithId( + pane_id, + should_float_if_hidden, + false, + env.client_id, + None, + )); + } else { + let _ = env + .senders + .send_to_screen(ScreenInstruction::UnsuppressOrExpandPane( + pane_id, + should_float_if_hidden, + )); + } } fn close_self(env: &PluginEnv) { @@ -1524,6 +1670,8 @@ fn apply_layout(env: &PluginEnv, layout: Layout) { tab_name: None, should_change_focus_to_new_tab: true, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }; tabs_to_open.push(action); } else { @@ -1542,6 +1690,8 @@ fn apply_layout(env: &PluginEnv, layout: Layout) { tab_name, should_change_focus_to_new_tab: should_focus_tab, cwd: cwd.clone(), + initial_panes: None, + first_pane_unblock_condition: None, }; tabs_to_open.push(action); } @@ -1562,6 +1712,8 @@ fn new_tab(env: &PluginEnv, name: Option<String>, cwd: Option<String>) { tab_name: name, should_change_focus_to_new_tab: true, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }; let error_msg = || format!("Failed to open new tab"); apply_action!(action, error_msg, env); @@ -1741,6 +1893,12 @@ fn write_chars(env: &PluginEnv, chars_to_write: String) { apply_action!(action, error_msg, env); } +fn copy_to_clipboard(env: &PluginEnv, text: String) -> Result<()> { + env.senders + .send_to_screen(ScreenInstruction::CopyTextToClipboard(text, env.plugin_id)) + .with_context(|| format!("failed to copy to clipboard in plugin {}", env.name())) +} + fn toggle_tab(env: &PluginEnv) { let error_msg = || format!("Failed to toggle tab"); let action = Action::ToggleTab; @@ -1938,19 +2096,31 @@ fn close_plugin_pane(env: &PluginEnv, plugin_pane_id: u32) { .non_fatal(); } -fn focus_terminal_pane(env: &PluginEnv, terminal_pane_id: u32, should_float_if_hidden: bool) { +fn focus_terminal_pane( + env: &PluginEnv, + terminal_pane_id: u32, + should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, +) { let action = Action::FocusTerminalPaneWithId { pane_id: terminal_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }; let error_msg = || format!("Failed to focus terminal pane"); apply_action!(action, error_msg, env); } -fn focus_plugin_pane(env: &PluginEnv, plugin_pane_id: u32, should_float_if_hidden: bool) { +fn focus_plugin_pane( + env: &PluginEnv, + plugin_pane_id: u32, + should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, +) { let action = Action::FocusPluginPaneWithId { pane_id: plugin_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }; let error_msg = || format!("Failed to focus plugin pane"); apply_action!(action, error_msg, env); @@ -2237,6 +2407,92 @@ fn write_chars_to_pane_id(env: &PluginEnv, chars: String, pane_id: PaneId) { .send_to_screen(ScreenInstruction::WriteToPaneId(bytes, pane_id)); } +fn send_sigint_to_pane_id(env: &PluginEnv, pane_id: PaneId) { + let err_context = || { + format!( + "failed to send SIGINT to pane {:?} from plugin {}", + pane_id, + env.name() + ) + }; + + env.senders + .send_to_pty(PtyInstruction::SendSigintToPaneId(pane_id)) + .with_context(err_context) + .non_fatal(); +} + +fn send_sigkill_to_pane_id(env: &PluginEnv, pane_id: PaneId) { + let err_context = || { + format!( + "failed to send SIGKILL to pane {:?} from plugin {}", + pane_id, + env.name() + ) + }; + + env.senders + .send_to_pty(PtyInstruction::SendSigkillToPaneId(pane_id)) + .with_context(err_context) + .non_fatal(); +} + +fn get_pane_pid(env: &PluginEnv, pane_id: PaneId) { + use crossbeam::channel::RecvTimeoutError; + use std::time::Duration; + + let err_context = || { + format!( + "failed to get PID for pane {:?} from plugin {}", + pane_id, + env.name() + ) + }; + + // Create oneshot channel for response + let (response_sender, response_receiver) = crossbeam::channel::bounded(1); + + // Send request directly to PTY thread + env.senders + .send_to_pty(PtyInstruction::GetPanePid { + pane_id, + response_channel: response_sender, + }) + .with_context(err_context) + .non_fatal(); + + // Block waiting for response with 100ms timeout + let response = match response_receiver.recv_timeout(Duration::from_millis(100)) { + Ok(response) => response, + Err(RecvTimeoutError::Timeout) => { + log::error!( + "GetPanePid timed out after 5s for plugin {} requesting pane {:?}", + env.plugin_id, + pane_id + ); + GetPanePidResponse::Err(format!("Timeout retrieving PID for pane {:?}", pane_id)) + }, + Err(RecvTimeoutError::Disconnected) => { + log::error!( + "GetPanePid channel disconnected for plugin {} requesting pane {:?}", + env.plugin_id, + pane_id + ); + GetPanePidResponse::Err(format!( + "Channel disconnected while retrieving PID for pane {:?}", + pane_id + )) + }, + }; + + // Encode response and write to plugin's stdin + let protobuf_response = ProtobufGetPanePidResponse::from(response); + let serialized = protobuf_response.encode_to_vec(); + wasi_write_object(env, &serialized) + .with_context(err_context) + .non_fatal(); +} + fn move_pane_with_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders @@ -2666,12 +2922,14 @@ fn replace_pane_with_existing_pane( env: &mut PluginEnv, pane_to_replace: PaneId, existing_pane: PaneId, + suppress_replaced_pane: bool, ) { let _ = env .senders .send_to_screen(ScreenInstruction::ReplacePaneWithExistingPane( pane_to_replace, existing_pane, + suppress_replaced_pane, )); } @@ -2762,6 +3020,7 @@ fn check_command_permission( | PluginCommand::WriteChars(..) | PluginCommand::WriteToPaneId(..) | PluginCommand::WriteCharsToPaneId(..) => PermissionType::WriteToStdin, + PluginCommand::CopyToClipboard(..) => PermissionType::WriteToClipboard, PluginCommand::SwitchTabTo(..) | PluginCommand::SwitchToMode(..) | PluginCommand::NewTabsWithLayout(..) @@ -2842,14 +3101,16 @@ fn check_command_permission( | PluginCommand::FloatMultiplePanes(..) | PluginCommand::EmbedMultiplePanes(..) | PluginCommand::ReplacePaneWithExistingPane(..) - | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, + | PluginCommand::KillSessions(..) + | PluginCommand::SendSigintToPaneId(..) + | PluginCommand::SendSigkillToPaneId(..) => PermissionType::ChangeApplicationState, PluginCommand::UnblockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..) | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins, - PluginCommand::ListClients | PluginCommand::DumpSessionLayout => { - PermissionType::ReadApplicationState - }, + PluginCommand::ListClients + | PluginCommand::DumpSessionLayout + | PluginCommand::GetPanePid { .. } => PermissionType::ReadApplicationState, PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => { PermissionType::Reconfigure }, @@ -2868,6 +3129,7 @@ fn check_command_permission( PermissionType::InterceptInput }, PluginCommand::GetPaneScrollback { .. } => PermissionType::ReadPaneContents, + PluginCommand::RunAction(..) => PermissionType::RunActionsAsUser, _ => return (PermissionStatus::Granted, None), }; diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 39bf4668..fee6054e 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -17,7 +17,10 @@ use nix::unistd::Pid; use std::sync::Arc; use std::{collections::HashMap, os::unix::io::RawFd, path::PathBuf}; use zellij_utils::{ - data::{Direction, Event, FloatingPaneCoordinates, NewPanePlacement, OriginatingPlugin}, + data::{ + CommandOrPlugin, Event, FloatingPaneCoordinates, GetPanePidResponse, NewPanePlacement, + OriginatingPlugin, + }, errors::prelude::*, errors::{ContextType, PtyContext}, input::{ @@ -66,6 +69,8 @@ pub enum PtyInstruction { Vec<FloatingPaneLayout>, usize, // tab_index HashMap<RunPluginOrAlias, Vec<u32>>, // plugin_ids + Option<Vec<CommandOrPlugin>>, // initial_panes + bool, // block_on_first_terminal bool, // should change focus to new tab (ClientId, bool), // bool -> is_web_client Option<NotificationEnd>, // completion signal @@ -112,6 +117,13 @@ pub enum PtyInstruction { }, ListClientsToPlugin(SessionLayoutMetadata, PluginId, ClientId), ReportPluginCwd(PluginId, PathBuf), + SendSigintToPaneId(PaneId), + SendSigkillToPaneId(PaneId), + GetPanePid { + pane_id: PaneId, + response_channel: crossbeam::channel::Sender<GetPanePidResponse>, + }, + UpdateAndReportCwds, Exit, } @@ -136,6 +148,10 @@ impl From<&PtyInstruction> for PtyContext { PtyInstruction::Reconfigure { .. } => PtyContext::Reconfigure, PtyInstruction::ListClientsToPlugin(..) => PtyContext::ListClientsToPlugin, PtyInstruction::ReportPluginCwd(..) => PtyContext::ReportPluginCwd, + PtyInstruction::SendSigintToPaneId(..) => PtyContext::SendSigintToPaneId, + PtyInstruction::SendSigkillToPaneId(..) => PtyContext::SendSigkillToPaneId, + PtyInstruction::GetPanePid { .. } => PtyContext::GetPanePid, + PtyInstruction::UpdateAndReportCwds => PtyContext::UpdateAndReportCwds, PtyInstruction::Exit => PtyContext::Exit, } } @@ -150,7 +166,8 @@ pub(crate) struct Pty { task_handles: HashMap<u32, JoinHandle<()>>, // terminal_id to join-handle default_editor: Option<PathBuf>, post_command_discovery_hook: Option<String>, - plugin_cwds: HashMap<u32, PathBuf>, // plugin_id -> cwd + plugin_cwds: HashMap<u32, PathBuf>, // plugin_id -> cwd + terminal_cwds: HashMap<u32, PathBuf>, // terminal_id -> cwd } pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> { @@ -430,6 +447,8 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> { floating_panes_layout, tab_index, plugin_ids, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, client_id_and_is_web_client, completion_tx, @@ -447,7 +466,9 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> { floating_panes_layout, terminal_action.clone(), plugin_ids, + initial_panes, tab_index, + block_on_first_terminal, should_change_focus_to_new_tab, client_id_and_is_web_client, completion_tx, @@ -702,6 +723,22 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> { } => { pty.reconfigure(default_editor, post_command_discovery_hook); }, + PtyInstruction::SendSigintToPaneId(pane_id) => { + pty.send_sigint_to_pane(pane_id); + }, + PtyInstruction::SendSigkillToPaneId(pane_id) => { + pty.send_sigkill_to_pane(pane_id); + }, + PtyInstruction::GetPanePid { + pane_id, + response_channel, + } => { + let response = pty.get_pane_pid(pane_id); + let _ = response_channel.send(response); + }, + PtyInstruction::UpdateAndReportCwds => { + pty.update_and_report_cwds(); + }, PtyInstruction::Exit => break, } } @@ -725,6 +762,7 @@ impl Pty { originating_plugins: HashMap::new(), post_command_discovery_hook, plugin_cwds: HashMap::new(), + terminal_cwds: HashMap::new(), } } pub fn get_default_terminal( @@ -774,12 +812,18 @@ impl Pty { .get(&client_id) .and_then(|pane| match pane { PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), - PaneId::Terminal(id) => self.id_to_child_pid.get(id).and_then(|&id| { - self.bus - .os_input - .as_ref() - .and_then(|input| input.get_cwd(Pid::from_raw(id))) - }), + PaneId::Terminal(id) => { + // Try to get CWD from OS, fall back to cached value + self.id_to_child_pid + .get(id) + .and_then(|&pid| { + self.bus + .os_input + .as_ref() + .and_then(|input| input.get_cwd(Pid::from_raw(pid))) + }) + .or_else(|| self.terminal_cwds.get(id).cloned()) + }, }) }; }; @@ -789,12 +833,16 @@ impl Pty { if run_command.cwd.is_none() { run_command.cwd = match pane_id { PaneId::Terminal(terminal_pane_id) => { - self.id_to_child_pid.get(terminal_pane_id).and_then(|&id| { - self.bus - .os_input - .as_ref() - .and_then(|input| input.get_cwd(Pid::from_raw(id))) - }) + // Try to get CWD from OS, fall back to cached value + self.id_to_child_pid + .get(terminal_pane_id) + .and_then(|&pid| { + self.bus + .os_input + .as_ref() + .and_then(|input| input.get_cwd(Pid::from_raw(pid))) + }) + .or_else(|| self.terminal_cwds.get(terminal_pane_id).cloned()) }, PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), }; @@ -938,6 +986,8 @@ impl Pty { self.task_handles.insert(terminal_id, terminal_bytes); self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); + let starts_held = false; Ok((terminal_id, starts_held)) } @@ -948,7 +998,9 @@ impl Pty { floating_panes_layout: Vec<FloatingPaneLayout>, default_shell: Option<TerminalAction>, plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>, + initial_panes: Option<Vec<CommandOrPlugin>>, tab_index: usize, + block_on_first_terminal: bool, should_change_focus_to_new_tab: bool, client_id_and_is_web_client: (ClientId, bool), completion_tx: Option<NotificationEnd>, @@ -959,6 +1011,22 @@ impl Pty { default_shell.unwrap_or_else(|| self.get_default_terminal(cwd, None)); let (client_id, is_web_client) = client_id_and_is_web_client; self.fill_cwd(&mut default_shell, client_id); + + // Match initial_panes commands to empty slots in the layout + let mut layout = layout; + if let Some(ref initial_panes_vec) = initial_panes { + for initial_pane in initial_panes_vec.iter() { + if let CommandOrPlugin::Command(run_command_action) = initial_pane { + let run_command: RunCommand = run_command_action.clone().into(); + if !layout.replace_next_empty_slot_with_run(Run::Command(run_command)) { + log::warn!("More initial_panes provided than empty slots available"); + break; + } + } + // Skip CommandOrPlugin::Plugin entries (already handled by plugin thread) + } + } + let extracted_run_instructions = layout.extract_run_instructions(); let extracted_floating_run_instructions = floating_panes_layout .iter() @@ -968,24 +1036,55 @@ impl Pty { // starts_held, // run_command, // file_descriptor) + let mut new_floating_panes_pids: Vec<(u32, bool, Option<RunCommand>, Result<RawFd>)> = vec![]; // same // as // new_pane_pids + + let mut originating_plugins_to_inform = vec![]; + for run_instruction in extracted_run_instructions { + let originating_plugin = run_instruction.as_ref().and_then(|r| { + if let Run::Command(run_command) = r { + run_command.originating_plugin.clone() + } else { + None + } + }); + let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { + terminal_id = Some(new_pane_data.0); new_pane_pids.push(new_pane_data); } + if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) + { + originating_plugins_to_inform.push((terminal_id, originating_plugin)); + } } for run_instruction in extracted_floating_run_instructions { + let originating_plugin = run_instruction.as_ref().and_then(|r| { + if let Run::Command(run_command) = r { + run_command.originating_plugin.clone() + } else { + None + } + }); + let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { + terminal_id = Some(new_pane_data.0); new_floating_panes_pids.push(new_pane_data); } + if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) + { + originating_plugins_to_inform.push((terminal_id, originating_plugin)); + } } + // Option<RunCommand> should only be Some if the pane starts held let new_tab_pane_ids: Vec<(u32, Option<RunCommand>)> = new_pane_pids .iter() @@ -1007,21 +1106,40 @@ impl Pty { } }) .collect(); + + // Track the first terminal_id if blocking is requested + let first_initial_pane_terminal_id = if block_on_first_terminal && !new_pane_pids.is_empty() + { + Some(new_pane_pids[0].0) + } else { + None + }; + + // Prepare blocking_terminal for ApplyLayout + let (direct_completion_tx, blocking_terminal) = + if let Some(terminal_id) = first_initial_pane_terminal_id { + (None, completion_tx.map(|tx| (terminal_id, tx))) + } else { + (completion_tx, None) + }; + self.bus .senders .send_to_screen(ScreenInstruction::ApplyLayout( layout, floating_panes_layout, - new_tab_pane_ids, - new_tab_floating_pane_ids, + new_tab_pane_ids.clone(), + new_tab_floating_pane_ids.clone(), plugin_ids, tab_index, should_change_focus_to_new_tab, (client_id, is_web_client), - completion_tx, + direct_completion_tx, + blocking_terminal, )) .with_context(err_context)?; let mut terminals_to_start = vec![]; + terminals_to_start.append(&mut new_pane_pids); terminals_to_start.append(&mut new_floating_panes_pids); for (terminal_id, starts_held, run_command, pid_primary) in terminals_to_start { @@ -1077,8 +1195,29 @@ impl Pty { }, } } + for (terminal_id, originating_plugin) in originating_plugins_to_inform { + self.inform_originating_plugin_of_open(terminal_id, originating_plugin); + } Ok(()) } + fn inform_originating_plugin_of_open( + &mut self, + terminal_id: u32, + originating_plugin: OriginatingPlugin, + ) { + self.originating_plugins + .insert(terminal_id, originating_plugin.clone()); + let update_event = Event::CommandPaneOpened(terminal_id, originating_plugin.context); + let _ = self + .bus + .senders + .send_to_plugin(PluginInstruction::Update(vec![( + Some(originating_plugin.plugin_id), + Some(originating_plugin.client_id), + update_event, + )])); + } + fn apply_run_instruction( &mut self, run_instruction: Option<Run>, @@ -1162,6 +1301,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some(( terminal_id, starts_held, @@ -1194,6 +1334,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some((terminal_id, starts_held, None, Ok(pid_primary)))) }, Err(err) => match err.downcast_ref::<ZellijError>() { @@ -1225,6 +1366,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some((terminal_id, starts_held, None, Ok(pid_primary)))) }, Err(err) => match err.downcast_ref::<ZellijError>() { @@ -1248,6 +1390,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some((terminal_id, starts_held, None, Ok(pid_primary)))) }, Err(err) => match err.downcast_ref::<ZellijError>() { @@ -1389,6 +1532,7 @@ impl Pty { self.task_handles.insert(id, terminal_bytes); self.id_to_child_pid.insert(id, child_fd); + self.capture_initial_cwd(id, child_fd); if let Some(originating_plugin) = self.originating_plugins.get(&id) { self.bus .senders @@ -1405,9 +1549,10 @@ impl Pty { } } pub fn populate_session_layout_metadata( - &self, + &mut self, session_layout_metadata: &mut SessionLayoutMetadata, ) { + log::info!("populate_session_layout_metadata"); let terminal_ids = session_layout_metadata.all_terminal_ids(); let mut terminal_ids_to_commands: HashMap<u32, Vec<String>> = HashMap::new(); let mut terminal_ids_to_cwds: HashMap<u32, PathBuf> = HashMap::new(); @@ -1476,12 +1621,18 @@ impl Pty { .get(&client_id) .and_then(|pane| match pane { PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), - PaneId::Terminal(id) => self.id_to_child_pid.get(id).and_then(|&id| { - self.bus - .os_input - .as_ref() - .and_then(|input| input.get_cwd(Pid::from_raw(id))) - }), + PaneId::Terminal(id) => { + // Try to get CWD from OS, fall back to cached value + self.id_to_child_pid + .get(id) + .and_then(|&pid| { + self.bus + .os_input + .as_ref() + .and_then(|input| input.get_cwd(Pid::from_raw(pid))) + }) + .or_else(|| self.terminal_cwds.get(id).cloned()) + }, }) }; @@ -1516,6 +1667,59 @@ impl Pty { ))?; Ok(()) } + fn capture_initial_cwd(&mut self, terminal_id: u32, child_fd: RawFd) { + if let Some(os_input) = self.bus.os_input.as_ref() { + if let Some(cwd) = os_input.get_cwd(Pid::from_raw(child_fd)) { + self.terminal_cwds.insert(terminal_id, cwd); + } + } + } + + pub fn update_and_report_cwds(&mut self) { + let terminal_ids: Vec<u32> = self.id_to_child_pid.keys().copied().collect(); + + let pids: Vec<_> = terminal_ids + .iter() + .filter_map(|id| self.id_to_child_pid.get(&id)) + .map(|pid| Pid::from_raw(*pid)) + .collect(); + + let (pids_to_cwds, _) = 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); + let cwd = process_id + .as_ref() + .and_then(|pid| pids_to_cwds.get(&Pid::from_raw(**pid))); + + if let Some(cwd) = cwd { + 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() + .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(), cwd.clone(), focused_client_ids), + )])); + } + self.terminal_cwds.insert(terminal_id, cwd.clone()); + } + } + } + pub fn reconfigure( &mut self, default_editor: Option<PathBuf>, @@ -1524,6 +1728,72 @@ impl Pty { self.default_editor = default_editor; self.post_command_discovery_hook = post_command_discovery_hook; } + + pub fn send_sigint_to_pane(&self, pane_id: PaneId) { + let err_context = || format!("failed to send SIGINT to pane {:?}", pane_id); + + match pane_id { + PaneId::Terminal(terminal_id) => { + if let Some(&child_fd) = self.id_to_child_pid.get(&terminal_id) { + let pid = Pid::from_raw(child_fd); + self.bus + .os_input + .as_ref() + .context("no OS I/O interface found") + .and_then(|os_input| os_input.send_sigint(pid)) + .with_context(err_context) + .non_fatal(); + } else { + log::warn!("Terminal pane {} not found or not running", terminal_id); + } + }, + PaneId::Plugin(plugin_id) => { + log::warn!("Cannot send SIGINT to plugin pane {}", plugin_id); + }, + } + } + + pub fn send_sigkill_to_pane(&self, pane_id: PaneId) { + let err_context = || format!("failed to send SIGKILL to pane {:?}", pane_id); + + match pane_id { + PaneId::Terminal(terminal_id) => { + if let Some(&child_fd) = self.id_to_child_pid.get(&terminal_id) { + let pid = Pid::from_raw(child_fd); + self.bus + .os_input + .as_ref() + .context("no OS I/O interface found") + .and_then(|os_input| os_input.force_kill(pid)) + .with_context(err_context) + .non_fatal(); + } else { + log::warn!("Terminal pane {} not found or not running", terminal_id); + } + }, + PaneId::Plugin(plugin_id) => { + log::warn!("Cannot send SIGKILL to plugin pane {}", plugin_id); + }, + } + } + + pub fn get_pane_pid(&self, pane_id: PaneId) -> GetPanePidResponse { + match pane_id { + PaneId::Terminal(terminal_id) => { + if let Some(&child_fd) = self.id_to_child_pid.get(&terminal_id) { + GetPanePidResponse::Ok(child_fd) + } else { + GetPanePidResponse::Err(format!( + "Terminal pane {} not found or not running", + terminal_id + )) + } + }, + PaneId::Plugin(plugin_id) => { + GetPanePidResponse::Err(format!("Cannot get PID for plugin pane {}", plugin_id)) + }, + } + } } impl Drop for Pty { diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 7d11ba65..ead4282d 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -19,7 +19,7 @@ use zellij_utils::{ channels::SenderWithContext, data::{ BareKey, ConnectToSession, Direction, Event, InputMode, KeyModifier, NewPanePlacement, - PluginCapabilities, ResizeStrategy, + PluginCapabilities, ResizeStrategy, UnblockCondition, }, envs, errors::prelude::*, @@ -39,21 +39,28 @@ use crate::ClientId; const ACTION_COMPLETION_TIMEOUT: Duration = Duration::from_secs(1); +#[derive(Debug, Clone)] +pub struct ActionCompletionResult { + pub exit_status: Option<i32>, + pub affected_pane_id: Option<PaneId>, +} + fn wait_for_action_completion( - receiver: oneshot::Receiver<Option<i32>>, // currently i32 signals an exit status, can be expanded as - // needed + receiver: oneshot::Receiver<ActionCompletionResult>, action_name: &str, wait_forever: bool, -) -> Option<i32> { +) -> ActionCompletionResult { let runtime = get_tokio_runtime(); if wait_forever { runtime.block_on(async { match receiver.await { - Ok(Some(payload)) => Some(payload), - Ok(None) => None, + Ok(result) => result, Err(e) => { log::error!("Failed to wait for action {}: {}", action_name, e); - None + ActionCompletionResult { + exit_status: None, + affected_pane_id: None, + } }, } }) @@ -61,15 +68,17 @@ fn wait_for_action_completion( match runtime .block_on(async { tokio::time::timeout(ACTION_COMPLETION_TIMEOUT, receiver).await }) { - Ok(Ok(Some(payload))) => Some(payload), - Ok(Ok(None)) => None, + Ok(Ok(result)) => result, Err(_) | Ok(Err(_)) => { log::error!( "Action {} did not complete within {:?} timeout", action_name, ACTION_COMPLETION_TIMEOUT ); - None + ActionCompletionResult { + exit_status: None, + affected_pane_id: None, + } }, } } @@ -83,8 +92,10 @@ fn wait_for_action_completion( // that it can be included in various other larger structs - DO NOT RELY ON CLONING IT! #[derive(Debug)] pub struct NotificationEnd { - channel: Option<oneshot::Sender<Option<i32>>>, + channel: Option<oneshot::Sender<ActionCompletionResult>>, exit_status: Option<i32>, + unblock_condition: Option<UnblockCondition>, + affected_pane_id: Option<PaneId>, // optional payload of the pane id affected by this action } impl Clone for NotificationEnd { @@ -93,26 +104,55 @@ impl Clone for NotificationEnd { NotificationEnd { channel: None, exit_status: self.exit_status, + unblock_condition: self.unblock_condition, + affected_pane_id: self.affected_pane_id, } } } impl NotificationEnd { - pub fn new(sender: oneshot::Sender<Option<i32>>) -> Self { + pub fn new(sender: oneshot::Sender<ActionCompletionResult>) -> Self { + NotificationEnd { + channel: Some(sender), + exit_status: None, + unblock_condition: None, + affected_pane_id: None, + } + } + + pub fn new_with_condition( + sender: oneshot::Sender<ActionCompletionResult>, + unblock_condition: UnblockCondition, + ) -> Self { NotificationEnd { channel: Some(sender), exit_status: None, + unblock_condition: Some(unblock_condition), + affected_pane_id: None, } } + pub fn set_exit_status(&mut self, exit_status: i32) { self.exit_status = Some(exit_status); } + + pub fn set_affected_pane_id(&mut self, pane_id: PaneId) { + self.affected_pane_id = Some(pane_id); + } + + pub fn unblock_condition(&self) -> Option<UnblockCondition> { + self.unblock_condition + } } impl Drop for NotificationEnd { fn drop(&mut self) { if let Some(tx) = self.channel.take() { - let _ = tx.send(self.exit_status); + let result = ActionCompletionResult { + exit_status: self.exit_status, + affected_pane_id: self.affected_pane_id, + }; + let _ = tx.send(result); } } } @@ -131,7 +171,7 @@ pub(crate) fn route_action( client_keybinds: Keybinds, default_mode: InputMode, os_input: Option<Box<dyn ServerOsApi>>, -) -> Result<bool> { +) -> Result<(bool, Option<ActionCompletionResult>)> { let mut should_break = false; let err_context = || format!("failed to route action for client {client_id}"); let action_name = action.to_string(); @@ -480,19 +520,51 @@ pub(crate) fn route_action( placement, pane_name, command, + unblock_condition, + near_current_pane, } => { let command = command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let set_pane_blocking = true; + + let notification_end = if let Some(condition) = unblock_condition { + Some(NotificationEnd::new_with_condition( + completion_tx, + condition, + )) + } else { + Some(NotificationEnd::new(completion_tx)) + }; + + // we prefer the pane id provided by the action explicitly over the one that originated + // it (this might be a bit misleading with "near_current_pane", but it's still the + // right behavior - in the latter case, if the originator does not wish for this + // behavior, they should not provide pane + // inside the placement, but rather have the current pane id be picked up instead) + let pane_id = match placement { + NewPanePlacement::Stacked(pane_id_to_stack_under) => { + pane_id_to_stack_under.map(|p| p.into()).or(pane_id) + }, + NewPanePlacement::InPlace { + pane_id_to_replace, .. + } => pane_id_to_replace.map(|p| p.into()).or(pane_id), + _ => pane_id, + }; + + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( command, pane_name, placement, false, - ClientTabIndexOrPaneId::ClientId(client_id), - Some(NotificationEnd::new(completion_tx)), + client_tab_index_or_paneid, + notification_end, set_pane_blocking, )) .with_context(err_context)?; @@ -505,19 +577,20 @@ pub(crate) fn route_action( in_place: should_open_in_place, start_suppressed, coordinates: floating_pane_coordinates, + near_current_pane, } => { let title = format!("Editing: {}", open_file_payload.path.display()); let open_file = TerminalAction::OpenFile(open_file_payload); let pty_instr = if should_open_in_place { match pane_id { - Some(pane_id) => PtyInstruction::SpawnInPlaceTerminal( + Some(pane_id) if near_current_pane => PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), false, ClientTabIndexOrPaneId::PaneId(pane_id), Some(NotificationEnd::new(completion_tx)), ), - None => PtyInstruction::SpawnInPlaceTerminal( + _ => PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), false, @@ -579,17 +652,23 @@ pub(crate) fn route_action( command: run_command, pane_name: name, coordinates: floating_pane_coordinates, + near_current_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Floating(floating_pane_coordinates), false, - ClientTabIndexOrPaneId::ClientId(client_id), + client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) @@ -598,28 +677,35 @@ pub(crate) fn route_action( Action::NewInPlacePane { command: run_command, pane_name: name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + let pane_id = match pane_id_to_replace { + Some(pane_id_to_replace) => pane_id_to_replace.try_into().ok(), + None => pane_id, + }; match pane_id { - Some(pane_id) => { + Some(pane_id) if near_current_pane => { senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( run_cmd, name, - false, + close_replace_pane, ClientTabIndexOrPaneId::PaneId(pane_id), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, - None => { + _ => { senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( run_cmd, name, - false, + close_replace_pane, ClientTabIndexOrPaneId::ClientId(client_id), Some(NotificationEnd::new(completion_tx)), )) @@ -630,12 +716,14 @@ pub(crate) fn route_action( Action::NewStackedPane { command: run_command, pane_name: name, + near_current_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + match pane_id { - Some(pane_id) => { + Some(pane_id) if near_current_pane => { senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, @@ -648,7 +736,7 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, - None => { + _ => { senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, @@ -667,17 +755,23 @@ pub(crate) fn route_action( direction, command: run_command, pane_name: name, + near_current_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Tiled(direction), false, - ClientTabIndexOrPaneId::ClientId(client_id), + client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) @@ -717,15 +811,23 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, - Action::Run { command } => { + Action::Run { + command, + near_current_pane, + } => { let run_cmd = Some(TerminalAction::RunCommand(command.clone().into())); + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, None, NewPanePlacement::Tiled(command.direction), false, - ClientTabIndexOrPaneId::ClientId(client_id), + client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) @@ -747,6 +849,8 @@ pub(crate) fn route_action( tab_name, should_change_focus_to_new_tab, cwd, + initial_panes, + first_pane_unblock_condition, } => { let shell = default_shell.clone(); let swap_tiled_layouts = @@ -755,6 +859,17 @@ pub(crate) fn route_action( .unwrap_or_else(|| default_layout.swap_floating_layouts.clone()); let is_web_client = false; // actions cannot be initiated directly from the web + // Construct completion_tx conditionally + let (completion_tx, block_on_first_terminal) = if let Some(condition) = + first_pane_unblock_condition + { + let notification = NotificationEnd::new_with_condition(completion_tx, condition); + wait_forever = true; + (notification, true) + } else { + (NotificationEnd::new(completion_tx), false) + }; + senders .send_to_screen(ScreenInstruction::NewTab( cwd, @@ -763,9 +878,11 @@ pub(crate) fn route_action( floating_panes_layout, tab_name, (swap_tiled_layouts, swap_floating_layouts), + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), - Some(NotificationEnd::new(completion_tx)), + Some(completion_tx), )) .with_context(err_context)?; }, @@ -921,20 +1038,12 @@ pub(crate) fn route_action( .with_context(err_context)?; }, Action::Confirm => { - senders - .send_to_screen(ScreenInstruction::ConfirmPrompt( - client_id, - Some(NotificationEnd::new(completion_tx)), - )) - .with_context(err_context)?; + // no-op, these are deprecated and should be removed when we upgrade the server/client + // contract }, Action::Deny => { - senders - .send_to_screen(ScreenInstruction::DenyPrompt( - client_id, - Some(NotificationEnd::new(completion_tx)), - )) - .with_context(err_context)?; + // no-op, these are deprecated and should be removed when we upgrade the server/client + // contract }, #[allow(clippy::single_match)] Action::SkipConfirm { action } => match *action { @@ -1147,11 +1256,13 @@ pub(crate) fn route_action( Action::FocusTerminalPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Terminal(pane_id), should_float_if_hidden, + should_be_in_place_if_hidden, client_id, Some(NotificationEnd::new(completion_tx)), )) @@ -1160,11 +1271,13 @@ pub(crate) fn route_action( Action::FocusPluginPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Plugin(pane_id), should_float_if_hidden, + should_be_in_place_if_hidden, client_id, Some(NotificationEnd::new(completion_tx)), )) @@ -1400,8 +1513,8 @@ pub(crate) fn route_action( .with_context(err_context)?; }, } - let exit_status = wait_for_action_completion(completion_rx, &action_name, wait_forever); - if let Some(exit_status) = exit_status { + let result = wait_for_action_completion(completion_rx, &action_name, wait_forever); + if let Some(exit_status) = result.exit_status { if let Some(cli_client_id) = cli_client_id { if let Some(os_input) = os_input { let _ = os_input.send_to_client( @@ -1413,14 +1526,14 @@ pub(crate) fn route_action( } } } - Ok(should_break) + Ok((should_break, Some(result))) } // this should only be used for one-off startup instructions macro_rules! send_to_screen_or_retry_queue { - ($rlocked_sessions:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{ - match $rlocked_sessions.as_ref() { - Some(session_metadata) => session_metadata.senders.send_to_screen($message), + ($senders:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{ + match $senders.as_ref() { + Some(senders) => senders.send_to_screen($message), None => { log::warn!("Server not ready, trying to place instruction in retry queue..."); if let Some(retry_queue) = $retry_queue.as_mut() { @@ -1455,8 +1568,11 @@ pub(crate) fn route_thread_main( >| -> Result<bool> { let mut should_break = false; - let rlocked_sessions = - session_data.read().to_anyhow().with_context(err_context)?; + let senders = session_data + .read() + .to_anyhow() + .ok() + .and_then(|r| r.as_ref().map(|r| r.senders.clone())); // Check if this is a watcher client and ignore input messages let is_watcher = session_state.read().unwrap().is_watcher(&client_id); @@ -1475,10 +1591,10 @@ pub(crate) fn route_thread_main( exit_reason: ExitReason::Normal, }, ); - let _ = rlocked_sessions.as_ref().map(|r| { - r.senders.send_to_screen( - ScreenInstruction::RemoveWatcherClient(client_id), - ) + let _ = senders.as_ref().map(|s| { + s.send_to_screen(ScreenInstruction::RemoveWatcherClient( + client_id, + )) }); should_break = true; } @@ -1487,7 +1603,7 @@ pub(crate) fn route_thread_main( // For watchers: send size to Screen for rendering adjustments, but // this does not affect the screen size send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::WatcherTerminalResize(client_id, *new_size), instruction.clone(), retry_queue @@ -1514,46 +1630,75 @@ pub(crate) fn route_thread_main( .unwrap() .set_last_active_client(client_id); - if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { - match rlocked_sessions.get_client_keybinds_and_mode(&client_id) { - Some((keybinds, input_mode, default_input_mode)) => { - for action in keybinds - .get_actions_for_key_in_mode_or_default_action( - &input_mode, - &key, - raw_bytes, - default_input_mode, - is_kitty_keyboard_protocol, - ) - { - if route_action( - action, + 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)) + { + if let Some(( + senders, + capabilities, + client_attributes, + default_shell, + layout, + client_input_mode, + )) = session_data_assets + { + for action in keybinds + .get_actions_for_key_in_mode_or_default_action( + &input_mode, + &key, + raw_bytes, + default_input_mode, + is_kitty_keyboard_protocol, + ) + { + // Send user input to plugin thread for logging + let _ = + senders.send_to_plugin(PluginInstruction::UserInput { client_id, - None, - None, - rlocked_sessions.senders.clone(), - rlocked_sessions.capabilities.clone(), - rlocked_sessions.client_attributes.clone(), - rlocked_sessions.default_shell.clone(), - rlocked_sessions.layout.clone(), - Some(&mut seen_cli_pipes), - keybinds.clone(), - rlocked_sessions - .session_configuration - .get_client_configuration(&client_id) - .options - .default_mode - .unwrap_or(InputMode::Normal) - .clone(), - Some(os_input.clone()), - )? { - should_break = true; - } + action: action.clone(), + terminal_id: None, + cli_client_id: None, + }); + + if route_action( + action, + client_id, + None, + None, + senders.clone(), + capabilities, + client_attributes.clone(), + default_shell.clone(), + layout.clone(), + Some(&mut seen_cli_pipes), + keybinds.clone(), + client_input_mode, + Some(os_input.clone()), + )? + .0 + { + should_break = true; } - }, - None => { - log::error!("Failed to get keybindings for client"); - }, + } } } }, @@ -1580,31 +1725,67 @@ pub(crate) fn route_thread_main( } else { maybe_client_id.unwrap_or(client_id) }; - if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { + + // Send user input to plugin thread for logging + if let Some(ref senders) = senders { + let _ = senders.send_to_plugin(PluginInstruction::UserInput { + client_id, + action: action.clone(), + terminal_id: maybe_pane_id, + cli_client_id: if is_cli_client { + Some(cli_client_id) + } else { + None + }, + }); + } + + 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(), + ) + }); + if let Some(( + senders, + capabilities, + client_attributes, + default_shell, + layout, + client_input_mode, + client_keybinds, + )) = session_data_assets + { if route_action( action, client_id, Some(cli_client_id), maybe_pane_id.map(|p| PaneId::Terminal(p)), - rlocked_sessions.senders.clone(), - rlocked_sessions.capabilities.clone(), - rlocked_sessions.client_attributes.clone(), - rlocked_sessions.default_shell.clone(), - rlocked_sessions.layout.clone(), + senders, + capabilities, + client_attributes, + default_shell, + layout, Some(&mut seen_cli_pipes), - rlocked_sessions - .session_configuration - .get_client_keybinds(&client_id) - .clone(), - rlocked_sessions - .session_configuration - .get_client_configuration(&client_id) - .options - .default_mode - .unwrap_or(InputMode::Normal) - .clone(), + client_keybinds, + client_input_mode, Some(os_input.clone()), - )? { + )? + .0 + { should_break = true; } } @@ -1614,7 +1795,7 @@ pub(crate) fn route_thread_main( if is_watcher { // For watchers: send size to Screen for tracking, don't affect screen size send_to_screen_or_retry_queue!( - rlocked_sessions, + senders.clone(), ScreenInstruction::WatcherTerminalResize(client_id, new_size), instruction, retry_queue @@ -1635,22 +1816,19 @@ pub(crate) fn route_thread_main( )) }) .and_then(|min_size| { - rlocked_sessions - .as_ref() - .context( - "couldn't get reference to read-locked session", - )? - .senders - .send_to_screen(ScreenInstruction::TerminalResize( + let _ = senders.as_ref().map(|s| { + s.send_to_screen(ScreenInstruction::TerminalResize( min_size, )) + }); + Ok(()) }) .with_context(err_context)?; } }, ClientToServerMsg::TerminalPixelDimensions { pixel_dimensions } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalPixelDimensions(pixel_dimensions), instruction, retry_queue @@ -1661,7 +1839,7 @@ pub(crate) fn route_thread_main( color: ref background_color_instruction, } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalBackgroundColor( background_color_instruction.clone() ), @@ -1674,7 +1852,7 @@ pub(crate) fn route_thread_main( color: ref foreground_color_instruction, } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalForegroundColor( foreground_color_instruction.clone() ), @@ -1687,7 +1865,7 @@ pub(crate) fn route_thread_main( ref color_registers, } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalColorRegisters( color_registers .iter() @@ -1718,10 +1896,11 @@ pub(crate) fn route_thread_main( pane_to_focus: pane_id_to_focus, is_web_client, } => { - let allow_web_connections = rlocked_sessions - .as_ref() - .map(|rlocked_sessions| { - rlocked_sessions.web_sharing.web_clients_allowed() + let allow_web_connections = session_data + .read() + .ok() + .and_then(|s| { + s.as_ref().map(|s| s.web_sharing.web_clients_allowed()) }) .unwrap_or(false); let should_allow_connection = !is_web_client || allow_web_connections; @@ -1801,7 +1980,9 @@ pub(crate) fn route_thread_main( }, None => { consecutive_unknown_messages_received += 1; - log::error!("Received unknown message from client."); + if consecutive_unknown_messages_received == 1 { + log::error!("Received unknown message from client."); + } if consecutive_unknown_messages_received >= 1000 { log::error!("Client sent over 1000 consecutive unknown messages, this is probably an infinite loop, logging client out"); let _ = os_input.send_to_client( diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 3c83d6b0..c8998831 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -12,9 +12,9 @@ use crate::route::NotificationEnd; use log::{debug, warn}; use zellij_utils::data::{ - Direction, FloatingPaneCoordinates, KeyWithModifier, NewPanePlacement, PaneContents, - PaneManifest, PaneScrollbackResponse, PluginPermission, Resize, ResizeStrategy, SessionInfo, - Styling, WebSharing, + CommandOrPlugin, Direction, FloatingPaneCoordinates, KeyWithModifier, NewPanePlacement, + PaneContents, PaneManifest, PaneScrollbackResponse, PluginPermission, Resize, ResizeStrategy, + SessionInfo, Styling, WebSharing, }; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; @@ -51,10 +51,7 @@ use crate::{ pty::{get_default_shell, ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, tab::{SuppressedPanes, Tab}, thread_bus::Bus, - ui::{ - loading_indication::LoadingIndication, - overlay::{Overlay, OverlayWindow}, - }, + ui::loading_indication::LoadingIndication, ClientId, ServerInstruction, }; use zellij_utils::{ @@ -218,6 +215,7 @@ pub enum ScreenInstruction { ToggleActiveTerminalFullscreen(ClientId, Option<NotificationEnd>), TogglePaneFrames(Option<NotificationEnd>), SetSelectable(PaneId, bool), + ShowPluginCursor(u32, ClientId, Option<(usize, usize)>), ClosePane( PaneId, Option<ClientId>, @@ -235,6 +233,8 @@ pub enum ScreenInstruction { Vec<FloatingPaneLayout>, Option<String>, (Vec<SwapTiledLayout>, Vec<SwapFloatingLayout>), // swap layouts + Option<Vec<CommandOrPlugin>>, // initial_panes + bool, // block_on_first_terminal bool, // should_change_focus_to_new_tab (ClientId, bool), // bool -> is_web_client Option<NotificationEnd>, // completion signal @@ -245,10 +245,11 @@ pub enum ScreenInstruction { Vec<(u32, HoldForCommand)>, // new pane pids Vec<(u32, HoldForCommand)>, // new floating pane pids HashMap<RunPluginOrAlias, Vec<u32>>, - usize, // tab_index - bool, // should change focus to new tab - (ClientId, bool), // bool -> is_web_client - Option<NotificationEnd>, // completion signal + usize, // tab_index + bool, // should change focus to new tab + (ClientId, bool), // bool -> is_web_client + Option<NotificationEnd>, // regular completion signal + Option<(u32, NotificationEnd)>, // blocking_terminal (terminal_id, completion_tx) ), SwitchTabNext(ClientId, Option<NotificationEnd>), SwitchTabPrev(ClientId, Option<NotificationEnd>), @@ -284,10 +285,6 @@ pub enum ScreenInstruction { Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus ), RemoveClient(ClientId), - AddOverlay(Overlay, ClientId), - RemoveOverlay(ClientId), - ConfirmPrompt(ClientId, Option<NotificationEnd>), - DenyPrompt(ClientId, Option<NotificationEnd>), UpdateSearch(Vec<u8>, ClientId, Option<NotificationEnd>), SearchDown(ClientId, Option<NotificationEnd>), SearchUp(ClientId, Option<NotificationEnd>), @@ -368,8 +365,12 @@ pub enum ScreenInstruction { ClientId, Option<NotificationEnd>, ), // bools are: should_float, should_open_in_place Option<PaneId> is the pane id to replace, Option<PathBuf> is an optional cwd, bool after is skip_cache - SuppressPane(PaneId, ClientId), // bool is should_float - FocusPaneWithId(PaneId, bool, ClientId, Option<NotificationEnd>), // bool is should_float + SuppressPane(PaneId, ClientId), + UnsuppressPane(PaneId, bool), // bool -> should float if hidden + UnsuppressOrExpandPane(PaneId, bool), // bool -> should float if hidden + FocusPaneWithId(PaneId, bool, bool, ClientId, Option<NotificationEnd>), // bools: + // should_float_if_hidden, + // should_be_in_place_if_hidden RenamePane(PaneId, Vec<u8>, Option<NotificationEnd>), RenameTab(usize, Vec<u8>, Option<NotificationEnd>), RequestPluginPermissions( @@ -422,6 +423,7 @@ pub enum ScreenInstruction { ResizePaneWithId(ResizeStrategy, PaneId), EditScrollbackForPaneWithId(PaneId, Option<NotificationEnd>), WriteToPaneId(Vec<u8>, PaneId), + CopyTextToClipboard(String, u32), // String - text to copy, u32 - plugin_id MovePaneWithPaneId(PaneId), MovePaneWithPaneIdInDirection(PaneId, Direction), ClearScreenForPaneId(PaneId), @@ -467,7 +469,7 @@ pub enum ScreenInstruction { SetMouseSelectionSupport(PaneId, bool), InterceptKeyPresses(PluginId, ClientId), ClearKeyPressesIntercepts(ClientId), - ReplacePaneWithExistingPane(PaneId, PaneId), + ReplacePaneWithExistingPane(PaneId, PaneId, bool), // bool -> suppress_replaced_pane AddWatcherClient(ClientId, Size), RemoveWatcherClient(ClientId), SetFollowedClient(ClientId), @@ -553,6 +555,7 @@ impl From<&ScreenInstruction> for ScreenContext { }, ScreenInstruction::TogglePaneFrames(..) => ScreenContext::TogglePaneFrames, ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, + ScreenInstruction::ShowPluginCursor(..) => ScreenContext::ShowPluginCursor, ScreenInstruction::ClosePane(..) => ScreenContext::ClosePane, ScreenInstruction::HoldPane(..) => ScreenContext::HoldPane, ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName, @@ -591,10 +594,6 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, ScreenInstruction::RemoveClient(..) => ScreenContext::RemoveClient, - ScreenInstruction::AddOverlay(..) => ScreenContext::AddOverlay, - ScreenInstruction::RemoveOverlay(..) => ScreenContext::RemoveOverlay, - ScreenInstruction::ConfirmPrompt(..) => ScreenContext::ConfirmPrompt, - ScreenInstruction::DenyPrompt(..) => ScreenContext::DenyPrompt, ScreenInstruction::UpdateSearch(..) => ScreenContext::UpdateSearch, ScreenInstruction::SearchDown(..) => ScreenContext::SearchDown, ScreenInstruction::SearchUp(..) => ScreenContext::SearchUp, @@ -633,6 +632,8 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin, ScreenInstruction::LaunchPlugin(..) => ScreenContext::LaunchPlugin, ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane, + ScreenInstruction::UnsuppressPane(..) => ScreenContext::UnsuppressPane, + ScreenInstruction::UnsuppressOrExpandPane(..) => ScreenContext::UnsuppressOrExpandPane, ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, @@ -657,6 +658,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenContext::EditScrollbackForPaneWithId }, ScreenInstruction::WriteToPaneId(..) => ScreenContext::WriteToPaneId, + ScreenInstruction::CopyTextToClipboard(..) => ScreenContext::CopyTextToClipboard, ScreenInstruction::MovePaneWithPaneId(..) => ScreenContext::MovePaneWithPaneId, ScreenInstruction::MovePaneWithPaneIdInDirection(..) => { ScreenContext::MovePaneWithPaneIdInDirection @@ -848,14 +850,13 @@ pub(crate) struct Screen { character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, stacked_resize: Rc<RefCell<bool>>, sixel_image_store: Rc<RefCell<SixelImageStore>>, - /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`] - overlay: OverlayWindow, terminal_emulator_colors: Rc<RefCell<Palette>>, terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>, connected_clients: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client /// The indices of this [`Screen`]'s active [`Tab`]s. active_tab_indices: BTreeMap<ClientId, usize>, tab_history: BTreeMap<ClientId, Vec<usize>>, + pane_history: BTreeMap<ClientId, Vec<PaneId>>, mode_info: BTreeMap<ClientId, ModeInfo>, default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication style: Style, @@ -942,10 +943,10 @@ impl Screen { connected_clients: Rc::new(RefCell::new(HashMap::new())), active_tab_indices: BTreeMap::new(), tabs: BTreeMap::new(), - overlay: OverlayWindow::default(), terminal_emulator_colors: Rc::new(RefCell::new(Palette::default())), terminal_emulator_color_codes: Rc::new(RefCell::new(HashMap::new())), tab_history: BTreeMap::new(), + pane_history: BTreeMap::new(), mode_info: BTreeMap::new(), default_mode_info: mode_info, draw_pane_frames, @@ -1644,11 +1645,6 @@ impl Screen { } } - /// Returns a mutable reference to this [`Screen`]'s active [`Overlays`]. - pub fn get_active_overlays_mut(&mut self) -> &mut Vec<Overlay> { - &mut self.overlay.overlay_stack - } - /// Returns a mutable reference to this [`Screen`]'s indexed [`Tab`]. pub fn get_indexed_tab_mut(&mut self, tab_index: usize) -> Option<&mut Tab> { self.get_tabs_mut().get_mut(&tab_index) @@ -1733,6 +1729,7 @@ impl Screen { tab_index: usize, should_change_client_focus: bool, client_id_and_is_web_client: (ClientId, bool), + blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Result<()> { if self.tabs.get(&tab_index).is_none() { // TODO: we should prevent this situation with a UI - eg. cannot close tabs with a @@ -1817,6 +1814,7 @@ impl Screen { new_floating_terminal_ids, new_plugin_ids, client_id, + blocking_terminal, )?; tab.update_input_modes()?; @@ -1949,18 +1947,6 @@ impl Screen { } } - // Optional: getter for debugging/monitoring - pub fn get_watcher_size(&self, client_id: &ClientId) -> Option<Size> { - self.watcher_clients - .get(client_id) - .map(|state| state.size()) - } - - // Optional: get all watcher sizes - pub fn get_all_watcher_sizes(&self) -> &HashMap<ClientId, WatcherState> { - &self.watcher_clients - } - pub fn generate_and_report_tab_state(&mut self) -> Result<Vec<TabInfo>> { let mut plugin_updates = vec![]; let mut tab_infos_for_screen_state = BTreeMap::new(); @@ -2063,6 +2049,8 @@ impl Screen { } fn log_and_report_session_state(&mut self) -> Result<()> { let err_context = || format!("Failed to log and report session state"); + + self.update_active_pane_ids(); // generate own session info let pane_manifest = self.generate_and_report_pane_state()?; let tab_infos = self.generate_and_report_tab_state()?; @@ -2090,6 +2078,11 @@ impl Screen { .count(), plugins: Default::default(), // these are filled in by the wasm thread tab_history: self.tab_history.clone(), + pane_history: self + .pane_history + .iter() + .map(|(k, v)| (*k, v.iter().map(|v| (*v).into()).collect())) + .collect(), }; self.bus .senders @@ -2496,6 +2489,7 @@ impl Screen { run_plugin: &RunPluginOrAlias, should_float: bool, move_to_focused_tab: bool, + should_be_in_place: bool, client_id: ClientId, ) -> Result<bool> { // true => found and focused, false => not @@ -2526,6 +2520,7 @@ impl Screen { None, true, )?; + // TODO: also should_be_in_place } else { new_active_tab.hide_floating_panes(); new_active_tab.add_tiled_pane( @@ -2542,7 +2537,7 @@ impl Screen { self.tabs .get_mut(&tab_index) .with_context(err_context)? - .focus_pane_with_id(plugin_pane_id, should_float, client_id) + .focus_pane_with_id(plugin_pane_id, should_float, should_be_in_place, client_id) .context("failed to focus plugin pane")?; self.log_and_report_session_state() .with_context(err_context)?; @@ -2556,6 +2551,7 @@ impl Screen { &mut self, pane_id: PaneId, should_float_if_hidden: bool, + should_open_in_place: bool, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to focus_plugin_pane"); @@ -2570,7 +2566,14 @@ impl Screen { self.tabs .iter_mut() .find(|(_, t)| t.position == tab_index) - .map(|(_, t)| t.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)) + .map(|(_, t)| { + t.focus_pane_with_id( + pane_id, + should_float_if_hidden, + should_open_in_place, + client_id, + ) + }) .with_context(err_context) .non_fatal(); }, @@ -2666,6 +2669,8 @@ impl Screen { Some(tiled_panes_layout), floating_panes_layout, tab_index, + None, // initial_panes + false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), None, @@ -2742,6 +2747,8 @@ impl Screen { Some(tiled_panes_layout), floating_panes_layout, tab_index, + None, // initial_panes + false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), None, @@ -2942,6 +2949,7 @@ impl Screen { &mut self, pane_id_to_replace: PaneId, pane_id_of_existing_pane: PaneId, + suppress_replaced_pane: bool, ) { let Some(tab_index_of_pane_id_to_replace) = self .tabs @@ -2965,7 +2973,7 @@ impl Screen { .tabs .iter_mut() .find(|(_, t)| t.position == tab_index_of_existing_pane) - .and_then(|(_, t)| t.extract_pane(pane_id_of_existing_pane, false)) + .and_then(|(_, t)| t.extract_pane(pane_id_of_existing_pane, true)) else { log::error!("Failed to find pane"); return; @@ -2975,11 +2983,19 @@ impl Screen { .iter_mut() .find(|(_, t)| t.position == tab_index_of_pane_id_to_replace) { - tab.1.close_pane_and_replace_with_other_pane( - pane_id_to_replace, - extracted_pane_from_other_tab, - None, - ); + if suppress_replaced_pane { + tab.1.suppress_pane_and_replace_with_other_pane( + pane_id_to_replace, + extracted_pane_from_other_tab, + None, + ); + } else { + tab.1.close_pane_and_replace_with_other_pane( + pane_id_to_replace, + extracted_pane_from_other_tab, + None, + ); + } } let _ = self.log_and_report_session_state(); } @@ -3505,6 +3521,18 @@ impl Screen { } } } + fn update_active_pane_ids(&mut self) { + let connected_clients: Vec<ClientId> = + self.connected_clients.borrow().keys().copied().collect(); + for client_id in connected_clients { + if let Some(active_pane_id) = self.get_active_pane_id(&client_id) { + let active_pane_id: PaneId = active_pane_id.into(); + let history = self.pane_history.entry(client_id).or_insert_with(|| vec![]); + history.retain(|e| e != &active_pane_id); + history.push(active_pane_id.into()); + } + } + } } #[cfg(not(test))] @@ -3692,9 +3720,11 @@ pub(crate) fn screen_thread_main( new_pane_placement, start_suppressed, client_or_tab_index, - completion_tx, + mut completion_tx, set_blocking, ) => { + completion_tx.as_mut().map(|c| c.set_affected_pane_id(pid)); + let blocking_notification = if set_blocking { completion_tx } else { None }; match client_or_tab_index { @@ -3747,6 +3777,7 @@ pub(crate) fn screen_thread_main( ClientTabIndexOrPaneId::PaneId(pane_id) => { let mut found = false; let all_tabs = screen.get_tabs_mut(); + let should_focus_pane = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.new_pane( @@ -3754,7 +3785,7 @@ pub(crate) fn screen_thread_main( initial_pane_title, invoked_with, start_suppressed, - true, + should_focus_pane, new_pane_placement, None, blocking_notification, // TODO: is this correct? @@ -4426,6 +4457,26 @@ pub(crate) fn screen_thread_main( screen.render(None)?; screen.log_and_report_session_state()?; }, + ScreenInstruction::ShowPluginCursor(pid, client_id, cursor_position) => { + let all_tabs = screen.get_tabs_mut(); + let mut found_plugin = false; + for tab in all_tabs.values_mut() { + if tab.has_plugin(pid) { + tab.show_plugin_cursor(pid, client_id, cursor_position); + found_plugin = true; + break; + } + } + if !found_plugin { + pending_events_waiting_for_tab.push(ScreenInstruction::ShowPluginCursor( + pid, + client_id, + cursor_position, + )); + } + screen.render(None)?; + screen.log_and_report_session_state()?; + }, ScreenInstruction::SetMouseSelectionSupport(pid, selection_support) => { let all_tabs = screen.get_tabs_mut(); let mut found_plugin = false; @@ -4564,6 +4615,8 @@ pub(crate) fn screen_thread_main( floating_panes_layout, tab_name, swap_layouts, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -4590,6 +4643,8 @@ pub(crate) fn screen_thread_main( layout, floating_panes_layout, tab_index, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -4604,9 +4659,14 @@ pub(crate) fn screen_thread_main( tab_index, should_change_focus_to_new_tab, (client_id, is_web_client), - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it + mut completion_tx, + blocking_terminal, ) => { + if let Some(first_terminal_pane) = new_pane_pids.iter().next() { + completion_tx + .as_mut() + .map(|c| c.set_affected_pane_id(PaneId::Terminal(first_terminal_pane.0))); + } screen.apply_layout( layout, floating_panes_layout, @@ -4616,6 +4676,7 @@ pub(crate) fn screen_thread_main( tab_index, should_change_focus_to_new_tab, (client_id, is_web_client), + blocking_terminal, )?; pending_tab_ids.remove(&tab_index); if pending_tab_ids.is_empty() { @@ -4749,6 +4810,8 @@ pub(crate) fn screen_thread_main( None, vec![], tab_index, + None, // initial_panes + false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -4885,7 +4948,7 @@ pub(crate) fn screen_thread_main( } }); if let Some(pane_id) = pane_id { - screen.focus_pane_with_id(pane_id, true, client_id)?; + screen.focus_pane_with_id(pane_id, true, false, client_id)?; } else if let Some(tab_position_to_focus) = tab_position_to_focus { screen.go_to_tab(tab_position_to_focus, client_id)?; } @@ -4915,37 +4978,6 @@ pub(crate) fn screen_thread_main( screen.log_and_report_session_state()?; screen.render(None)?; }, - ScreenInstruction::AddOverlay(overlay, _client_id) => { - screen.get_active_overlays_mut().pop(); - screen.get_active_overlays_mut().push(overlay); - }, - ScreenInstruction::RemoveOverlay(_client_id) => { - screen.get_active_overlays_mut().pop(); - screen.render(None)?; - }, - ScreenInstruction::ConfirmPrompt( - _client_id, - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it - ) => { - let overlay = screen.get_active_overlays_mut().pop(); - let instruction = overlay.and_then(|o| o.prompt_confirm()); - if let Some(instruction) = instruction { - screen - .bus - .senders - .send_to_server(*instruction) - .context("failed to confirm prompt")?; - } - }, - ScreenInstruction::DenyPrompt( - _client_id, - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it - ) => { - screen.get_active_overlays_mut().pop(); - screen.render(None)?; - }, ScreenInstruction::UpdateSearch( c, client_id, @@ -5441,6 +5473,7 @@ pub(crate) fn screen_thread_main( &run_plugin, should_float, move_to_focused_tab, + should_open_in_place, client_id, )? { screen.render(None)?; @@ -5560,14 +5593,42 @@ pub(crate) fn screen_thread_main( } screen.log_and_report_session_state()?; }, + ScreenInstruction::UnsuppressPane(pane_id, should_float_if_hidden) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.unsuppress_pane(pane_id, should_float_if_hidden); + drop(screen.render(None)); + break; + } + } + screen.log_and_report_session_state()?; + }, + ScreenInstruction::UnsuppressOrExpandPane(pane_id, should_float_if_hidden) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.unsuppress_or_expand_pane(pane_id, should_float_if_hidden); + drop(screen.render(None)); + break; + } + } + screen.log_and_report_session_state()?; + }, ScreenInstruction::FocusPaneWithId( pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { - screen.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)?; + screen.focus_pane_with_id( + pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + client_id, + )?; screen.log_and_report_session_state()?; }, ScreenInstruction::RenamePane( @@ -5655,9 +5716,11 @@ pub(crate) fn screen_thread_main( invoked_with, close_replaced_pane, client_id_tab_index_or_pane_id, - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it + mut completion_tx, ) => { + completion_tx + .as_mut() + .map(|c| c.set_affected_pane_id(new_pane_id)); screen.replace_pane( new_pane_id, hold_for_command, @@ -5822,6 +5885,24 @@ pub(crate) fn screen_thread_main( } screen.render(None)?; }, + ScreenInstruction::CopyTextToClipboard(text, plugin_id) => { + let plugin_pane_id = PaneId::Plugin(plugin_id); + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&plugin_pane_id) { + tab.copy_text_to_clipboard(&text) + .with_context(|| { + format!( + "failed to copy text to clipboard from plugin {}", + plugin_id + ) + }) + .non_fatal(); + break; + } + } + screen.render(None)?; + }, ScreenInstruction::MovePaneWithPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { @@ -6053,7 +6134,7 @@ pub(crate) fn screen_thread_main( // waiting for it ) => { if let Some(last_pane_id) = screen.stack_panes(pane_ids_to_stack) { - let _ = screen.focus_pane_with_id(last_pane_id, false, client_id); + let _ = screen.focus_pane_with_id(last_pane_id, false, false, client_id); let _ = screen.render(None); let pane_group = screen.get_client_pane_group(&client_id); if !pane_group.is_empty() { @@ -6213,9 +6294,15 @@ pub(crate) fn screen_thread_main( ScreenInstruction::ClearKeyPressesIntercepts(client_id) => { keybind_intercepts.remove(&client_id); }, - ScreenInstruction::ReplacePaneWithExistingPane(old_pane_id, new_pane_id) => { - screen.replace_pane_with_existing_pane(old_pane_id, new_pane_id) - }, + ScreenInstruction::ReplacePaneWithExistingPane( + old_pane_id, + new_pane_id, + suppress_replaced_pane, + ) => screen.replace_pane_with_existing_pane( + old_pane_id, + new_pane_id, + suppress_replaced_pane, + ), ScreenInstruction::AddWatcherClient(client_id, size) => { screen .add_watcher_client(client_id) diff --git a/zellij-server/src/tab/layout_applier.rs b/zellij-server/src/tab/layout_applier.rs index 5e52888a..559dc5db 100644 --- a/zellij-server/src/tab/layout_applier.rs +++ b/zellij-server/src/tab/layout_applier.rs @@ -11,7 +11,7 @@ use crate::{ plugins::PluginInstruction, pty::PtyInstruction, thread_bus::ThreadSenders, - ClientId, + ClientId, NotificationEnd, }; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; @@ -42,6 +42,7 @@ pub struct LayoutApplier<'a> { arrow_fonts: bool, styled_underlines: bool, explicitly_disable_kitty_keyboard_protocol: bool, + blocking_terminal: Option<(u32, NotificationEnd)>, } impl<'a> LayoutApplier<'a> { @@ -65,6 +66,7 @@ impl<'a> LayoutApplier<'a> { arrow_fonts: bool, styled_underlines: bool, explicitly_disable_kitty_keyboard_protocol: bool, + blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Self { let viewport = viewport.clone(); let senders = senders.clone(); @@ -97,6 +99,7 @@ impl<'a> LayoutApplier<'a> { arrow_fonts, styled_underlines, explicitly_disable_kitty_keyboard_protocol, + blocking_terminal, } } pub fn apply_layout( @@ -496,6 +499,18 @@ impl<'a> LayoutApplier<'a> { Some(Run::Command(run_command)) => Some(run_command.to_string()), _ => None, }; + + // Check if this terminal should receive the blocking completion_tx + let notification_end = if let Some((blocking_pid, _)) = &self.blocking_terminal { + if *blocking_pid == pid { + self.blocking_terminal.take().map(|(_, tx)| tx) + } else { + None + } + } else { + None + }; + let mut new_pane = TerminalPane::new( pid, *position_and_size, @@ -513,7 +528,7 @@ impl<'a> LayoutApplier<'a> { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, - None, + notification_end, ); if let Some(pane_initial_contents) = &layout.pane_initial_contents { new_pane.handle_pty_bytes(pane_initial_contents.as_bytes().into()); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index cd87b817..7e441a0e 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -7,7 +7,6 @@ mod layout_applier; mod swap_layouts; use copy_command::CopyCommand; -use serde; use std::env::temp_dir; use std::net::IpAddr; use std::path::PathBuf; @@ -46,7 +45,6 @@ use crate::{ thread_bus::ThreadSenders, ClientId, ServerInstruction, }; -use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::rc::Rc; use std::time::Instant; @@ -279,16 +277,6 @@ pub(crate) struct Tab { web_server_port: u16, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(crate = "self::serde")] -pub(crate) struct TabData { - pub position: usize, - pub name: String, - pub active: bool, - pub mode_info: ModeInfo, - pub colors: Styling, -} - // FIXME: Use a struct that has a pane_type enum, to reduce all of the duplication pub trait Pane { fn x(&self) -> usize; @@ -304,7 +292,8 @@ pub trait Pane { fn set_geom_override(&mut self, pane_geom: PaneGeom); fn handle_pty_bytes(&mut self, _bytes: VteBytes) {} fn handle_plugin_bytes(&mut self, _client_id: ClientId, _bytes: VteBytes) {} - fn cursor_coordinates(&self) -> Option<(usize, usize)>; + fn show_cursor(&mut self, _client_id: ClientId, _cursor_position: Option<(usize, usize)>) {} + fn cursor_coordinates(&self, _client_id: Option<ClientId>) -> Option<(usize, usize)>; fn is_mid_frame(&self) -> bool { false } @@ -818,6 +807,7 @@ impl Tab { new_floating_terminal_ids: Vec<(u32, HoldForCommand)>, new_plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>, client_id: ClientId, + blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Result<()> { self.swap_layouts .set_base_layout((layout.clone(), floating_panes_layout.clone())); @@ -841,6 +831,7 @@ impl Tab { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, + blocking_terminal, ) .apply_layout( layout, @@ -911,6 +902,7 @@ impl Tab { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, + None, ) .apply_floating_panes_layout_to_existing_panes(&layout_candidate) .non_fatal(); @@ -949,6 +941,7 @@ impl Tab { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, + None, ) .apply_tiled_panes_layout_to_existing_panes(&layout_candidate); if application_res.is_err() { @@ -2080,6 +2073,26 @@ impl Tab { drop(replaced_pane); } } + pub fn suppress_pane_and_replace_with_other_pane( + &mut self, + pane_id_to_replace: PaneId, + pane_to_replace_with: Box<dyn Pane>, + _completion_tx: Option<NotificationEnd>, + ) { + let mut replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) { + self.floating_panes + .replace_pane(pane_id_to_replace, pane_to_replace_with) + .ok() + } else { + self.tiled_panes + .replace_pane(pane_id_to_replace, pane_to_replace_with) + }; + + if let Some(replaced_pane) = replaced_pane.take() { + let is_scrollback_editor = false; + self.insert_suppressed_pane(replaced_pane.pid(), (is_scrollback_editor, replaced_pane)); + } + } pub fn horizontal_split( &mut self, pid: PaneId, @@ -2683,7 +2696,7 @@ impl Tab { .get(&active_pane_id) .or_else(|| self.tiled_panes.get_pane(active_pane_id))?; active_terminal - .cursor_coordinates() + .cursor_coordinates(Some(client_id)) .map(|(x_in_terminal, y_in_terminal)| { let x = active_terminal.x() + x_in_terminal; let y = active_terminal.y() + y_in_terminal; @@ -3381,21 +3394,16 @@ impl Tab { if let Some(pane) = self.tiled_panes.get_pane_mut(id) { pane.set_selectable(selectable); if !selectable { - // there are some edge cases in which this causes a hard crash when there are no - // other selectable panes - ideally this should never happen unless it's a - // configuration error - but this *does* sometimes happen with the default - // configuration as well since we set this at run time. I left this here because - // this should very rarely happen and I hope in my heart that we will stop setting - // this at runtime in the default configuration at some point - // - // If however this is not the case and we find this does cause crashes, we can - // solve it by adding a "dangling_clients" struct to Tab which we would fill with - // the relevant client ids in this case and drain as soon as a new selectable pane - // is opened self.tiled_panes.move_clients_out_of_pane(id); } } else if let Some(pane) = self.floating_panes.get_pane_mut(id) { pane.set_selectable(selectable); + if !selectable { + self.floating_panes.move_clients_out_of_pane(id); + if !self.floating_panes.has_selectable_panes() { + self.hide_floating_panes(); + } + } } // we do this here because if there is a non-selectable pane on the edge, we consider it // outside the viewport (a ui-pane, eg. the status-bar and tab-bar) and need to adjust for it @@ -3539,8 +3547,15 @@ impl Tab { closed_pane.reset_logical_position(); } closed_pane - } else if self.suppressed_panes.contains_key(&id) { - self.suppressed_panes.remove(&id).map(|s_p| s_p.1) + } else if let Some(suppressed_key_of_pane) = self + .suppressed_panes + .iter() + .find_map(|(key, (_, pane))| if &pane.pid() == &id { Some(*key) } else { None }) + { + // TODO: test this (from the path in screen.rs focus_plugin_pane ~line 2519 + self.suppressed_panes + .remove(&suppressed_key_of_pane) + .map(|s_p| s_p.1) } else { None } @@ -4674,6 +4689,19 @@ impl Tab { } Ok(()) } + pub fn copy_text_to_clipboard(&self, text: &str) -> Result<()> { + self.write_selection_to_clipboard(text) + .with_context(|| format!("failed to write text to clipboard"))?; + self.senders + .send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()), + )])) + .with_context(|| "failed to inform plugins about clipboard copy") + .non_fatal(); + Ok(()) + } fn write_selection_to_clipboard(&self, selection: &str) -> Result<()> { let err_context = || format!("failed to write selection to clipboard: '{}'", selection); @@ -4950,6 +4978,26 @@ impl Tab { plugin_pane.update_loading_indication(loading_indication); } } + pub fn show_plugin_cursor( + &mut self, + pid: u32, + client_id: ClientId, + cursor_position: Option<(usize, usize)>, + ) { + if let Some(plugin_pane) = self + .tiled_panes + .get_pane_mut(PaneId::Plugin(pid)) + .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) + .map(|s_p| &mut s_p.1) + }) + { + plugin_pane.show_cursor(client_id, cursor_position); + } + } pub fn start_plugin_loading_indication( &mut self, pid: u32, @@ -5009,7 +5057,7 @@ impl Tab { .find(|(_id, (_, pane))| { run_plugin_or_alias.is_equivalent_to_run(pane.invoked_with()) }) - .map(|(id, _)| *id) + .map(|(_, (_, pane))| pane.pid()) // TODO: does this break things???? }) } @@ -5017,6 +5065,7 @@ impl Tab { &mut self, pane_id: PaneId, should_float: bool, + should_be_in_place: bool, client_id: ClientId, ) -> Result<()> { // TODO: should error if pane is not selectable @@ -5031,18 +5080,45 @@ impl Tab { }; focused_floating_pane }) - .or_else(|_| match self.suppressed_panes.remove(&pane_id) { - Some(mut pane) => { - pane.1.set_selectable(true); - if should_float { - self.show_floating_panes(); - self.add_floating_pane(pane.1, pane_id, None, true) - } else { - self.hide_floating_panes(); - self.add_tiled_pane(pane.1, pane_id, Some(client_id)) - } - }, - None => Ok(()), + // TODO: change suppressed_panes to be a proper struct with methods that make sense rather + // than doing this dance every time + .or_else(|_| { + match self + .suppressed_panes + .extract_if(|_key, (_, pane)| pane.pid() == pane_id) + .next() + .map(|(_key, (_, pane))| pane) + { + Some(mut pane) => { + pane.set_selectable(true); + if should_float { + self.show_floating_panes(); + self.add_floating_pane(pane, pane_id, None, true) + } else if should_be_in_place { + let replaced_pane = if self.are_floating_panes_visible() { + self.floating_panes + .replace_active_pane(pane, client_id) + .ok() + } else { + self.tiled_panes.replace_active_pane(pane, client_id) + }; + if let Some(replaced_pane) = replaced_pane { + let is_scrollback_editor = false; + self.insert_suppressed_pane( + pane_id, + (is_scrollback_editor, replaced_pane), + ); + } else { + log::error!("Could not find pane to replace, aborting."); + } + Ok(()) + } else { + self.hide_floating_panes(); + self.add_tiled_pane(pane, pane_id, Some(client_id)) + } + }, + None => Ok(()), + } }) } pub fn focus_suppressed_pane_for_all_clients(&mut self, pane_id: PaneId) { @@ -5064,11 +5140,101 @@ impl Tab { // scrollback editor), but it has to take itself out on its own (eg. a plugin using the // show_self() method) if let Some(pane) = self.extract_pane(pane_id, true) { - let is_scrollback_editor = false; - self.suppressed_panes - .insert(pane_id, (is_scrollback_editor, pane)); + self.insert_suppressed_pane(pane_id, (false, pane)); } } + pub fn unsuppress_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { + // removes a pane from being suppressed (hidden) but does not focus it + match self + .suppressed_panes + .extract_if(|_key, (_, pane)| pane.pid() == pane_id) + .next() + .map(|(_key, (_, pane))| pane) + { + Some(pane) => { + if should_float_if_hidden { + self.add_floating_pane(pane, pane_id, None, true) + .non_fatal(); + } else { + self.add_tiled_pane(pane, pane_id, None).non_fatal(); + } + }, + None => { + log::error!("Could not find suppressed pane with id: {:?}", pane_id); + }, + } + } + pub fn unsuppress_or_expand_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { + // removes a pane from being suppressed (hidden) but does not focus it + match self + .suppressed_panes + .extract_if(|_key, (_, pane)| pane.pid() == pane_id) + .next() + .map(|(_key, (_, pane))| pane) + { + Some(pane) => { + if should_float_if_hidden { + self.add_floating_pane(pane, pane_id, None, true) + .non_fatal(); + } else { + self.add_tiled_pane(pane, pane_id, None).non_fatal(); + } + }, + None => { + let expand_panes_success = self.tiled_panes.expand_pane_in_stack(pane_id).len() > 0; + if !expand_panes_success { + log::error!( + "Could not find suppressed or stacked pane with id: {:?}", + pane_id + ); + } + }, + } + } + fn insert_suppressed_pane( + &mut self, + suppressing_pane_id: PaneId, + pane_to_suppress: (bool, Box<dyn Pane>), + ) { + // bool -> is_scrollback_editor + // this method is intended to insert an existing provided pane into suppressed_panes, while + // making sure any existing panes already in suppressed_panes still remain there + // if there's a key collision (eg. the suppressing_pane_id already suppresses another + // pane), we fix the colliding pane so that it now becomes suppressed by its own id (i.e. + // needs to explicitly be removed from suppressed_panes by eg. a plugin action rather than + // being conditionally removed when its suppressing pane is closed or itself suppressed) + // + // - suppressing_pane_id: the id of the pane suppressing this pane - the pane that if + // closed, should trigger this pane being unsuppressed + // - pane_to_suppress: the pane itself that we want to suppress + + // this closure removes and returns an existing pane from the map if it exists under this key + // and inserts the given pane into the map under this key + let mut insert_and_return_existing_value = + |key: PaneId, value: (bool, Box<dyn Pane>)| -> Option<(bool, Box<dyn Pane>)> { + let existing = self.suppressed_panes.remove(&key); + self.suppressed_panes.insert(key, value); + existing + }; + + // here we try to insert a pane into the suppressed panes map while making sure not to drop + // (close) any existing panes in the map. We repeat the action of remapping existing panes + // under their own keys until all keys either reference the suppressing_pane_id (only one + // can do this) or themselves + let mut key_to_insert = suppressing_pane_id; + let mut pane_to_insert = Some(pane_to_suppress); + loop { + pane_to_insert = pane_to_insert + .take() + .and_then(|p| insert_and_return_existing_value(key_to_insert, p)); + if let Some(pane_to_insert) = pane_to_insert.as_ref() { + key_to_insert = pane_to_insert.1.pid(); + } else { + break; + } + } + } + pub fn pane_infos(&self) -> Vec<PaneInfo> { let mut pane_info = vec![]; let current_pane_group = { self.current_pane_group.borrow().clone_inner() }; @@ -5076,9 +5242,11 @@ impl Tab { let mut floating_pane_info = self.floating_panes.pane_info(¤t_pane_group); pane_info.append(&mut tiled_pane_info); pane_info.append(&mut floating_pane_info); - for (pane_id, (_is_scrollback_editor, pane)) in self.suppressed_panes.iter() { + for (_pane_id_of_suppressing_pane, (_is_scrollback_editor, pane)) in + self.suppressed_panes.iter() + { let mut pane_info_for_suppressed_pane = - pane_info_for_pane(pane_id, pane, ¤t_pane_group); + pane_info_for_pane(&pane.pid(), pane, ¤t_pane_group); pane_info_for_suppressed_pane.is_floating = false; pane_info_for_suppressed_pane.is_suppressed = true; pane_info_for_suppressed_pane.is_focused = false; @@ -5364,7 +5532,7 @@ impl Tab { } pub fn add_suppressed_panes(&mut self, mut suppressed_panes: SuppressedPanes) { for (pane_id, suppressed_pane_entry) in suppressed_panes.drain() { - self.suppressed_panes.insert(pane_id, suppressed_pane_entry); + self.insert_suppressed_pane(pane_id, suppressed_pane_entry); } } pub fn toggle_pane_pinned(&mut self, client_id: ClientId) { @@ -5531,7 +5699,7 @@ pub fn pane_info_for_pane( pane_info.pane_content_rows = pane.get_content_rows(); pane_info.pane_columns = pane.cols(); pane_info.pane_content_columns = pane.get_content_columns(); - pane_info.cursor_coordinates_in_pane = pane.cursor_coordinates(); + pane_info.cursor_coordinates_in_pane = pane.cursor_coordinates(None); pane_info.is_selectable = pane.selectable(); pane_info.title = pane.current_title(); pane_info.exited = pane.exited(); diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 029df49d..6eb39eae 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -138,6 +138,9 @@ impl ServerOsApi for FakeInputOutput { fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> { unimplemented!() } + fn send_sigint(&self, pid: Pid) -> Result<()> { + unimplemented!() + } } struct MockPtyInstructionBus { @@ -280,6 +283,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -361,6 +365,7 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -469,6 +474,7 @@ fn create_new_tab_with_swap_layouts( new_floating_terminal_ids, new_plugin_ids, client_id, + None, ) .unwrap(); tab @@ -554,6 +560,7 @@ fn create_new_tab_with_os_api( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -648,6 +655,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str) floating_pane_ids, HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -734,6 +742,7 @@ fn create_new_tab_with_mock_pty_writer( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -822,6 +831,7 @@ fn create_new_tab_with_sixel_support( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -1191,7 +1201,7 @@ fn render_stacks_without_pane_frames() { ) .unwrap(); } - tab.focus_pane_with_id(PaneId::Terminal(1), false, client_id); + tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); for i in 7..9 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( @@ -1206,7 +1216,7 @@ fn render_stacks_without_pane_frames() { ) .unwrap(); } - let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, client_id); + let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); for i in 9..11 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( @@ -1226,8 +1236,8 @@ fn render_stacks_without_pane_frames() { ResizeStrategy::new(Resize::Increase, Some(Direction::Right)), ) .unwrap(); - let _ = tab.focus_pane_with_id(PaneId::Terminal(7), false, client_id); - let _ = tab.focus_pane_with_id(PaneId::Terminal(5), false, client_id); + let _ = tab.focus_pane_with_id(PaneId::Terminal(7), false, false, client_id); + let _ = tab.focus_pane_with_id(PaneId::Terminal(5), false, false, client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index f5284b88..085f8211 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -106,6 +106,9 @@ impl ServerOsApi for FakeInputOutput { fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> { unimplemented!() } + fn send_sigint(&self, pid: Pid) -> Result<()> { + unimplemented!() + } } fn tab_resize_increase(tab: &mut Tab, id: ClientId) { @@ -220,6 +223,7 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab { vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -304,6 +308,7 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab { vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -386,6 +391,7 @@ fn create_new_tab_with_cell_size( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab diff --git a/zellij-server/src/ui/components/nested_list.rs b/zellij-server/src/ui/components/nested_list.rs index c2aa2860..b4b76b9b 100644 --- a/zellij-server/src/ui/components/nested_list.rs +++ b/zellij-server/src/ui/components/nested_list.rs @@ -49,6 +49,7 @@ pub fn nested_list( Some(padding + bulletin.len()), &coordinates, &style_declaration, + &style.colors, text_style.bold(Some(AnsiCode::On)), ); text = pad_line(text, max_width, padding, text_width); diff --git a/zellij-server/src/ui/components/ribbon.rs b/zellij-server/src/ui/components/ribbon.rs index 98b681a5..adfa3a79 100644 --- a/zellij-server/src/ui/components/ribbon.rs +++ b/zellij-server/src/ui/components/ribbon.rs @@ -34,6 +34,7 @@ pub fn ribbon( padding, &component_coordinates, &declaration, + &colors, text_style, ); let mut stringified = component_coordinates diff --git a/zellij-server/src/ui/components/table.rs b/zellij-server/src/ui/components/table.rs index 978b741e..80ab2a37 100644 --- a/zellij-server/src/ui/components/table.rs +++ b/zellij-server/src/ui/components/table.rs @@ -22,7 +22,8 @@ pub fn table( if is_too_high(row_index + 1, &coordinates) { break; } - for cell in row { + let cell_count = row.iter().len(); + for (cell_index, cell) in row.into_iter().enumerate() { let declaration = if is_title_row { style.colors.table_title } else { @@ -38,16 +39,21 @@ pub fn table( } else { CharacterStyles::from(declaration) }; + + // Default: bold + let text_style = text_style.bold(Some(AnsiCode::On)); + // here we intentionally don't pass our coordinates even if we have them, because // these cells have already been padded and truncated - let (text, _text_width) = stringify_text( - &cell, - None, - &None, - &declaration, - text_style.bold(Some(AnsiCode::On)), - ); - stringified.push_str(&format!("{}{} {}", text_style, text, RESET_STYLES)); + let (text, _text_width) = + stringify_text(&cell, None, &None, &declaration, &style.colors, text_style); + if cell_index == cell_count.saturating_sub(1) { + // do not add padding between columns for the last cell + stringified.push_str(&format!("{}{}{}", text_style, text, RESET_STYLES)); + } else { + // add padding between columns + stringified.push_str(&format!("{}{} {}", text_style, text, RESET_STYLES)); + } } let next_row_instruction = coordinates .as_ref() diff --git a/zellij-server/src/ui/components/text.rs b/zellij-server/src/ui/components/text.rs index c0088e60..eb8667f8 100644 --- a/zellij-server/src/ui/components/text.rs +++ b/zellij-server/src/ui/components/text.rs @@ -15,6 +15,7 @@ pub fn text(content: Text, style: &Style, component_coordinates: Option<Coordina style.colors.text_unselected }; + // Start with base style from declaration let base_text_style = CharacterStyles::from(declaration).bold(Some(AnsiCode::On)); let (text, _text_width) = stringify_text( @@ -22,6 +23,7 @@ pub fn text(content: Text, style: &Style, component_coordinates: Option<Coordina None, &component_coordinates, &declaration, + &style.colors, base_text_style, ); match component_coordinates { @@ -39,6 +41,7 @@ pub fn stringify_text( left_padding: Option<usize>, coordinates: &Option<Coordinates>, style: &StyleDeclaration, + styling: &zellij_utils::data::Styling, component_text_style: CharacterStyles, ) -> (String, usize) { let mut text_width = 0; @@ -48,6 +51,7 @@ pub fn stringify_text( } else { component_text_style }; + stringified.push_str(&format!("{}", base_text_style)); for (i, character) in text.text.chars().enumerate() { let character_width = character.width().unwrap_or(0); if is_too_wide( @@ -67,7 +71,7 @@ pub fn stringify_text( if !text.indices.is_empty() || text.selected || text.opaque { let character_with_styling = - color_index_character(character, i, &text, style, base_text_style); + color_index_character(character, i, &text, style, styling, base_text_style); stringified.push_str(&character_with_styling); } else { stringified.push(character) @@ -100,12 +104,32 @@ pub fn color_index_character( index: usize, text: &Text, declaration: &StyleDeclaration, + styling: &zellij_utils::data::Styling, base_text_style: CharacterStyles, ) -> String { - let character_style = text - .style_of_index(index, declaration) + let mut character_style = text + .style_of_index(index, declaration, styling) .map(|foreground_style| base_text_style.foreground(Some(foreground_style.into()))) .unwrap_or(base_text_style); + + // Apply dim and unbold per-character based on index levels 4 and 5 + if text.is_unbold_at(index) { + // Remove bold for this character + character_style = character_style.bold(Some(AnsiCode::Reset)); + } else if text.is_dimmed_at(index) { + // Apply dim for this character + character_style = character_style + .foreground(Some(AnsiCode::Reset)) // some terminals (eg. alacritty) do not support dimming non 16 + // colors, so we have to defer to the terminal's default here + .dim(Some(AnsiCode::On)); + } else { + character_style = character_style + .bold(Some(AnsiCode::On)) + .dim(Some(AnsiCode::Reset)); // default, to reset any + // possible dim/bold values + // from previous indices + } + format!("{}{}{}", character_style, character, base_text_style) } @@ -141,7 +165,46 @@ impl Text { } } - pub fn style_of_index(&self, index: usize, style: &StyleDeclaration) -> Option<PaletteColor> { + pub fn is_dimmed_at(&self, index: usize) -> bool { + const DIM_LEVEL: usize = 4; + self.indices + .get(DIM_LEVEL) + .map(|indices| indices.contains(&index)) + .unwrap_or(false) + } + + pub fn is_unbold_at(&self, index: usize) -> bool { + const UNBOLD_LEVEL: usize = 5; + self.indices + .get(UNBOLD_LEVEL) + .map(|indices| indices.contains(&index)) + .unwrap_or(false) + } + + pub fn style_of_index( + &self, + index: usize, + style: &StyleDeclaration, + styling: &zellij_utils::data::Styling, + ) -> Option<PaletteColor> { + const ERROR_COLOR_LEVEL: usize = 6; + const SUCCESS_COLOR_LEVEL: usize = 7; + + // Check error color first (highest precedence) + if let Some(indices) = self.indices.get(ERROR_COLOR_LEVEL) { + if indices.contains(&index) { + return Some(styling.exit_code_error.base); + } + } + + // Check success color (second highest precedence) + if let Some(indices) = self.indices.get(SUCCESS_COLOR_LEVEL) { + if indices.contains(&index) { + return Some(styling.exit_code_success.base); + } + } + + // Check regular emphasis levels (existing code) let index_variant_styles = [ style.emphasis_0, style.emphasis_1, diff --git a/zellij-server/src/ui/mod.rs b/zellij-server/src/ui/mod.rs index d9d5e3a0..1a17e743 100644 --- a/zellij-server/src/ui/mod.rs +++ b/zellij-server/src/ui/mod.rs @@ -1,6 +1,5 @@ pub mod boundaries; pub mod components; pub mod loading_indication; -pub mod overlay; pub mod pane_boundaries_frame; pub mod pane_contents_and_ui; diff --git a/zellij-server/src/ui/overlay/mod.rs b/zellij-server/src/ui/overlay/mod.rs deleted file mode 100644 index 7e3f821b..00000000 --- a/zellij-server/src/ui/overlay/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! This module handles the overlay's over the [`Screen`] -//! -//! They consist of: -//! -//! prompt's: -//! -//! notification's: - -pub mod prompt; - -use crate::ServerInstruction; -use zellij_utils::errors::prelude::*; -use zellij_utils::pane_size::Size; - -#[derive(Clone, Debug)] -pub struct Overlay { - pub overlay_type: OverlayType, -} - -pub trait Overlayable { - /// Generates vte_output that can be passed into - /// the `render()` function - fn generate_overlay(&self, size: Size) -> Result<String>; -} - -#[derive(Clone, Debug)] -pub enum OverlayType { - Prompt(prompt::Prompt), -} - -impl Overlayable for OverlayType { - fn generate_overlay(&self, size: Size) -> Result<String> { - match &self { - OverlayType::Prompt(prompt) => prompt - .generate_overlay(size) - .context("failed to generate VTE output from overlay type"), - } - } -} - -/// Entrypoint from [`Screen`], which holds the context in which -/// the overlays are being rendered. -/// The most recent overlays draw over the previous overlays. -#[derive(Clone, Debug, Default)] -pub struct OverlayWindow { - pub overlay_stack: Vec<Overlay>, -} - -impl Overlayable for OverlayWindow { - fn generate_overlay(&self, size: Size) -> Result<String> { - let mut output = String::new(); - //let clear_display = "\u{1b}[2J"; - //output.push_str(&clear_display); - for overlay in &self.overlay_stack { - let vte_output = overlay - .generate_overlay(size) - .context("failed to generate VTE output from overlay window")?; - output.push_str(&vte_output); - } - Ok(output) - } -} - -impl Overlay { - pub fn prompt_confirm(self) -> Option<Box<ServerInstruction>> { - match self.overlay_type { - OverlayType::Prompt(p) => p.confirm(), - } - } - pub fn prompt_deny(self) -> Option<Box<ServerInstruction>> { - match self.overlay_type { - OverlayType::Prompt(p) => p.deny(), - } - } -} - -impl Overlayable for Overlay { - fn generate_overlay(&self, size: Size) -> Result<String> { - self.overlay_type - .generate_overlay(size) - .context("failed to generate VTE output from overlay") - } -} - -impl Overlay { - pub fn new(overlay_type: OverlayType) -> Self { - Self { overlay_type } - } - - fn pad_cols(output: &mut String, cols: usize) { - if let Some(padding) = cols.checked_sub(output.len()) { - for _ in 0..padding { - output.push(' '); - } - } - } -} diff --git a/zellij-server/src/ui/overlay/prompt.rs b/zellij-server/src/ui/overlay/prompt.rs deleted file mode 100644 index 0cbfe351..00000000 --- a/zellij-server/src/ui/overlay/prompt.rs +++ /dev/null @@ -1,65 +0,0 @@ -use zellij_utils::pane_size::Size; - -use super::{Overlay, OverlayType, Overlayable}; -use crate::{ClientId, ServerInstruction}; -use zellij_utils::errors::prelude::*; - -use std::fmt::Write; - -#[derive(Clone, Debug)] -pub struct Prompt { - pub message: String, - on_confirm: Option<Box<ServerInstruction>>, - on_deny: Option<Box<ServerInstruction>>, -} - -impl Prompt { - pub fn new( - message: String, - on_confirm: Option<Box<ServerInstruction>>, - on_deny: Option<Box<ServerInstruction>>, - ) -> Self { - Self { - message, - on_confirm, - on_deny, - } - } - pub fn confirm(self) -> Option<Box<ServerInstruction>> { - self.on_confirm - } - pub fn deny(self) -> Option<Box<ServerInstruction>> { - self.on_deny - } -} - -impl Overlayable for Prompt { - fn generate_overlay(&self, size: Size) -> Result<String> { - let mut output = String::new(); - let rows = size.rows; - let mut vte_output = self.message.clone(); - Overlay::pad_cols(&mut vte_output, size.cols); - for (x, h) in vte_output.chars().enumerate() { - write!( - &mut output, - "\u{1b}[{};{}H\u{1b}[48;5;238m{}", - rows, - x + 1, - h, - ) - .context("failed to generate VTE output from prompt")?; - } - Ok(output) - } -} - -pub fn _generate_quit_prompt(client_id: ClientId) -> Overlay { - let prompt = Prompt::new( - (" Do you want to quit zellij? [Y]es / [N]o").to_string(), - Some(Box::new(ServerInstruction::ClientExit(client_id, None))), - None, - ); - Overlay { - overlay_type: OverlayType::Prompt(prompt), - } -} diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index c17a9b66..326dd96f 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -159,7 +159,7 @@ impl<'a> PaneContentsAndUi<'a> { ) { let cursor_is_visible = self .pane - .cursor_coordinates() + .cursor_coordinates(Some(*fake_cursor_client_id)) .map(|(x, y)| { self.output .cursor_is_visible(self.pane.x() + x, self.pane.y() + y) @@ -279,6 +279,7 @@ impl<'a> PaneContentsAndUi<'a> { .add_post_vte_instruction_to_client(client_id, &vte_output); } } + Ok(()) } pub fn render_pane_boundaries( diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 171096f7..6345d6b8 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -249,6 +249,9 @@ impl ServerOsApi for FakeInputOutput { fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> { unimplemented!() } + fn send_sigint(&self, pid: Pid) -> Result<()> { + unimplemented!() + } } fn create_new_screen(size: Size, advanced_mouse_actions: bool) -> Screen { @@ -402,6 +405,8 @@ impl MockScreen { initial_floating_panes_layout.clone(), tab_name, (vec![], vec![]), // swap layouts + None, // initial_panes + false, should_change_focus_to_new_tab, (self.main_client_id, false), None, @@ -416,6 +421,7 @@ impl MockScreen { true, (self.main_client_id, false), None, + None, )); self.last_opened_tab_index = Some(tab_index); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for the async render @@ -492,6 +498,8 @@ impl MockScreen { initial_floating_panes_layout.clone(), tab_name, (vec![], vec![]), // swap layouts + None, // initial_panes + false, should_change_focus_to_new_tab, (self.main_client_id, false), None, @@ -506,6 +514,7 @@ impl MockScreen { true, (self.main_client_id, false), None, + None, )); self.last_opened_tab_index = Some(tab_index); screen_thread @@ -528,6 +537,8 @@ impl MockScreen { vec![], // floating_panes_layout tab_name, (vec![], vec![]), // swap layouts + None, // initial_panes + false, should_change_focus_to_new_tab, (self.main_client_id, false), None, @@ -542,6 +553,7 @@ impl MockScreen { true, (self.main_client_id, false), None, + None, )); self.last_opened_tab_index = Some(tab_index); } @@ -756,6 +768,7 @@ fn new_tab(screen: &mut Screen, pid: u32, tab_index: usize) { tab_index, true, (client_id, false), + None, ) .expect("TEST"); } @@ -2638,6 +2651,8 @@ pub fn send_cli_new_pane_action_with_default_parameters() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2684,6 +2699,8 @@ pub fn send_cli_new_pane_action_with_split_direction() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2730,6 +2747,8 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2787,6 +2806,8 @@ pub fn send_cli_new_pane_action_with_floating_pane_and_coordinates() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2826,6 +2847,7 @@ pub fn send_cli_edit_action_with_default_parameters() { width: None, height: None, pinned: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2865,6 +2887,7 @@ pub fn send_cli_edit_action_with_line_number() { width: None, height: None, pinned: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2904,6 +2927,7 @@ pub fn send_cli_edit_action_with_split_direction() { width: None, height: None, pinned: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3089,6 +3113,13 @@ pub fn send_cli_new_tab_action_default_params() { layout: None, layout_dir: None, cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, }; send_cli_action_to_server(&session_metadata, new_tab_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -3129,6 +3160,13 @@ pub fn send_cli_new_tab_action_with_name_and_layout() { ))), layout_dir: None, cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, }; send_cli_action_to_server(&session_metadata, new_tab_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -3699,6 +3737,7 @@ pub fn screen_can_break_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(100)); // move back to make sure the other pane is in the previous tab @@ -3806,6 +3845,7 @@ pub fn screen_can_break_floating_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(200)); // move back to make sure the other pane is in the previous tab @@ -3880,6 +3920,7 @@ pub fn screen_can_break_plugin_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(100)); // move back to make sure the other pane is in the previous tab @@ -3958,6 +3999,7 @@ pub fn screen_can_break_floating_plugin_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(100)); // move back to make sure the other pane is in the previous tab diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap index e36f78b8..d322d6c1 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap index 73fec393..b21357e7 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap index c09ce65b..fc5e1af5 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(Some(Down)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(Some(Down)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap index ca824efe..2428bce0 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap @@ -36,6 +36,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap index de7e1ed5..fcb5c135 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", new_pane_instruction)" --- -Some(SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false)) +Some(SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false)) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap index 75f460d5..57aaa650 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap index 4d5b11c8..3c3d85e7 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Floating(Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None })), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Floating(Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None })), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap index 7ac623b8..2bdd6975 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap index 30640244..0bdf3e1d 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap @@ -59,6 +59,8 @@ Some( ), [], 0, + None, + false, true, ( 1, diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap index 1a0677e6..d61ae4d7 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap @@ -86,6 +86,8 @@ NewTab( ), [], 1, + None, + false, true, ( 10, @@ -95,6 +97,8 @@ NewTab( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap index ed703453..5c157762 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap @@ -13,6 +13,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), |
