aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-14 08:35:00 +0100
committerGitHub <noreply@github.com>2026-03-14 08:35:00 +0100
commita1e224754759252778d8d3c30050e10bc9a82f84 (patch)
tree326ed657bbbb34ed98006f6bbc36fd918fdb1b0a
parentAdd `--pane-id` and `--tab-id` to all relevant CLI commands (#4846) (diff)
downloadsl0ppij-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
-rw-r--r--CHANGELOG.md1
-rw-r--r--default-plugins/compact-bar/src/action_types.rs2
-rw-r--r--default-plugins/compact-bar/src/keybind_utils.rs2
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs2
-rw-r--r--default-plugins/status-bar/src/second_line.rs2
-rw-r--r--default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs2
-rw-r--r--zellij-client/src/cli_client.rs1
-rw-r--r--zellij-server/src/output/mod.rs13
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs14
-rw-r--r--zellij-server/src/panes/grid.rs66
-rw-r--r--zellij-server/src/panes/plugin_pane.rs11
-rw-r--r--zellij-server/src/panes/terminal_pane.rs9
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs14
-rw-r--r--zellij-server/src/panes/unit/grid_tests.rs122
-rw-r--r--zellij-server/src/plugins/mod.rs3
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap1
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs97
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs11
-rw-r--r--zellij-server/src/route.rs13
-rw-r--r--zellij-server/src/screen.rs191
-rw-r--r--zellij-server/src/tab/mod.rs70
-rw-r--r--zellij-server/src/unit/screen_tests.rs453
-rw-r--r--zellij-utils/assets/prost/api.action.rs2
-rw-r--r--zellij-utils/assets/prost/api.event.rs7
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs8
-rw-r--r--zellij-utils/src/cli.rs12
-rw-r--r--zellij-utils/src/client_server_contract/client_to_server.proto1
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto10
-rw-r--r--zellij-utils/src/data.rs16
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/input/actions.rs106
-rw-r--r--zellij-utils/src/ipc.rs1
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs22
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs37
-rw-r--r--zellij-utils/src/kdl/mod.rs9
-rw-r--r--zellij-utils/src/plugin_api/action.proto1
-rw-r--r--zellij-utils/src/plugin_api/action.rs7
-rw-r--r--zellij-utils/src/plugin_api/event.proto2
-rw-r--r--zellij-utils/src/plugin_api/event.rs63
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap4
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap4
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap4
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap4
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,
},