diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-03-14 08:35:00 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-14 08:35:00 +0100 |
| commit | a1e224754759252778d8d3c30050e10bc9a82f84 (patch) | |
| tree | 326ed657bbbb34ed98006f6bbc36fd918fdb1b0a | |
| parent | Add `--pane-id` and `--tab-id` to all relevant CLI commands (#4846) (diff) | |
| download | sl0ppij-a1e224754759252778d8d3c30050e10bc9a82f84.tar.gz sl0ppij-a1e224754759252778d8d3c30050e10bc9a82f84.tar.xz sl0ppij-a1e224754759252778d8d3c30050e10bc9a82f84.zip | |
Ansi dump support (#4848)
* add missing pane-id and tab-id arguments to all cli actions
* rustfmt
* dump-screen --ansi
* edit-scrollback --ansi
* add --ansi to subscribe
* add tests for ansi additions
* add ansi render report to plugins
* rustfmt
* fix e2e tests
* add pr
43 files changed, 1298 insertions, 123 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d64743..bcf7a916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat: allow configuring focus_follows_mouse and mouse_click_through (https://github.com/zellij-org/zellij/pull/4836) * fix: handle very large writes to the pty (https://github.com/zellij-org/zellij/pull/4818) * feat: add --pane-id and --tab-id to all relevant CLI actions (https://github.com/zellij-org/zellij/pull/4846) +* feat: add --ansi flag to all relevant CLI/plugin-api screendump commands (https://github.com/zellij-org/zellij/pull/4848) ## [0.43.1] - 2025-08-08 diff --git a/default-plugins/compact-bar/src/action_types.rs b/default-plugins/compact-bar/src/action_types.rs index 8a4de277..626a3107 100644 --- a/default-plugins/compact-bar/src/action_types.rs +++ b/default-plugins/compact-bar/src/action_types.rs @@ -131,7 +131,7 @@ impl ActionType { Action::ToggleActiveSyncTab => ActionType::ToggleActiveSyncTab, Action::ToggleTab => ActionType::ToggleTab, Action::BreakPane => ActionType::BreakPane, - Action::EditScrollback => ActionType::EditScrollback, + Action::EditScrollback { .. } => ActionType::EditScrollback, Action::Detach => ActionType::Detach, Action::Quit => ActionType::Quit, action if action.launches_plugin("session-manager") => ActionType::SessionManager, diff --git a/default-plugins/compact-bar/src/keybind_utils.rs b/default-plugins/compact-bar/src/keybind_utils.rs index 2f84497c..77599d53 100644 --- a/default-plugins/compact-bar/src/keybind_utils.rs +++ b/default-plugins/compact-bar/src/keybind_utils.rs @@ -654,7 +654,7 @@ impl KeybindProcessor { } ) }, - |action: &Action| matches!(action, Action::EditScrollback), + |action: &Action| matches!(action, Action::EditScrollback { .. }), ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index 928d0d46..33558725 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -1463,7 +1463,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier (s("Scroll half page"), s("Scroll"), action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])), (s("Edit scrollback in default editor"), s("Edit"), - single_action_key(&km, &[Action::EditScrollback, TO_NORMAL])), + single_action_key(&km, &[Action::EditScrollback { ansi: false }, TO_NORMAL])), (s("Select pane"), s("Select"), to_basemode_key), ]} else if mi.mode == IM::EnterSearch { vec![ (s("When done"), s("Done"), action_key(&km, &[A::SwitchToMode{input_mode: IM::Search}])), diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 6231db4f..0ac127a4 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -229,7 +229,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier (s("Scroll half page"), s("Scroll"), action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])), (s("Edit scrollback in default editor"), s("Edit"), - action_key(&km, &[Action::EditScrollback, TO_NORMAL])), + action_key(&km, &[Action::EditScrollback { ansi: false }, TO_NORMAL])), (s("Select pane"), s("Select"), to_normal_key), ]} else if mi.mode == IM::EnterSearch { vec![ (s("When done"), s("Done"), action_key(&km, &[A::SwitchToMode{input_mode: IM::Search}])), diff --git a/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs b/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs index 2d858889..55905e43 100644 --- a/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs +++ b/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs @@ -75,7 +75,7 @@ fn add_keybinds<'a>(help: &'a ModeInfo) -> Vec<ANSIString<'a>> { let edit_buffer = action_key( &help.get_keybinds_for_mode(InputMode::Scroll), &[ - Action::EditScrollback, + Action::EditScrollback { ansi: false }, Action::SwitchToMode { input_mode: InputMode::Normal, }, diff --git a/zellij-client/src/cli_client.rs b/zellij-client/src/cli_client.rs index 909fa8a2..fa302368 100644 --- a/zellij-client/src/cli_client.rs +++ b/zellij-client/src/cli_client.rs @@ -275,6 +275,7 @@ pub fn start_subscribe_client( os_input.send_to_server(ClientToServerMsg::SubscribeToPaneRenders { pane_ids: pane_ids.clone(), scrollback: subscribe_cli.scrollback, + ansi: subscribe_cli.ansi, }); // Track remaining panes for exit-on-all-closed diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs index 46c1580b..8ab442fd 100644 --- a/zellij-server/src/output/mod.rs +++ b/zellij-server/src/output/mod.rs @@ -363,6 +363,7 @@ pub struct Output { styled_underlines: bool, osc8_hyperlinks: bool, pane_render_report: PaneRenderReport, + pub collect_ansi_pane_contents: bool, cursor_coordinates: Option<(usize, usize)>, } @@ -671,6 +672,18 @@ impl Output { self.pane_render_report .add_pane_contents(client_ids, pane_id.into(), pane_contents); } + pub fn add_pane_contents_with_ansi( + &mut self, + client_ids: &[ClientId], + pane_id: PaneId, + pane_contents: PaneContents, + ) { + self.pane_render_report.add_pane_contents_with_ansi( + client_ids, + pane_id.into(), + pane_contents, + ); + } pub fn drain_pane_render_report(&mut self) -> PaneRenderReport { let empty_pane_render_report = PaneRenderReport::default(); std::mem::replace(&mut self.pane_render_report, empty_pane_render_report) diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index ed0bb54c..ba7b61c5 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -408,6 +408,13 @@ impl FloatingPanes { pane.pid(), pane.pane_contents(None, false, None), ); + if output.collect_ansi_pane_contents { + output.add_pane_contents_with_ansi( + &connected_clients, + pane.pid(), + pane.pane_contents_with_ansi(None, false, None), + ); + } }, PaneId::Plugin(_) => { for client_id in &connected_clients { @@ -416,6 +423,13 @@ impl FloatingPanes { pane.pid(), pane.pane_contents(Some(*client_id), false, None), ); + if output.collect_ansi_pane_contents { + output.add_pane_contents_with_ansi( + &[*client_id], + pane.pid(), + pane.pane_contents_with_ansi(Some(*client_id), false, None), + ); + } } }, } diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 35b2d92b..f57150bf 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -3101,6 +3101,72 @@ impl Grid { PaneContents::new(viewport, self.selection.start, self.selection.end) } } + pub fn pane_contents_with_ansi( + &self, + get_full_scrollback: bool, + max_scrollback_lines: Option<usize>, + ) -> PaneContents { + use std::fmt::Write; + + let extract_row_with_ansi = |row: &Row| -> String { + let mut buf = String::new(); + let mut last_styles: Option<RcCharacterStyles> = None; + + let last_non_space = row + .columns + .iter() + .rposition(|tc| { + let space = tc.character == ' '; + let styled = !matches!(tc.styles.background, Some(AnsiCode::Reset) | None); + !space || styled + }) + .map(|i| i + 1) + .unwrap_or(0); + + for tc in row.columns.iter().take(last_non_space) { + if last_styles.as_ref() != Some(&tc.styles) { + write!(buf, "{}", tc.styles).unwrap(); + last_styles = Some(tc.styles.clone()); + } + buf.push(tc.character); + } + if last_styles.is_some() { + buf.push_str("\u{1b}[m"); + } + buf + }; + + let mut viewport: Vec<String> = Vec::with_capacity(self.viewport.len()); + for row in &self.viewport { + viewport.push(extract_row_with_ansi(row)); + } + + if get_full_scrollback { + let mut lines_above_viewport: Vec<String> = Vec::with_capacity(self.lines_above.len()); + for row in &self.lines_above { + lines_above_viewport.push(extract_row_with_ansi(row)); + } + if let Some(max) = max_scrollback_lines { + if max > 0 && lines_above_viewport.len() > max { + let start = lines_above_viewport.len() - max; + lines_above_viewport = lines_above_viewport.split_off(start); + } + } + let mut lines_below_viewport: Vec<String> = Vec::with_capacity(self.lines_below.len()); + for row in &self.lines_below { + lines_below_viewport.push(extract_row_with_ansi(row)); + } + PaneContents::new_with_scrollback( + viewport, + self.selection.start, + self.selection.end, + lines_above_viewport, + lines_below_viewport, + ) + } else { + PaneContents::new(viewport, self.selection.start, self.selection.end) + } + } } impl Perform for Grid { diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index c2960949..aeed38c7 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -892,6 +892,17 @@ impl Pane for PluginPane { .map(|g| g.pane_contents(get_full_scrollback, max_scrollback_lines)) .unwrap_or_else(Default::default) } + fn pane_contents_with_ansi( + &self, + client_id: Option<ClientId>, + get_full_scrollback: bool, + max_scrollback_lines: Option<usize>, + ) -> PaneContents { + client_id + .and_then(|c| self.grids.get(&c)) + .map(|g| g.pane_contents_with_ansi(get_full_scrollback, max_scrollback_lines)) + .unwrap_or_else(Default::default) + } } impl PluginPane { diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index d4b43297..97aa34d6 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -948,6 +948,15 @@ impl Pane for TerminalPane { self.grid .pane_contents(get_full_scrollback, max_scrollback_lines) } + fn pane_contents_with_ansi( + &self, + _client_id: Option<ClientId>, + get_full_scrollback: bool, + max_scrollback_lines: Option<usize>, + ) -> PaneContents { + self.grid + .pane_contents_with_ansi(get_full_scrollback, max_scrollback_lines) + } 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); diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 42bb4f0b..bb867c13 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -1048,6 +1048,13 @@ impl TiledPanes { pane.pid(), pane.pane_contents(None, false, None), ); + if output.collect_ansi_pane_contents { + output.add_pane_contents_with_ansi( + &connected_clients, + pane.pid(), + pane.pane_contents_with_ansi(None, false, None), + ); + } }, PaneId::Plugin(_) => { for client_id in &connected_clients { @@ -1056,6 +1063,13 @@ impl TiledPanes { pane.pid(), pane.pane_contents(Some(*client_id), false, None), ); + if output.collect_ansi_pane_contents { + output.add_pane_contents_with_ansi( + &[*client_id], + pane.pid(), + pane.pane_contents_with_ansi(Some(*client_id), false, None), + ); + } } }, } diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs index c482d802..17c74006 100644 --- a/zellij-server/src/panes/unit/grid_tests.rs +++ b/zellij-server/src/panes/unit/grid_tests.rs @@ -5026,6 +5026,128 @@ fn pane_contents_no_scrollback_when_flag_false() { } // ===================================================================== +// pane_contents_with_ansi Tests +// ===================================================================== + +fn create_grid_with_colored_scrollback() -> Grid { + let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); + let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); + let mut grid = Grid::new( + 5, + 40, + Rc::new(RefCell::new(Palette::default())), + terminal_emulator_color_codes, + Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), + sixel_image_store, + Style::default(), + false, + true, + true, + true, + false, + ); + let mut parser = vte::Parser::new(); + for i in 0..25 { + let line = format!("\x1b[31mred line {}\x1b[0m\r\n", i); + for byte in line.as_bytes() { + parser.advance(&mut grid, *byte); + } + } + grid +} + +#[test] +fn pane_contents_with_ansi_preserves_escape_codes() { + let grid = create_grid_with_colored_scrollback(); + let result = grid.pane_contents_with_ansi(false, None); + let has_ansi = result.viewport.iter().any(|line| line.contains("\x1b[")); + assert!( + has_ansi, + "pane_contents_with_ansi should preserve ANSI escape codes in viewport. Lines: {:?}", + result.viewport + ); +} + +#[test] +fn pane_contents_strips_escape_codes() { + let grid = create_grid_with_colored_scrollback(); + let result = grid.pane_contents(false, None); + let has_ansi = result.viewport.iter().any(|line| line.contains("\x1b[")); + assert!( + !has_ansi, + "pane_contents should strip ANSI escape codes from viewport. Lines: {:?}", + result.viewport + ); +} + +#[test] +fn pane_contents_with_ansi_scrollback_preserves_escape_codes() { + let grid = create_grid_with_colored_scrollback(); + let result = grid.pane_contents_with_ansi(true, None); + let has_ansi = result + .lines_above_viewport + .iter() + .any(|line| line.contains("\x1b[")); + assert!( + has_ansi, + "pane_contents_with_ansi should preserve ANSI escape codes in scrollback. Lines: {:?}", + result.lines_above_viewport + ); +} + +#[test] +fn pane_contents_with_ansi_scrollback_truncation() { + let grid = create_grid_with_colored_scrollback(); + let result = grid.pane_contents_with_ansi(true, Some(3)); + assert_eq!( + result.lines_above_viewport.len(), + 3, + "Should truncate to 3 scrollback lines" + ); + let all_have_ansi = result + .lines_above_viewport + .iter() + .all(|line| line.contains("\x1b[")); + assert!( + all_have_ansi, + "All truncated scrollback lines should contain ANSI codes. Lines: {:?}", + result.lines_above_viewport + ); +} + +#[test] +fn pane_contents_with_ansi_no_scrollback_when_flag_false() { + let grid = create_grid_with_colored_scrollback(); + let result = grid.pane_contents_with_ansi(false, Some(5)); + assert!( + result.lines_above_viewport.is_empty(), + "get_full_scrollback=false should never collect scrollback even with ansi" + ); +} + +#[test] +fn pane_contents_with_ansi_and_without_have_same_text() { + let grid = create_grid_with_colored_scrollback(); + let plain = grid.pane_contents(false, None); + let ansi = grid.pane_contents_with_ansi(false, None); + assert_eq!( + plain.viewport.len(), + ansi.viewport.len(), + "Both should have the same number of viewport lines" + ); + // Strip ANSI codes from the ansi version and compare plain text content + let ansi_escape = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + for (plain_line, ansi_line) in plain.viewport.iter().zip(ansi.viewport.iter()) { + let stripped = ansi_escape.replace_all(ansi_line, "").to_string(); + assert_eq!( + *plain_line, stripped, + "After stripping ANSI codes, text content should match" + ); + } +} + +// ===================================================================== // Highlight Layer Priority Tests // ===================================================================== diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 34fe5b72..c1b7f1c9 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -760,8 +760,7 @@ pub(crate) fn plugin_thread_main( wasm_bridge.update_plugins(updates, shutdown_send.clone())?; }, PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, _events) => { - // no-op, there used to be stuff we did here - now there isn't, but we might want - // to add stuff here in the future + wasm_bridge.notify_screen_of_ansi_subscription_change(); }, PluginInstruction::PermissionRequestResult( plugin_id, 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 ec0711fa..6bc552d1 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 @@ -5,6 +5,7 @@ expression: "format!(\"{:#?}\", new_tab_event)" Some( EditScrollback( 1, + false, Some( NotificationEnd { channel: None, diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 3e791ddc..d187ef49 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -506,6 +506,17 @@ impl WasmBridge { plugin_map.remove_plugins(pid).into_iter().collect() }; + // Check if any removed plugin was subscribed to ANSI pane render + let was_subscribed_to_ansi = + plugins_to_cleanup + .iter() + .any(|((_, _), (_, subscriptions, _))| { + subscriptions + .lock() + .unwrap() + .contains(&EventType::PaneRenderReportWithAnsi) + }); + // Schedule cleanup on each plugin's pinned thread for ((plugin_id, client_id), (running_plugin, subscriptions, workers)) in plugins_to_cleanup { @@ -590,6 +601,11 @@ impl WasmBridge { .senders .send_to_background_jobs(BackgroundJob::ReportPluginList(plugin_list)); + // If any unloaded plugin was subscribed to ANSI pane content, re-check remaining plugins + if was_subscribed_to_ansi { + self.notify_screen_of_ansi_subscription_change(); + } + Ok(()) } pub fn reload_plugin_with_id(&mut self, plugin_id: u32) -> Result<()> { @@ -1277,46 +1293,43 @@ impl WasmBridge { fn get_changed_panes_per_client( &self, - new_report: &PaneRenderReport, + new_contents: &HashMap<ClientId, HashMap<zellij_utils::data::PaneId, PaneContents>>, + previous_contents: Option< + &HashMap<ClientId, HashMap<zellij_utils::data::PaneId, PaneContents>>, + >, ) -> HashMap<ClientId, HashMap<zellij_utils::data::PaneId, PaneContents>> { let mut result: HashMap<ClientId, HashMap<zellij_utils::data::PaneId, PaneContents>> = HashMap::new(); // First report - return everything grouped by client - let Some(prev_report) = &self.previous_pane_render_report else { - for (client_id, panes) in &new_report.all_pane_contents { + let Some(prev_contents) = previous_contents else { + for (client_id, panes) in new_contents { result.insert(*client_id, panes.clone()); } return result; }; // Compare each client's panes - for (client_id, new_panes) in &new_report.all_pane_contents { + for (client_id, new_panes) in new_contents { let mut client_panes: HashMap<zellij_utils::data::PaneId, PaneContents> = HashMap::new(); - - for (pane_id, new_contents) in new_panes { - let has_changed = prev_report - .all_pane_contents + for (pane_id, new_pane_contents) in new_panes { + let has_changed = prev_contents .get(client_id) .and_then(|prev_panes| prev_panes.get(pane_id)) - .map(|prev_contents| { - // Check if viewport or selected_text changed - prev_contents.viewport != new_contents.viewport - || prev_contents.selected_text != new_contents.selected_text + .map(|prev_pane_contents| { + prev_pane_contents.viewport != new_pane_contents.viewport + || prev_pane_contents.selected_text != new_pane_contents.selected_text }) - .unwrap_or(true); // New pane - treat as changed - + .unwrap_or(true); if has_changed { - client_panes.insert(*pane_id, new_contents.clone()); + client_panes.insert(*pane_id, new_pane_contents.clone()); } } - if !client_panes.is_empty() { result.insert(*client_id, client_panes); } } - result } @@ -1325,15 +1338,59 @@ impl WasmBridge { pane_render_report: PaneRenderReport, shutdown_sender: Sender<()>, ) -> Result<()> { - let changed_panes_per_client = self.get_changed_panes_per_client(&pane_render_report); + // Plain content (existing behavior) + let changed_panes_per_client = self.get_changed_panes_per_client( + &pane_render_report.all_pane_contents, + self.previous_pane_render_report + .as_ref() + .map(|r| &r.all_pane_contents), + ); for (client_id, client_panes) in changed_panes_per_client { let updates = vec![(None, Some(client_id), Event::PaneRenderReport(client_panes))]; self.update_plugins(updates, shutdown_sender.clone())?; } + + // ANSI content (new behavior) + if !pane_render_report.all_pane_contents_with_ansi.is_empty() { + let changed_ansi_panes_per_client = self.get_changed_panes_per_client( + &pane_render_report.all_pane_contents_with_ansi, + self.previous_pane_render_report + .as_ref() + .map(|r| &r.all_pane_contents_with_ansi), + ); + for (client_id, client_panes) in changed_ansi_panes_per_client { + let updates = vec![( + None, + Some(client_id), + Event::PaneRenderReportWithAnsi(client_panes), + )]; + self.update_plugins(updates, shutdown_sender.clone())?; + } + } + self.previous_pane_render_report = Some(pane_render_report); Ok(()) } + pub fn notify_screen_of_ansi_subscription_change(&self) { + let any_plugin_needs_ansi = { + let mut plugin_map = self.plugin_map.lock().unwrap(); + plugin_map + .running_plugins_and_subscriptions() + .iter() + .any(|(_, _, _, subs)| { + subs.lock() + .unwrap() + .contains(&EventType::PaneRenderReportWithAnsi) + }) + }; + let _ = self + .senders + .send_to_screen(ScreenInstruction::PluginSubscribedToAnsiPaneContents( + any_plugin_needs_ansi, + )); + } + pub fn cleanup(&mut self) { self.loading_plugins.clear(); @@ -2021,7 +2078,9 @@ fn check_event_permission( | Event::HighlightClicked { .. } | Event::InputReceived => PermissionType::ReadApplicationState, Event::WebServerStatus(..) => PermissionType::StartWebServer, - Event::PaneRenderReport(..) => PermissionType::ReadPaneContents, + Event::PaneRenderReport(..) | Event::PaneRenderReportWithAnsi(..) => { + PermissionType::ReadPaneContents + }, Event::UserAction(..) => PermissionType::InterceptInput, _ => return (PermissionStatus::Granted, None), }; diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index d48a80b9..db63e1ae 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -817,6 +817,15 @@ fn unsubscribe(env: &PluginEnv, event_list: HashSet<EventType>) -> Result<()> { .lock() .to_anyhow()? .retain(|k| !event_list.contains(k)); + if event_list.contains(&EventType::PaneRenderReportWithAnsi) { + let _ = env + .senders + .send_to_plugin(PluginInstruction::PluginSubscribedToEvents( + env.plugin_id, + env.client_id, + HashSet::new(), // empty set signals a recheck, not a new subscription + )); + } Ok(()) } @@ -2882,7 +2891,7 @@ fn delete_all_dead_sessions() -> Result<()> { } fn edit_scrollback(env: &PluginEnv) { - let action = Action::EditScrollback; + let action = Action::EditScrollback { ansi: false }; let error_msg = || format!("Failed to edit scrollback"); apply_action!(action, error_msg, env); } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index d6b3b48e..7a2459f8 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -439,6 +439,7 @@ pub(crate) fn route_action( file_path, include_scrollback, pane_id, + ansi, } => { senders .send_to_screen(ScreenInstruction::DumpScreen( @@ -448,6 +449,7 @@ pub(crate) fn route_action( pane_id.map(|p| p.into()), Some(NotificationEnd::new(completion_tx)), cli_client_id, + ansi, )) .with_context(err_context)?; }, @@ -474,18 +476,20 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, - Action::EditScrollback => { + Action::EditScrollback { ansi } => { senders .send_to_screen(ScreenInstruction::EditScrollback( client_id, + ansi, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::EditScrollbackRaw => { senders - .send_to_screen(ScreenInstruction::EditScrollbackRaw( + .send_to_screen(ScreenInstruction::EditScrollback( client_id, + true, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; @@ -1911,10 +1915,11 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, - Action::EditScrollbackByPaneId { pane_id } => { + Action::EditScrollbackByPaneId { pane_id, ansi } => { senders .send_to_screen(ScreenInstruction::EditScrollbackWithPaneId( pane_id.into(), + ansi, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; @@ -2545,6 +2550,7 @@ pub(crate) fn route_thread_main( ClientToServerMsg::SubscribeToPaneRenders { ref pane_ids, ref scrollback, + ansi, } => { send_to_screen_or_retry_queue!( senders, @@ -2552,6 +2558,7 @@ pub(crate) fn route_thread_main( client_id, pane_ids: pane_ids.clone(), scrollback: *scrollback, + ansi, }, instruction, retry_queue diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 0b90c7f9..da9f67a6 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -307,6 +307,7 @@ pub enum ScreenInstruction { Option<PaneId>, Option<NotificationEnd>, Option<ClientId>, // cli_client_id - used to send output to the CLI client's STDOUT + bool, // ansi - preserve ANSI styling in the dump output ), DumpLayout(Option<PathBuf>, ClientId, Option<NotificationEnd>), // PathBuf is the default configured // shell @@ -328,8 +329,7 @@ pub enum ScreenInstruction { tab_id: usize, response_channel: crossbeam::channel::Sender<Option<TabInfo>>, }, - EditScrollback(ClientId, Option<NotificationEnd>), - EditScrollbackRaw(ClientId, Option<NotificationEnd>), + EditScrollback(ClientId, bool, Option<NotificationEnd>), GetPaneScrollback { pane_id: PaneId, client_id: ClientId, @@ -696,10 +696,12 @@ pub enum ScreenInstruction { client_id: ClientId, pane_ids: Vec<zellij_utils::data::PaneId>, scrollback: Option<usize>, + ansi: bool, }, NotifyPaneClosedToSubscribers { pane_id: zellij_utils::data::PaneId, }, + PluginSubscribedToAnsiPaneContents(bool), // true = at least one plugin needs ANSI content // Pane-targeting CLI variants ScrollUpWithPaneId(PaneId, Option<NotificationEnd>), ScrollDownWithPaneId(PaneId, Option<NotificationEnd>), @@ -713,7 +715,7 @@ pub enum ScreenInstruction { MovePaneWithPaneIdCli(PaneId, Option<Direction>, Option<NotificationEnd>), MovePaneBackwardsWithPaneId(PaneId, Option<NotificationEnd>), ClearScreenWithPaneId(PaneId, Option<NotificationEnd>), - EditScrollbackWithPaneId(PaneId, Option<NotificationEnd>), + EditScrollbackWithPaneId(PaneId, bool, Option<NotificationEnd>), ToggleFullscreenWithPaneId(PaneId, Option<NotificationEnd>), TogglePaneEmbedOrFloatingWithPaneId(PaneId, Option<NotificationEnd>), CloseFocusWithPaneId(PaneId, Option<NotificationEnd>), @@ -798,7 +800,6 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::GetPaneInfo { .. } => ScreenContext::GetPaneInfo, ScreenInstruction::GetTabInfo { .. } => ScreenContext::GetTabInfo, ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback, - ScreenInstruction::EditScrollbackRaw(..) => ScreenContext::EditScrollback, // fallback ScreenInstruction::GetPaneScrollback { .. } => ScreenContext::GetPaneScrollback, ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown, @@ -1002,6 +1003,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::NotifyPaneClosedToSubscribers { .. } => { ScreenContext::NotifyPaneClosedToSubscribers }, + ScreenInstruction::PluginSubscribedToAnsiPaneContents(..) => { + ScreenContext::PluginSubscribedToAnsiPaneContents + }, // Pane-targeting CLI variants ScreenInstruction::ScrollUpWithPaneId(..) => ScreenContext::ScrollUpWithPaneId, ScreenInstruction::ScrollDownWithPaneId(..) => ScreenContext::ScrollDownWithPaneId, @@ -1181,6 +1185,7 @@ impl WatcherState { struct PaneRenderSubscription { pane_ids: HashSet<zellij_utils::data::PaneId>, previous_viewports: HashMap<zellij_utils::data::PaneId, Vec<String>>, + ansi: bool, } /// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes). @@ -1250,6 +1255,7 @@ pub(crate) struct Screen { cached_layouts: Vec<LayoutInfo>, cached_layout_errors: Vec<LayoutWithError>, pane_render_subscribers: HashMap<ClientId, PaneRenderSubscription>, + plugins_need_ansi_pane_contents: bool, } impl Screen { @@ -1349,6 +1355,7 @@ impl Screen { cached_layouts: vec![], cached_layout_errors: vec![], pane_render_subscribers: HashMap::new(), + plugins_need_ansi_pane_contents: false, } } @@ -1900,6 +1907,10 @@ impl Screen { self.osc8_hyperlinks, ); + let has_ansi_subscribers = self.pane_render_subscribers.values().any(|s| s.ansi); + output.collect_ansi_pane_contents = + has_ansi_subscribers || self.plugins_need_ansi_pane_contents; + for (tab_index, tab) in &mut self.tabs { if tab.has_selectable_tiled_panes() { // Pass None for normal client rendering @@ -4419,6 +4430,7 @@ impl Screen { subscriber_client_id: ClientId, pane_ids: Vec<zellij_utils::data::PaneId>, scrollback: Option<usize>, + ansi: bool, ) { let mut previous_viewports = HashMap::new(); let mut valid_pane_ids = HashSet::new(); @@ -4445,8 +4457,15 @@ impl Screen { PaneId::Plugin(_) => regular_client_id, PaneId::Terminal(_) => None, }; - let contents = - pane.pane_contents(query_client_id, get_full_scrollback, max_lines); + let contents = if ansi { + pane.pane_contents_with_ansi( + query_client_id, + get_full_scrollback, + max_lines, + ) + } else { + pane.pane_contents(query_client_id, get_full_scrollback, max_lines) + }; if let Some(os_input) = &self.bus.os_input { let scrollback_data = if scrollback.is_some() { @@ -4490,6 +4509,7 @@ impl Screen { PaneRenderSubscription { pane_ids: valid_pane_ids, previous_viewports, + ansi, }, ); } @@ -4498,7 +4518,8 @@ impl Screen { let Some(pane_map) = report.all_pane_contents.values().next() else { return; }; - self.deliver_subscriber_updates_from_map(pane_map); + let ansi_pane_map = report.all_pane_contents_with_ansi.values().next(); + self.deliver_subscriber_updates_from_map(pane_map, ansi_pane_map); } fn deliver_to_pane_subscribers_directly(&mut self) { // Collect unique pane IDs across all subscribers @@ -4508,31 +4529,50 @@ impl Screen { .flat_map(|sub| sub.pane_ids.iter().copied()) .collect(); + let has_ansi_subscribers = self.pane_render_subscribers.values().any(|s| s.ansi); + // Query pane contents directly from tabs let mut pane_map: HashMap<zellij_utils::data::PaneId, PaneContents> = HashMap::new(); + let mut ansi_pane_map: HashMap<zellij_utils::data::PaneId, PaneContents> = HashMap::new(); for pane_id in &all_subscribed_ids { let server_pane_id: PaneId = (*pane_id).into(); for tab in self.tabs.values() { if let Some(pane) = tab.get_pane_with_id(server_pane_id) { pane_map.insert(*pane_id, pane.pane_contents(None, false, None)); + if has_ansi_subscribers { + ansi_pane_map + .insert(*pane_id, pane.pane_contents_with_ansi(None, false, None)); + } break; } } } - self.deliver_subscriber_updates_from_map(&pane_map); + let ansi_map_ref = if has_ansi_subscribers { + Some(&ansi_pane_map) + } else { + None + }; + self.deliver_subscriber_updates_from_map(&pane_map, ansi_map_ref); } fn deliver_subscriber_updates_from_map( &mut self, pane_map: &HashMap<zellij_utils::data::PaneId, PaneContents>, + ansi_pane_map: Option<&HashMap<zellij_utils::data::PaneId, PaneContents>>, ) { // Collect updates to send, avoiding borrow conflicts let mut updates_to_send: Vec<(ClientId, ServerToClientMsg)> = Vec::new(); let mut dead_subscribers: Vec<ClientId> = Vec::new(); for (subscriber_id, subscription) in &self.pane_render_subscribers { + let effective_map = if subscription.ansi { + ansi_pane_map.unwrap_or(pane_map) + } else { + pane_map + }; + for pane_id in &subscription.pane_ids { - if let Some(contents) = pane_map.get(pane_id) { + if let Some(contents) = effective_map.get(pane_id) { let changed = subscription .previous_viewports .get(pane_id) @@ -5293,6 +5333,7 @@ pub(crate) fn screen_thread_main( pane_id, completion_tx, cli_client_id, + ansi, ) => { match file { Some(file_path) => { @@ -5301,26 +5342,47 @@ pub(crate) fn screen_thread_main( Some(pane_id) => { for tab in screen.get_tabs_mut().values_mut() { if tab.has_pane_with_pid(&pane_id) { - tab.dump_terminal_screen( - Some(file_path.clone()), - pane_id, - full, - )?; + if ansi { + tab.dump_with_ansi_terminal_screen( + Some(file_path.clone()), + pane_id, + full, + )?; + } else { + tab.dump_terminal_screen( + Some(file_path.clone()), + pane_id, + full, + )?; + } break; } } }, None => { - active_tab_and_connected_client_id!( - screen, - client_id, - |tab: &mut Tab, client_id: ClientId| tab.dump_active_terminal_screen( - Some(file_path.to_string()), + if ansi { + active_tab_and_connected_client_id!( + screen, client_id, - full - ), - ? - ); + |tab: &mut Tab, client_id: ClientId| tab.dump_with_ansi_active_terminal_screen( + Some(file_path.to_string()), + client_id, + full + ), + ? + ); + } else { + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, client_id: ClientId| tab.dump_active_terminal_screen( + Some(file_path.to_string()), + client_id, + full + ), + ? + ); + } }, } screen.render(None)?; @@ -5333,10 +5395,18 @@ pub(crate) fn screen_thread_main( let mut result = String::new(); for tab in screen.get_tabs_mut().values_mut() { if tab.has_pane_with_pid(&pane_id) { - if let Some(dump) = - tab.get_dump_terminal_screen(pane_id, full) - { - result = dump; + if ansi { + if let Some(dump) = tab + .get_dump_with_ansi_terminal_screen(pane_id, full) + { + result = dump; + } + } else { + if let Some(dump) = + tab.get_dump_terminal_screen(pane_id, full) + { + result = dump; + } } break; } @@ -5345,15 +5415,27 @@ pub(crate) fn screen_thread_main( }, None => { let mut result = String::new(); - active_tab_and_connected_client_id!( - screen, - client_id, - |tab: &mut Tab, client_id: ClientId| { - result = tab.get_dump_active_terminal_screen(client_id, full); - Ok::<(), anyhow::Error>(()) - }, - ? - ); + if ansi { + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, client_id: ClientId| { + result = tab.get_dump_with_ansi_active_terminal_screen(client_id, full); + Ok::<(), anyhow::Error>(()) + }, + ? + ); + } else { + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, client_id: ClientId| { + result = tab.get_dump_active_terminal_screen(client_id, full); + Ok::<(), anyhow::Error>(()) + }, + ? + ); + } result }, }; @@ -5491,21 +5573,17 @@ pub(crate) fn screen_thread_main( .with_context(err_context) .non_fatal(); }, - ScreenInstruction::EditScrollback(client_id, completion_tx) => { - active_tab_and_connected_client_id!( - screen, - client_id, - |tab: &mut Tab, client_id: ClientId| tab.edit_scrollback(client_id, completion_tx), - ? - ); - screen.render(None)?; - screen.log_and_report_session_state()?; - }, - ScreenInstruction::EditScrollbackRaw(client_id, completion_tx) => { + ScreenInstruction::EditScrollback(client_id, ansi, completion_tx) => { active_tab_and_connected_client_id!( screen, client_id, - |tab: &mut Tab, client_id: ClientId| tab.edit_scrollback_raw(client_id, completion_tx), + |tab: &mut Tab, client_id: ClientId| { + if ansi { + tab.edit_scrollback_raw(client_id, completion_tx) + } else { + tab.edit_scrollback(client_id, completion_tx) + } + }, ? ); screen.render(None)?; @@ -8217,12 +8295,16 @@ pub(crate) fn screen_thread_main( client_id, pane_ids, scrollback, + ansi, } => { - screen.subscribe_to_pane_renders(client_id, pane_ids, scrollback); + screen.subscribe_to_pane_renders(client_id, pane_ids, scrollback, ansi); }, ScreenInstruction::NotifyPaneClosedToSubscribers { pane_id } => { screen.notify_pane_closed_to_subscribers(pane_id); }, + ScreenInstruction::PluginSubscribedToAnsiPaneContents(has_subscribers) => { + screen.plugins_need_ansi_pane_contents = has_subscribers; + }, // Pane-targeting CLI handlers ScreenInstruction::ScrollUpWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); @@ -8455,13 +8537,18 @@ pub(crate) fn screen_thread_main( } screen.render(None)?; }, - ScreenInstruction::EditScrollbackWithPaneId(pane_id, completion_tx) => { + ScreenInstruction::EditScrollbackWithPaneId(pane_id, ansi, completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { - tab.edit_scrollback_for_pane_with_id(pane_id, completion_tx) - .non_fatal(); + if ansi { + tab.edit_scrollback_raw_for_pane_with_id(pane_id, completion_tx) + .non_fatal(); + } else { + tab.edit_scrollback_for_pane_with_id(pane_id, completion_tx) + .non_fatal(); + } found = true; break; } diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index b95567c1..390563a2 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -608,6 +608,14 @@ pub trait Pane { _get_full_scrollback: bool, _max_scrollback_lines: Option<usize>, ) -> PaneContents; + fn pane_contents_with_ansi( + &self, + _client_id: Option<ClientId>, + _get_full_scrollback: bool, + _max_scrollback_lines: Option<usize>, + ) -> PaneContents { + Default::default() + } fn update_exit_status(&mut self, _exit_status: i32) {} fn set_plugin_regex_highlights( &mut self, @@ -4102,6 +4110,18 @@ impl Tab { } Ok(()) } + pub fn dump_with_ansi_terminal_screen( + &mut self, + file: Option<String>, + pane_id: PaneId, + full: bool, + ) -> Result<()> { + if let Some(pane) = self.get_pane_with_id(pane_id) { + let dump = pane.dump_screen_with_ansi(full, None); + self.os_api.write_to_file(dump, file).non_fatal() + } + Ok(()) + } pub fn get_dump_active_terminal_screen(&mut self, client_id: ClientId, full: bool) -> String { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.dump_screen(full, Some(client_id)) @@ -4109,6 +4129,17 @@ impl Tab { String::new() } } + pub fn get_dump_with_ansi_active_terminal_screen( + &mut self, + client_id: ClientId, + full: bool, + ) -> String { + if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { + active_pane.dump_screen_with_ansi(full, Some(client_id)) + } else { + String::new() + } + } pub fn get_dump_terminal_screen(&mut self, pane_id: PaneId, full: bool) -> Option<String> { if let Some(pane) = self.get_pane_with_id(pane_id) { Some(pane.dump_screen(full, None)) @@ -4116,6 +4147,17 @@ impl Tab { None } } + pub fn get_dump_with_ansi_terminal_screen( + &mut self, + pane_id: PaneId, + full: bool, + ) -> Option<String> { + if let Some(pane) = self.get_pane_with_id(pane_id) { + Some(pane.dump_screen_with_ansi(full, None)) + } else { + None + } + } pub fn edit_scrollback( &mut self, client_id: ClientId, @@ -4194,6 +4236,34 @@ impl Tab { Ok(()) } } + pub fn edit_scrollback_raw_for_pane_with_id( + &mut self, + pane_id: PaneId, + completion_tx: Option<NotificationEnd>, + ) -> Result<()> { + if let PaneId::Terminal(_terminal_pane_id) = pane_id { + let mut file = temp_dir(); + file.push(format!("{}.dump", Uuid::new_v4())); + self.dump_with_ansi_terminal_screen( + Some(String::from(file.to_string_lossy())), + pane_id, + true, + ) + .non_fatal(); + let line_number = self + .get_pane_with_id(pane_id) + .and_then(|a_t| a_t.get_line_number()); + self.senders.send_to_pty(PtyInstruction::OpenInPlaceEditor( + file, + line_number, + ClientTabIndexOrPaneId::PaneId(pane_id), + completion_tx, + )) + } else { + log::error!("Editing plugin pane scrollback is currently unsupported."); + Ok(()) + } + } pub fn scroll_active_terminal_up(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.scroll_up(1, client_id); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 1bbe2479..5c2e9630 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -2442,6 +2442,7 @@ pub fn send_cli_dump_screen_action() { path: Some(PathBuf::from("/tmp/foo")), full: true, pane_id: None, + ansi: false, }; let _ = mock_screen.to_screen.send(ScreenInstruction::PtyBytes( 0, @@ -2473,7 +2474,10 @@ pub fn send_cli_edit_scrollback_action() { PtyInstruction::Exit, pty_receiver ); - let cli_action = CliAction::EditScrollback { pane_id: None }; + let cli_action = CliAction::EditScrollback { + pane_id: None, + ansi: false, + }; let _ = mock_screen.to_screen.send(ScreenInstruction::PtyBytes( 0, "fill pane up with something".as_bytes().to_vec(), @@ -5220,7 +5224,12 @@ fn subscriber_receives_initial_delivery() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); let msgs = messages.lock().unwrap(); let client_msgs = msgs.get(&100).unwrap(); @@ -5244,7 +5253,12 @@ fn subscriber_receives_initial_with_scrollback() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], Some(0)); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + Some(0), + false, + ); let msgs = messages.lock().unwrap(); let client_msgs = msgs.get(&100).unwrap(); @@ -5268,7 +5282,12 @@ fn subscriber_no_update_on_unchanged_viewport() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); let initial_viewport = { let msgs = messages.lock().unwrap(); @@ -5287,7 +5306,7 @@ fn subscriber_no_update_on_unchanged_viewport() { ..Default::default() }, ); - screen.deliver_subscriber_updates_from_map(&pane_map); + screen.deliver_subscriber_updates_from_map(&pane_map, None); let msgs = messages.lock().unwrap(); let client_msgs = msgs.get(&100).unwrap(); @@ -5300,7 +5319,12 @@ fn subscriber_receives_update_on_changed_viewport() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); let mut pane_map = HashMap::new(); pane_map.insert( @@ -5310,7 +5334,7 @@ fn subscriber_receives_update_on_changed_viewport() { ..Default::default() }, ); - screen.deliver_subscriber_updates_from_map(&pane_map); + screen.deliver_subscriber_updates_from_map(&pane_map, None); let msgs = messages.lock().unwrap(); let client_msgs = msgs.get(&100).unwrap(); @@ -5336,7 +5360,12 @@ fn subscriber_error_for_nonexistent_pane() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(999)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(999)], + None, + false, + ); let msgs = messages.lock().unwrap(); let client_msgs = msgs.get(&100).unwrap(); @@ -5372,6 +5401,7 @@ fn subscriber_state_registered_for_multiple_panes() { zellij_utils::data::PaneId::Terminal(2), ], None, + false, ); let sub = screen.pane_render_subscribers.get(&100).unwrap(); @@ -5390,8 +5420,18 @@ fn multiple_subscribers_receive_updates() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); - screen.subscribe_to_pane_renders(101, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); + screen.subscribe_to_pane_renders( + 101, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); let mut pane_map = HashMap::new(); pane_map.insert( @@ -5401,7 +5441,7 @@ fn multiple_subscribers_receive_updates() { ..Default::default() }, ); - screen.deliver_subscriber_updates_from_map(&pane_map); + screen.deliver_subscriber_updates_from_map(&pane_map, None); let msgs = messages.lock().unwrap(); let client_100_msgs = msgs.get(&100).unwrap(); @@ -5416,7 +5456,12 @@ fn subscriber_removed_on_remove_client() { let (mut screen, _messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); assert!(screen.pane_render_subscribers.contains_key(&100)); let _ = screen.remove_client(100); @@ -5429,7 +5474,12 @@ fn subscriber_removed_when_all_panes_closed() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); screen.notify_pane_closed_to_subscribers(zellij_utils::data::PaneId::Terminal(1)); @@ -5470,6 +5520,7 @@ fn subscriber_partial_close() { zellij_utils::data::PaneId::Terminal(2), ], None, + false, ); screen.notify_pane_closed_to_subscribers(zellij_utils::data::PaneId::Terminal(1)); @@ -5518,6 +5569,7 @@ fn subscriber_full_close_sequence() { zellij_utils::data::PaneId::Terminal(2), ], None, + false, ); screen.notify_pane_closed_to_subscribers(zellij_utils::data::PaneId::Terminal(1)); @@ -5551,7 +5603,12 @@ fn delivery_path_a_and_b_produce_same_content() { let (mut screen, messages) = create_new_screen_with_message_capture(size); new_tab(&mut screen, 1, 0); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); messages.lock().unwrap().get_mut(&100).unwrap().clear(); @@ -5581,7 +5638,10 @@ fn delivery_path_a_and_b_produce_same_content() { pane_map.insert(zellij_utils::data::PaneId::Terminal(1), contents.clone()); let mut all_pane_contents = HashMap::new(); all_pane_contents.insert(1 as ClientId, pane_map); - let report = PaneRenderReport { all_pane_contents }; + let report = PaneRenderReport { + all_pane_contents, + all_pane_contents_with_ansi: HashMap::new(), + }; screen.deliver_to_pane_subscribers_from_report(&report); let viewport_a = { @@ -5628,7 +5688,12 @@ fn close_tab_notifies_subscribers() { new_tab(&mut screen, 1, 0); new_tab(&mut screen, 2, 1); - screen.subscribe_to_pane_renders(100, vec![zellij_utils::data::PaneId::Terminal(1)], None); + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); messages.lock().unwrap().get_mut(&100).unwrap().clear(); @@ -5688,6 +5753,7 @@ fn close_pane_notifies_subscribers_via_instruction() { zellij_utils::data::PaneId::Terminal(1), ], scrollback: None, + ansi: false, }); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -5742,6 +5808,7 @@ fn integration_pty_bytes_delivered_to_subscriber() { client_id: 100, pane_ids: vec![zellij_utils::data::PaneId::Terminal(0)], scrollback: None, + ansi: false, }); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -5816,6 +5883,7 @@ fn integration_pty_bytes_not_delivered_when_viewport_unchanged() { client_id: 100, pane_ids: vec![zellij_utils::data::PaneId::Terminal(0)], scrollback: None, + ansi: false, }); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -5881,6 +5949,7 @@ fn integration_scrollback_from_pre_subscription_pty_bytes() { client_id: 100, pane_ids: vec![zellij_utils::data::PaneId::Terminal(0)], scrollback: Some(0), + ansi: false, }); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -5997,6 +6066,7 @@ fn integration_no_scrollback_when_not_requested() { client_id: 100, pane_ids: vec![zellij_utils::data::PaneId::Terminal(0)], scrollback: None, + ansi: false, }); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -6057,6 +6127,7 @@ fn integration_subscriber_survives_after_regular_client_detach() { client_id: 100, pane_ids: vec![zellij_utils::data::PaneId::Terminal(0)], scrollback: None, + ansi: false, }); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -6480,6 +6551,7 @@ pub fn send_cli_edit_scrollback_with_pane_id() { std::thread::sleep(std::time::Duration::from_millis(100)); let cli_action = CliAction::EditScrollback { pane_id: Some("terminal_0".to_string()), + ansi: false, }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -6934,3 +7006,352 @@ pub fn move_tab_by_id_verifies_screen_state() { assert_eq!(screen.get_tab_by_id(0).unwrap().position, original_pos_1); assert_eq!(screen.get_tab_by_id(1).unwrap().position, original_pos_0); } + +// ========================================== +// Category 5: ANSI flag tests +// ========================================== + +#[test] +pub fn send_cli_dump_screen_action_with_ansi() { + let size = Size { cols: 80, rows: 20 }; + let client_id = 10; + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let mut mock_screen = MockScreen::new(size); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + let cli_action = CliAction::DumpScreen { + path: Some(PathBuf::from("/tmp/foo_ansi")), + full: true, + pane_id: None, + ansi: true, + }; + let _ = mock_screen.to_screen.send(ScreenInstruction::PtyBytes( + 0, + "\x1b[31mred text\x1b[0m".as_bytes().to_vec(), + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + send_cli_action_to_server(&session_metadata, cli_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![server_thread, screen_thread]); + let fs = mock_screen.os_input.fake_filesystem.lock().unwrap(); + let dumped_content = fs.values().next().expect("Should have dumped a file"); + assert!( + dumped_content.contains("\x1b["), + "Dumped file should contain ANSI escape codes when ansi flag is true. Content: {:?}", + dumped_content + ); +} + +#[test] +pub fn send_cli_dump_screen_action_without_ansi_strips_codes() { + let size = Size { cols: 80, rows: 20 }; + let client_id = 10; + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let mut mock_screen = MockScreen::new(size); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + let cli_action = CliAction::DumpScreen { + path: Some(PathBuf::from("/tmp/foo_plain")), + full: true, + pane_id: None, + ansi: false, + }; + let _ = mock_screen.to_screen.send(ScreenInstruction::PtyBytes( + 0, + "\x1b[31mred text\x1b[0m".as_bytes().to_vec(), + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + send_cli_action_to_server(&session_metadata, cli_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![server_thread, screen_thread]); + let fs = mock_screen.os_input.fake_filesystem.lock().unwrap(); + let dumped_content = fs.values().next().expect("Should have dumped a file"); + assert!( + !dumped_content.contains("\x1b["), + "Dumped file should NOT contain ANSI escape codes when ansi flag is false. Content: {:?}", + dumped_content + ); +} + +#[test] +pub fn send_cli_edit_scrollback_action_with_ansi() { + let size = Size { cols: 80, rows: 20 }; + let client_id = 10; + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let mut mock_screen = MockScreen::new(size); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_receiver = mock_screen.pty_receiver.take().unwrap(); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::Exit, + pty_receiver + ); + let cli_action = CliAction::EditScrollback { + pane_id: None, + ansi: true, + }; + let _ = mock_screen.to_screen.send(ScreenInstruction::PtyBytes( + 0, + "\x1b[31mred text\x1b[0m".as_bytes().to_vec(), + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + send_cli_action_to_server(&session_metadata, cli_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![pty_thread, screen_thread]); + let fs = mock_screen.os_input.fake_filesystem.lock().unwrap(); + let dumped_content = fs.values().next().expect("Should have dumped a file"); + assert!( + dumped_content.contains("\x1b["), + "Edit scrollback dump should contain ANSI escape codes when ansi flag is true. Content: {:?}", + dumped_content + ); +} + +#[test] +pub fn send_cli_edit_scrollback_with_pane_id_and_ansi() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; + let mut mock_screen = MockScreen::new(size); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(None, vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + std::thread::sleep(std::time::Duration::from_millis(100)); + let cli_action = CliAction::EditScrollback { + pane_id: Some("terminal_0".to_string()), + ansi: true, + }; + send_cli_action_to_server(&session_metadata, cli_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![server_thread, screen_thread]); + assert!( + true, + "EditScrollback with pane_id and ansi CLI action completed without errors" + ); +} + +#[test] +fn subscriber_ansi_flag_preserved_in_subscription() { + let size = Size { cols: 80, rows: 20 }; + let (mut screen, _messages) = create_new_screen_with_message_capture(size); + new_tab(&mut screen, 1, 0); + + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); + screen.subscribe_to_pane_renders( + 101, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + true, + ); + + assert!( + !screen.pane_render_subscribers.get(&100).unwrap().ansi, + "Subscriber 100 should have ansi=false" + ); + assert!( + screen.pane_render_subscribers.get(&101).unwrap().ansi, + "Subscriber 101 should have ansi=true" + ); +} + +#[test] +fn subscriber_ansi_and_plain_receive_different_content() { + let size = Size { cols: 80, rows: 20 }; + let (mut screen, messages) = create_new_screen_with_message_capture(size); + new_tab(&mut screen, 1, 0); + + // Subscribe plain and ansi subscribers + screen.subscribe_to_pane_renders( + 100, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + false, + ); + screen.subscribe_to_pane_renders( + 101, + vec![zellij_utils::data::PaneId::Terminal(1)], + None, + true, + ); + + // Clear initial messages + messages.lock().unwrap().get_mut(&100).unwrap().clear(); + messages.lock().unwrap().get_mut(&101).unwrap().clear(); + + // Reset previous viewports to force delivery + screen + .pane_render_subscribers + .get_mut(&100) + .unwrap() + .previous_viewports + .insert( + zellij_utils::data::PaneId::Terminal(1), + vec!["old".to_string()], + ); + screen + .pane_render_subscribers + .get_mut(&101) + .unwrap() + .previous_viewports + .insert( + zellij_utils::data::PaneId::Terminal(1), + vec!["old".to_string()], + ); + + // Build plain and ansi maps with different content + let mut plain_map = HashMap::new(); + plain_map.insert( + zellij_utils::data::PaneId::Terminal(1), + PaneContents { + viewport: vec!["plain text".to_string()], + ..Default::default() + }, + ); + let mut ansi_map = HashMap::new(); + ansi_map.insert( + zellij_utils::data::PaneId::Terminal(1), + PaneContents { + viewport: vec!["\x1b[31mred text\x1b[0m".to_string()], + ..Default::default() + }, + ); + screen.deliver_subscriber_updates_from_map(&plain_map, Some(&ansi_map)); + + let msgs = messages.lock().unwrap(); + let plain_msgs = msgs.get(&100).unwrap(); + let ansi_msgs = msgs.get(&101).unwrap(); + + assert_eq!( + plain_msgs.len(), + 1, + "Plain subscriber should receive one update" + ); + assert_eq!( + ansi_msgs.len(), + 1, + "Ansi subscriber should receive one update" + ); + + match &plain_msgs[0] { + ServerToClientMsg::PaneRenderUpdate { viewport, .. } => { + assert_eq!(viewport, &vec!["plain text".to_string()]); + }, + other => panic!("Expected PaneRenderUpdate, got {:?}", other), + } + match &ansi_msgs[0] { + ServerToClientMsg::PaneRenderUpdate { viewport, .. } => { + assert_eq!(viewport, &vec!["\x1b[31mred text\x1b[0m".to_string()]); + }, + other => panic!("Expected PaneRenderUpdate, got {:?}", other), + } +} + +#[test] +fn integration_subscribe_with_ansi_flag() { + let size = Size { cols: 80, rows: 20 }; + let mut mock_screen = MockScreen::new(size); + mock_screen.drop_all_pty_messages(); + let screen_thread = mock_screen.run(None, vec![]); + + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver + ); + + let _ = mock_screen + .to_screen + .send(ScreenInstruction::SubscribeToPaneRenders { + client_id: 100, + pane_ids: vec![zellij_utils::data::PaneId::Terminal(0)], + scrollback: None, + ansi: true, + }); + std::thread::sleep(std::time::Duration::from_millis(100)); + + let _ = mock_screen.to_screen.send(ScreenInstruction::PtyBytes( + 0, + "\x1b[31mred text\x1b[0m\r\n".as_bytes().to_vec(), + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, plugin_thread, screen_thread]); + + let msgs = mock_screen + .os_input + .server_to_client_messages + .lock() + .unwrap(); + let subscriber_msgs = msgs.get(&100).unwrap_or(&vec![]).clone(); + + assert!( + subscriber_msgs.len() >= 2, + "Should have at least initial + update, got {}", + subscriber_msgs.len() + ); + + match &subscriber_msgs[0] { + ServerToClientMsg::PaneRenderUpdate { is_initial, .. } => { + assert!(*is_initial, "First message should be initial"); + }, + other => panic!("Expected PaneRenderUpdate, got {:?}", other), + } + + let has_ansi_content = subscriber_msgs.iter().any(|m| match m { + ServerToClientMsg::PaneRenderUpdate { + is_initial: false, + viewport, + .. + } => viewport.iter().any(|line| line.contains("\x1b[")), + _ => false, + }); + assert!( + has_ansi_content, + "ANSI subscriber should receive viewport lines with ANSI escape codes. Messages: {:?}", + subscriber_msgs + ); +} diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index 53e3b898..2849f268 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -623,6 +623,8 @@ pub struct DumpScreenPayload { pub pane_id: ::core::option::Option<PaneId>, #[prost(bool, tag="4")] pub dump_to_stdout: bool, + #[prost(bool, tag="5")] + pub ansi: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 30cc8439..894b26be 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -9,7 +9,7 @@ pub struct EventNameList { pub struct Event { #[prost(enumeration="EventType", tag="1")] pub name: i32, - #[prost(oneof="event::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36")] + #[prost(oneof="event::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37")] pub payload: ::core::option::Option<event::Payload>, } /// Nested message and enum types in `Event`. @@ -87,6 +87,8 @@ pub mod event { PluginConfigurationChangedPayload(super::PluginConfigurationChangedPayload), #[prost(message, tag="36")] HighlightClickedPayload(super::HighlightClickedPayload), + #[prost(message, tag="37")] + PaneRenderReportWithAnsiPayload(super::PaneRenderReportPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -803,6 +805,7 @@ pub enum EventType { AvailableLayoutInfo = 40, PluginConfigurationChanged = 41, HighlightClicked = 42, + PaneRenderReportWithAnsi = 43, } impl EventType { /// String value of the enum field names used in the ProtoBuf definition. @@ -853,6 +856,7 @@ impl EventType { EventType::AvailableLayoutInfo => "AvailableLayoutInfo", EventType::PluginConfigurationChanged => "PluginConfigurationChanged", EventType::HighlightClicked => "HighlightClicked", + EventType::PaneRenderReportWithAnsi => "PaneRenderReportWithAnsi", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -900,6 +904,7 @@ impl EventType { "AvailableLayoutInfo" => Some(Self::AvailableLayoutInfo), "PluginConfigurationChanged" => Some(Self::PluginConfigurationChanged), "HighlightClicked" => Some(Self::HighlightClicked), + "PaneRenderReportWithAnsi" => Some(Self::PaneRenderReportWithAnsi), _ => None, } } diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs index 3496655c..4d305e00 100644 --- a/zellij-utils/assets/prost_ipc/client_server_contract.rs +++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs @@ -437,6 +437,8 @@ pub struct SaveSessionAction { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct EditScrollbackAction { + #[prost(bool, tag="1")] + pub ansi: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -750,6 +752,8 @@ pub struct DumpScreenAction { pub pane_id: ::core::option::Option<PaneId>, #[prost(bool, tag="4")] pub dump_to_stdout: bool, + #[prost(bool, tag="5")] + pub ansi: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1974,6 +1978,8 @@ pub struct ClearScreenByPaneIdAction { pub struct EditScrollbackByPaneIdAction { #[prost(message, optional, tag="1")] pub pane_id: ::core::option::Option<PaneId>, + #[prost(bool, tag="2")] + pub ansi: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -2977,6 +2983,8 @@ pub struct SubscribeToPaneRendersMsg { pub pane_ids: ::prost::alloc::vec::Vec<PaneId>, #[prost(uint32, optional, tag="2")] pub scrollback: ::core::option::Option<u32>, + #[prost(bool, tag="3")] + pub ansi: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 005c8b66..4c79354c 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -153,6 +153,10 @@ pub struct SubscribeCli { /// Output format #[clap(short, long, default_value = "raw", arg_enum)] pub format: SubscribeFormat, + + /// Preserve ANSI styling in the output + #[clap(long, value_parser, default_value("false"), takes_value(false))] + pub ansi: bool, } #[derive(Debug, Clone, Serialize, Deserialize, ArgEnum)] @@ -773,6 +777,10 @@ pub enum CliAction { /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3). If not specified, dumps the focused pane. #[clap(short, long, value_parser)] pane_id: Option<String>, + + /// Preserve ANSI styling in the dump output + #[clap(short, long, value_parser, default_value("false"), takes_value(false))] + ansi: bool, }, /// Dump current layout to stdout DumpLayout, @@ -783,6 +791,10 @@ pub enum CliAction { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option<String>, + + /// Preserve ANSI styling in the scrollback dump + #[clap(short, long, value_parser, default_value("false"), takes_value(false))] + ansi: bool, }, /// Scroll up in the focused pane ScrollUp { diff --git a/zellij-utils/src/client_server_contract/client_to_server.proto b/zellij-utils/src/client_server_contract/client_to_server.proto index 8fdedac2..043b855e 100644 --- a/zellij-utils/src/client_server_contract/client_to_server.proto +++ b/zellij-utils/src/client_server_contract/client_to_server.proto @@ -102,4 +102,5 @@ message FailedToStartWebServerMsg { message SubscribeToPaneRendersMsg { repeated PaneId pane_ids = 1; optional uint32 scrollback = 2; + bool ansi = 3; } diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index f93f2632..864083b7 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -286,7 +286,9 @@ message MovePaneBackwardsAction {} message ClearScreenAction {} message DumpLayoutAction {} message SaveSessionAction {} -message EditScrollbackAction {} +message EditScrollbackAction { + bool ansi = 1; +} message ScrollUpAction {} message ScrollDownAction {} message ScrollToBottomAction {} @@ -420,6 +422,7 @@ message DumpScreenAction { bool include_scrollback = 2; optional PaneId pane_id = 3; bool dump_to_stdout = 4; + bool ansi = 5; } message ScrollUpAtAction { @@ -1187,7 +1190,10 @@ message ResizeByPaneIdAction { PaneId pane_id = 1; ResizeAction resize_action = message MovePaneByPaneIdAction { PaneId pane_id = 1; optional Direction direction = 2; } message MovePaneBackwardsByPaneIdAction { PaneId pane_id = 1; } message ClearScreenByPaneIdAction { PaneId pane_id = 1; } -message EditScrollbackByPaneIdAction { PaneId pane_id = 1; } +message EditScrollbackByPaneIdAction { + PaneId pane_id = 1; + bool ansi = 2; +} message ToggleFullscreenByPaneIdAction { PaneId pane_id = 1; } message TogglePaneEmbedOrFloatingByPaneIdAction { PaneId pane_id = 1; } message CloseFocusByPaneIdAction { PaneId pane_id = 1; } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 724f0abf..46a0daa5 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1009,6 +1009,7 @@ pub enum Event { /// An action was performed by the user (requires InterceptInput permission) UserAction(Action, ClientId, Option<u32>, Option<ClientId>), // Action, client_id, terminal_id, cli_client_id PaneRenderReport(HashMap<PaneId, PaneContents>), + PaneRenderReportWithAnsi(HashMap<PaneId, PaneContents>), ActionComplete(Action, Option<PaneId>, BTreeMap<String, String>), // Action, pane_id, context CwdChanged(PaneId, PathBuf, Vec<ClientId>), // pane_id, cwd, focused_client_ids AvailableLayoutInfo(Vec<LayoutInfo>, Vec<LayoutWithError>), @@ -2374,6 +2375,7 @@ impl ClientInfo { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct PaneRenderReport { pub all_pane_contents: HashMap<ClientId, HashMap<PaneId, PaneContents>>, + pub all_pane_contents_with_ansi: HashMap<ClientId, HashMap<PaneId, PaneContents>>, } impl PaneRenderReport { @@ -2391,6 +2393,20 @@ impl PaneRenderReport { p.insert(pane_id, pane_contents.clone()); } } + pub fn add_pane_contents_with_ansi( + &mut self, + client_ids: &[ClientId], + pane_id: PaneId, + pane_contents: PaneContents, + ) { + for client_id in client_ids { + let p = self + .all_pane_contents_with_ansi + .entry(*client_id) + .or_insert_with(|| HashMap::new()); + p.insert(pane_id, pane_contents.clone()); + } + } } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index a07f4c84..7f137d58 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -445,6 +445,7 @@ pub enum ScreenContext { PreviousSwapLayoutWithTabId, NextSwapLayoutWithTabId, MoveTabWithTabId, + PluginSubscribedToAnsiPaneContents, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index f26f6e9a..e2d5485b 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -179,12 +179,15 @@ pub enum Action { file_path: Option<String>, include_scrollback: bool, pane_id: Option<PaneId>, + ansi: bool, }, /// Dumps DumpLayout, /// Save the current session state to disk SaveSession, - EditScrollback, + EditScrollback { + ansi: bool, + }, EditScrollbackRaw, /// Scroll up in focus pane. ScrollUp, @@ -580,6 +583,7 @@ pub enum Action { }, EditScrollbackByPaneId { pane_id: PaneId, + ansi: bool, }, ToggleFocusFullscreenByPaneId { pane_id: PaneId, @@ -838,6 +842,7 @@ impl Action { path, full, pane_id, + ansi, } => match pane_id { Some(pane_id_str) => { let parsed_pane_id = PaneId::from_str(&pane_id_str); @@ -847,6 +852,7 @@ impl Action { file_path: path.map(|p| p.as_os_str().to_string_lossy().into()), include_scrollback: full, pane_id: Some(parsed_pane_id), + ansi, }]) }, Err(_e) => { @@ -861,19 +867,20 @@ impl Action { file_path: path.map(|p| p.as_os_str().to_string_lossy().into()), include_scrollback: full, pane_id: None, + ansi, }]), }, CliAction::DumpLayout => Ok(vec![Action::DumpLayout]), CliAction::SaveSession => Ok(vec![Action::SaveSession]), - CliAction::EditScrollback { pane_id } => match pane_id { + CliAction::EditScrollback { pane_id, ansi } => match pane_id { Some(pane_id_str) => { let pane_id = PaneId::from_str(&pane_id_str) .map_err(|_| format!( "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)" ))?; - Ok(vec![Action::EditScrollbackByPaneId { pane_id }]) + Ok(vec![Action::EditScrollbackByPaneId { pane_id, ansi }]) }, - None => Ok(vec![Action::EditScrollback]), + None => Ok(vec![Action::EditScrollback { ansi }]), }, CliAction::ScrollUp { pane_id } => match pane_id { Some(pane_id_str) => { @@ -2521,14 +2528,16 @@ mod tests { fn test_edit_scrollback_with_pane_id() { let cli_action = CliAction::EditScrollback { pane_id: Some("terminal_15".to_string()), + ansi: false, }; let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); assert!(result.is_ok()); let actions = result.unwrap(); assert_eq!(actions.len(), 1); match &actions[0] { - Action::EditScrollbackByPaneId { pane_id } => { + Action::EditScrollbackByPaneId { pane_id, ansi } => { assert!(matches!(pane_id, PaneId::Terminal(15))); + assert!(!ansi); }, _ => panic!("Expected EditScrollbackByPaneId action"), } @@ -2536,12 +2545,15 @@ mod tests { #[test] fn test_edit_scrollback_without_pane_id() { - let cli_action = CliAction::EditScrollback { pane_id: None }; + let cli_action = CliAction::EditScrollback { + pane_id: None, + ansi: false, + }; let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); assert!(result.is_ok()); let actions = result.unwrap(); assert_eq!(actions.len(), 1); - assert!(matches!(actions[0], Action::EditScrollback)); + assert!(matches!(actions[0], Action::EditScrollback { ansi: false })); } // 14. ToggleFullscreen @@ -2995,4 +3007,84 @@ mod tests { _ => panic!("Expected MoveTab action"), } } + + // 28. ANSI flag tests + + #[test] + fn test_edit_scrollback_with_ansi_flag() { + let cli_action = CliAction::EditScrollback { + pane_id: None, + ansi: true, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + assert!(matches!(actions[0], Action::EditScrollback { ansi: true })); + } + + #[test] + fn test_edit_scrollback_with_pane_id_and_ansi() { + let cli_action = CliAction::EditScrollback { + pane_id: Some("terminal_15".to_string()), + ansi: true, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::EditScrollbackByPaneId { pane_id, ansi } => { + assert_eq!(*pane_id, PaneId::Terminal(15)); + assert!(*ansi); + }, + _ => panic!("Expected EditScrollbackByPaneId action"), + } + } + + #[test] + fn test_dump_screen_with_ansi_flag() { + let cli_action = CliAction::DumpScreen { + path: Some(PathBuf::from("/tmp/test")), + full: true, + pane_id: None, + ansi: true, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::DumpScreen { + ansi, + include_scrollback, + .. + } => { + assert!(*ansi); + assert!(*include_scrollback); + }, + _ => panic!("Expected DumpScreen action"), + } + } + + #[test] + fn test_dump_screen_with_pane_id_and_ansi() { + let cli_action = CliAction::DumpScreen { + path: None, + full: false, + pane_id: Some("terminal_5".to_string()), + ansi: true, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::DumpScreen { pane_id, ansi, .. } => { + assert_eq!(*pane_id, Some(PaneId::Terminal(5))); + assert!(*ansi); + }, + _ => panic!("Expected DumpScreen action"), + } + } } diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 656bec4c..219d392d 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -151,6 +151,7 @@ pub enum ClientToServerMsg { SubscribeToPaneRenders { pane_ids: Vec<PaneId>, scrollback: Option<usize>, + ansi: bool, }, } diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index 11948521..0d6e2d39 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -118,9 +118,11 @@ impl From<ClientToServerMsg> for ProtoClientToServerMsg { ClientToServerMsg::SubscribeToPaneRenders { pane_ids, scrollback, + ansi, } => client_to_server_msg::Message::SubscribeToPaneRenders(SubscribeToPaneRendersMsg { pane_ids: pane_ids.into_iter().map(|id| id.into()).collect(), scrollback: scrollback.map(|s| s as u32), + ansi, }), }; @@ -242,6 +244,7 @@ impl TryFrom<ProtoClientToServerMsg> for ClientToServerMsg { Ok(ClientToServerMsg::SubscribeToPaneRenders { pane_ids: pane_ids?, scrollback: msg.scrollback.map(|s| s as usize), + ansi: msg.ansi, }) }, None => Err(anyhow!("Empty ClientToServerMsg message")), @@ -1003,6 +1006,7 @@ impl From<crate::input::actions::Action> file_path, include_scrollback, pane_id, + ansi, } => { let dump_to_stdout = file_path.is_none(); ActionType::DumpScreen(DumpScreenAction { @@ -1010,16 +1014,17 @@ impl From<crate::input::actions::Action> include_scrollback, pane_id: pane_id.map(|p| p.into()), dump_to_stdout, + ansi, }) }, crate::input::actions::Action::DumpLayout => { ActionType::DumpLayout(DumpLayoutAction {}) }, - crate::input::actions::Action::EditScrollback => { - ActionType::EditScrollback(EditScrollbackAction {}) + crate::input::actions::Action::EditScrollback { ansi } => { + ActionType::EditScrollback(EditScrollbackAction { ansi }) }, crate::input::actions::Action::EditScrollbackRaw => { - ActionType::EditScrollback(EditScrollbackAction {}) // fallback to default edit scrollback + ActionType::EditScrollback(EditScrollbackAction { ansi: true }) }, crate::input::actions::Action::ScrollUp => ActionType::ScrollUp(ScrollUpAction {}), crate::input::actions::Action::ScrollUpAt { position } => { @@ -1648,9 +1653,10 @@ impl From<crate::input::actions::Action> pane_id: Some(pane_id.into()), }) }, - crate::input::actions::Action::EditScrollbackByPaneId { pane_id } => { + crate::input::actions::Action::EditScrollbackByPaneId { pane_id, ansi } => { ActionType::EditScrollbackByPaneId(EditScrollbackByPaneIdAction { pane_id: Some(pane_id.into()), + ansi, }) }, crate::input::actions::Action::ToggleFocusFullscreenByPaneId { pane_id } => { @@ -1822,11 +1828,16 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> file_path, include_scrollback: dump_screen_action.include_scrollback, pane_id: dump_screen_action.pane_id.and_then(|p| p.try_into().ok()), + ansi: dump_screen_action.ansi, }) }, ActionType::DumpLayout(_) => Ok(crate::input::actions::Action::DumpLayout), ActionType::SaveSession(_) => Ok(crate::input::actions::Action::SaveSession), - ActionType::EditScrollback(_) => Ok(crate::input::actions::Action::EditScrollback), + ActionType::EditScrollback(edit_scrollback_action) => { + Ok(crate::input::actions::Action::EditScrollback { + ansi: edit_scrollback_action.ansi, + }) + }, ActionType::ScrollUp(_) => Ok(crate::input::actions::Action::ScrollUp), ActionType::ScrollUpAt(scroll_action) => { Ok(crate::input::actions::Action::ScrollUpAt { @@ -2531,6 +2542,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .pane_id .ok_or_else(|| anyhow!("EditScrollbackByPaneId missing pane_id"))? .try_into()?, + ansi: a.ansi, }) }, ActionType::ToggleFullscreenByPaneId(a) => Ok( diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index 3202f28a..78e68f52 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -878,6 +878,7 @@ fn test_client_messages() { file_path: Some("/path/to/file".to_owned()), include_scrollback: false, pane_id: None, + ansi: false, }, terminal_id: Some(1), client_id: Some(100), @@ -888,6 +889,7 @@ fn test_client_messages() { file_path: Some("/path/to/file".to_owned()), include_scrollback: true, pane_id: None, + ansi: false, }, terminal_id: Some(1), client_id: Some(100), @@ -898,6 +900,7 @@ fn test_client_messages() { file_path: Some("/path/to/file".to_owned()), include_scrollback: true, pane_id: Some(PaneId::Terminal(5)), + ansi: false, }, terminal_id: Some(1), client_id: Some(100), @@ -908,6 +911,7 @@ fn test_client_messages() { file_path: None, include_scrollback: false, pane_id: None, + ansi: false, }, terminal_id: Some(1), client_id: Some(100), @@ -920,7 +924,7 @@ fn test_client_messages() { is_cli_client: true, }); test_client_roundtrip!(ClientToServerMsg::Action { - action: Action::EditScrollback, + action: Action::EditScrollback { ansi: false }, terminal_id: Some(1), client_id: Some(100), is_cli_client: true, @@ -3067,14 +3071,17 @@ fn test_client_messages() { test_client_roundtrip!(ClientToServerMsg::SubscribeToPaneRenders { pane_ids: vec![PaneId::Terminal(1), PaneId::Plugin(2)], scrollback: Some(100), + ansi: false, }); test_client_roundtrip!(ClientToServerMsg::SubscribeToPaneRenders { pane_ids: vec![PaneId::Terminal(0)], scrollback: None, + ansi: false, }); test_client_roundtrip!(ClientToServerMsg::SubscribeToPaneRenders { pane_ids: vec![PaneId::Terminal(1)], scrollback: Some(0), + ansi: true, }); // Pane-targeting roundtrips test_client_roundtrip!(ClientToServerMsg::Action { @@ -3206,6 +3213,34 @@ fn test_client_messages() { test_client_roundtrip!(ClientToServerMsg::Action { action: Action::EditScrollbackByPaneId { pane_id: PaneId::Terminal(1), + ansi: false, + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + // ANSI flag roundtrip tests + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::DumpScreen { + file_path: Some("/path".to_owned()), + include_scrollback: true, + pane_id: None, + ansi: true, + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::EditScrollback { ansi: true }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::EditScrollbackByPaneId { + pane_id: PaneId::Terminal(1), + ansi: true, }, terminal_id: Some(1), client_id: Some(100), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 28727d17..ead21dc1 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -49,8 +49,8 @@ macro_rules! parse_kdl_action_arguments { "FocusNextPane" => Ok(Action::FocusNextPane), "FocusPreviousPane" => Ok(Action::FocusPreviousPane), "SwitchFocus" => Ok(Action::SwitchFocus), - "EditScrollback" => Ok(Action::EditScrollback), - "EditScrollbackRaw" => Ok(Action::EditScrollbackRaw), + "EditScrollback" => Ok(Action::EditScrollback { ansi: false }), + "EditScrollbackRaw" => Ok(Action::EditScrollback { ansi: true }), "ScrollUp" => Ok(Action::ScrollUp), "ScrollDown" => Ok(Action::ScrollDown), "ScrollToBottom" => Ok(Action::ScrollToBottom), @@ -544,6 +544,7 @@ impl Action { file_path: Some(string), include_scrollback: false, pane_id: None, + ansi: false, }), "DumpLayout" => Ok(Action::DumpLayout), "NewPane" => { @@ -696,6 +697,7 @@ impl Action { file_path: Some(file), include_scrollback: _, pane_id: _, + ansi: _, } => { let mut node = KdlNode::new("DumpScreen"); node.push(file.clone()); @@ -705,7 +707,8 @@ impl Action { file_path: None, .. } => None, Action::DumpLayout => Some(KdlNode::new("DumpLayout")), - Action::EditScrollback => Some(KdlNode::new("EditScrollback")), + Action::EditScrollback { ansi: false } => Some(KdlNode::new("EditScrollback")), + Action::EditScrollback { ansi: true } => Some(KdlNode::new("EditScrollbackRaw")), Action::EditScrollbackRaw => Some(KdlNode::new("EditScrollbackRaw")), Action::ScrollUp => Some(KdlNode::new("ScrollUp")), Action::ScrollDown => Some(KdlNode::new("ScrollDown")), diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 57442e6c..5f92ad94 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -383,6 +383,7 @@ message DumpScreenPayload { bool include_scrollback = 2; optional PaneId pane_id = 3; bool dump_to_stdout = 4; + bool ansi = 5; } enum ActionName { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index 9117bee5..3ac937b2 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -222,13 +222,14 @@ impl TryFrom<ProtobufAction> for Action { file_path, include_scrollback, pane_id, + ansi: payload.ansi, }) }, _ => Err("Wrong payload for Action::DumpScreen"), }, Some(ProtobufActionName::EditScrollback) => match protobuf_action.optional_payload { Some(_) => Err("EditScrollback should not have a payload"), - None => Ok(Action::EditScrollback), + None => Ok(Action::EditScrollback { ansi: false }), }, Some(ProtobufActionName::ScrollUp) => match protobuf_action.optional_payload { Some(_) => Err("ScrollUp should not have a payload"), @@ -1178,6 +1179,7 @@ impl TryFrom<Action> for ProtobufAction { file_path, include_scrollback, pane_id, + ansi, } => { let dump_to_stdout = file_path.is_none(); Ok(ProtobufAction { @@ -1187,10 +1189,11 @@ impl TryFrom<Action> for ProtobufAction { include_scrollback, pane_id: pane_id.and_then(|p| p.try_into().ok()), dump_to_stdout, + ansi, })), }) }, - Action::EditScrollback => Ok(ProtobufAction { + Action::EditScrollback { .. } => Ok(ProtobufAction { name: ProtobufActionName::EditScrollback as i32, optional_payload: None, }), diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 349a800a..1ecfa484 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -65,6 +65,7 @@ enum EventType { AvailableLayoutInfo = 40; PluginConfigurationChanged = 41; HighlightClicked = 42; + PaneRenderReportWithAnsi = 43; } message EventNameList { @@ -109,6 +110,7 @@ message Event { AvailableLayoutInfoPayload available_layout_info_payload = 34; PluginConfigurationChangedPayload plugin_configuration_changed_payload = 35; HighlightClickedPayload highlight_clicked_payload = 36; + PaneRenderReportPayload pane_render_report_with_ansi_payload = 37; } } diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 883fcb4f..bb5b43d3 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -409,6 +409,14 @@ impl TryFrom<ProtobufEvent> for Event { }, _ => Err("Malformed payload for the PaneRenderReport Event"), }, + Some(ProtobufEventType::PaneRenderReportWithAnsi) => match protobuf_event.payload { + Some(ProtobufEventPayload::PaneRenderReportWithAnsiPayload(protobuf_payload)) => { + Ok(Event::PaneRenderReportWithAnsi( + protobuf_payload.try_into()?, + )) + }, + _ => Err("Malformed payload for the PaneRenderReportWithAnsi Event"), + }, Some(ProtobufEventType::UserAction) => match protobuf_event.payload { Some(ProtobufEventPayload::UserActionPayload(protobuf_payload)) => { let action: Action = protobuf_payload @@ -920,6 +928,12 @@ impl TryFrom<Event> for ProtobufEvent { pane_contents_map.try_into()?, )), }), + Event::PaneRenderReportWithAnsi(pane_contents_map) => Ok(ProtobufEvent { + name: ProtobufEventType::PaneRenderReportWithAnsi as i32, + payload: Some(event::Payload::PaneRenderReportWithAnsiPayload( + pane_contents_map.try_into()?, + )), + }), Event::UserAction(action, client_id, terminal_id, cli_client_id) => { let protobuf_action: ProtobufAction = action .try_into() @@ -1937,6 +1951,7 @@ impl TryFrom<ProtobufEventType> for EventType { ProtobufEventType::FailedToStartWebServer => EventType::FailedToStartWebServer, ProtobufEventType::InterceptedKeyPress => EventType::InterceptedKeyPress, ProtobufEventType::PaneRenderReport => EventType::PaneRenderReport, + ProtobufEventType::PaneRenderReportWithAnsi => EventType::PaneRenderReportWithAnsi, ProtobufEventType::UserAction => EventType::UserAction, ProtobufEventType::ActionComplete => EventType::ActionComplete, ProtobufEventType::CwdChanged => EventType::CwdChanged, @@ -1987,6 +2002,7 @@ impl TryFrom<EventType> for ProtobufEventType { EventType::FailedToStartWebServer => ProtobufEventType::FailedToStartWebServer, EventType::InterceptedKeyPress => ProtobufEventType::InterceptedKeyPress, EventType::PaneRenderReport => ProtobufEventType::PaneRenderReport, + EventType::PaneRenderReportWithAnsi => ProtobufEventType::PaneRenderReportWithAnsi, EventType::UserAction => ProtobufEventType::UserAction, EventType::ActionComplete => ProtobufEventType::ActionComplete, EventType::CwdChanged => ProtobufEventType::CwdChanged, @@ -2913,3 +2929,50 @@ impl TryFrom<SelectedText> for ProtobufSelectedText { }) } } + +#[test] +fn serialize_pane_render_report_with_ansi_event() { + use prost::Message; + let pane_render_report_with_ansi_event = Event::PaneRenderReportWithAnsi(Default::default()); + let protobuf_event: ProtobufEvent = pane_render_report_with_ansi_event + .clone() + .try_into() + .unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + pane_render_report_with_ansi_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_pane_render_report_with_ansi_event_with_data() { + use prost::Message; + use std::collections::HashMap; + let mut pane_contents_map = HashMap::new(); + pane_contents_map.insert( + PaneId::Terminal(1), + PaneContents { + viewport: vec![ + "\x1b[31mred text\x1b[0m".to_owned(), + "\x1b[1mbold text\x1b[0m".to_owned(), + ], + selected_text: None, + lines_above_viewport: vec![], + lines_below_viewport: vec![], + }, + ); + let event = Event::PaneRenderReportWithAnsi(pane_contents_map); + let protobuf_event: ProtobufEvent = event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + event, deserialized_event, + "PaneRenderReportWithAnsi event with ANSI data properly serialized/deserialized" + ); +} diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap index a379914b..ff0a3ae4 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -2211,7 +2211,9 @@ Config { ), key_modifiers: {}, }: [ - EditScrollback, + EditScrollback { + ansi: false, + }, SwitchToMode { input_mode: Normal, }, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap index 1a28d529..04e30a09 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -2211,7 +2211,9 @@ Config { ), key_modifiers: {}, }: [ - EditScrollback, + EditScrollback { + ansi: false, + }, SwitchToMode { input_mode: Normal, }, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap index 2d4bb618..9cb4295d 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap @@ -2211,7 +2211,9 @@ Config { ), key_modifiers: {}, }: [ - EditScrollback, + EditScrollback { + ansi: false, + }, SwitchToMode { input_mode: Normal, }, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap index e0f74d91..eb72f108 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -2211,7 +2211,9 @@ Config { ), key_modifiers: {}, }: [ - EditScrollback, + EditScrollback { + ansi: false, + }, SwitchToMode { input_mode: Normal, }, |
