aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-09 15:37:42 +0100
committerGitHub <noreply@github.com>2026-03-09 15:37:42 +0100
commit47ba6331c36f1b55f1a7d7ad3c0ab511629586a3 (patch)
tree37faab2182e15a2bc137991d37f282439df9ebc6
parentdocs(changelog): dump viewport of pane id (diff)
downloadsl0ppij-47ba6331c36f1b55f1a7d7ad3c0ab511629586a3.tar.gz
sl0ppij-47ba6331c36f1b55f1a7d7ad3c0ab511629586a3.tar.xz
sl0ppij-47ba6331c36f1b55f1a7d7ad3c0ab511629586a3.zip
allow dumping screen to STDOUT as well (#4823)
-rw-r--r--docs/MANPAGE.md3
-rw-r--r--zellij-server/src/route.rs1
-rw-r--r--zellij-server/src/screen.rs90
-rw-r--r--zellij-server/src/tab/mod.rs14
-rw-r--r--zellij-server/src/unit/screen_tests.rs2
-rw-r--r--zellij-utils/assets/prost/api.action.rs2
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs2
-rw-r--r--zellij-utils/src/cli.rs6
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto1
-rw-r--r--zellij-utils/src/input/actions.rs8
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs21
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs16
-rw-r--r--zellij-utils/src/kdl/mod.rs7
-rw-r--r--zellij-utils/src/plugin_api/action.proto1
-rw-r--r--zellij-utils/src/plugin_api/action.rs28
15 files changed, 153 insertions, 49 deletions
diff --git a/docs/MANPAGE.md b/docs/MANPAGE.md
index 731929b9..1d490c80 100644
--- a/docs/MANPAGE.md
+++ b/docs/MANPAGE.md
@@ -156,7 +156,8 @@ ACTIONS
* __MoveFocus: <Direction\>__ - moves focus in the specified direction (Left,
Right, Up, Down).
* __Clear__ - clears current screen.
-* __DumpScreen: <File\> [--pane-id <ID\>]__ - dumps the pane content to the specified file.
+* __DumpScreen: [File\] [--pane-id <ID\>]__ - dumps the pane content to a file or STDOUT.
+ If a file path is provided, writes the content to that file. If omitted, prints the content to STDOUT.
If --pane-id is provided, dumps the specified pane; otherwise dumps the focused pane.
<ID\> can be a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1).
A bare integer is equivalent to a terminal pane id with the same number.
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index f6ee7949..3f7250e0 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -435,6 +435,7 @@ pub(crate) fn route_action(
include_scrollback,
pane_id.map(|p| p.into()),
Some(NotificationEnd::new(completion_tx)),
+ cli_client_id,
))
.with_context(err_context)?;
},
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 581a98d2..84aecf44 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -301,11 +301,12 @@ pub enum ScreenInstruction {
Exit,
ClearScreen(ClientId, Option<NotificationEnd>),
DumpScreen(
- String,
+ Option<String>,
ClientId,
bool,
Option<PaneId>,
Option<NotificationEnd>,
+ Option<ClientId>, // cli_client_id - used to send output to the CLI client's STDOUT
),
DumpLayout(Option<PathBuf>, ClientId, Option<NotificationEnd>), // PathBuf is the default configured
// shell
@@ -5129,32 +5130,79 @@ pub(crate) fn screen_thread_main(
client_id,
full,
pane_id,
- _completion_tx, // the action ends here, dropping this will release anything
- // waiting for it
+ completion_tx,
+ cli_client_id,
) => {
- match pane_id {
- 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.clone()), pane_id, full)?;
- break;
- }
+ match file {
+ Some(file_path) => {
+ // Write dump to file (existing behavior)
+ match pane_id {
+ 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,
+ )?;
+ 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()),
+ client_id,
+ full
+ ),
+ ?
+ );
+ },
}
+ screen.render(None)?;
+ drop(completion_tx);
},
None => {
- active_tab_and_connected_client_id!(
- screen,
- client_id,
- |tab: &mut Tab, client_id: ClientId| tab.dump_active_terminal_screen(
- Some(file.to_string()),
- client_id,
- full
- ),
- ?
- );
+ // Dump to STDOUT via Log
+ let dump = match pane_id {
+ Some(pane_id) => {
+ 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;
+ }
+ break;
+ }
+ }
+ result
+ },
+ 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>(())
+ },
+ ?
+ );
+ result
+ },
+ };
+ screen.bus.senders.send_to_server(ServerInstruction::Log(
+ vec![dump],
+ cli_client_id.unwrap_or(client_id),
+ completion_tx,
+ ))?;
},
}
- screen.render(None)?;
},
ScreenInstruction::DumpLayout(default_shell, client_id, completion_tx) => {
let err_context = || format!("Failed to dump layout");
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index fba48de1..ad6d956b 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -4096,6 +4096,20 @@ impl Tab {
}
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))
+ } 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))
+ } else {
+ None
+ }
+ }
pub fn edit_scrollback(
&mut self,
client_id: ClientId,
diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs
index b7a9bb7e..06c0056d 100644
--- a/zellij-server/src/unit/screen_tests.rs
+++ b/zellij-server/src/unit/screen_tests.rs
@@ -2435,7 +2435,7 @@ pub fn send_cli_dump_screen_action() {
server_receiver
);
let cli_action = CliAction::DumpScreen {
- path: PathBuf::from("/tmp/foo"),
+ path: Some(PathBuf::from("/tmp/foo")),
full: true,
pane_id: None,
};
diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs
index 07303884..53e3b898 100644
--- a/zellij-utils/assets/prost/api.action.rs
+++ b/zellij-utils/assets/prost/api.action.rs
@@ -621,6 +621,8 @@ pub struct DumpScreenPayload {
pub include_scrollback: bool,
#[prost(message, optional, tag="3")]
pub pane_id: ::core::option::Option<PaneId>,
+ #[prost(bool, tag="4")]
+ pub dump_to_stdout: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs
index 2bb51289..9eabb990 100644
--- a/zellij-utils/assets/prost_ipc/client_server_contract.rs
+++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs
@@ -696,6 +696,8 @@ pub struct DumpScreenAction {
pub include_scrollback: bool,
#[prost(message, optional, tag="3")]
pub pane_id: ::core::option::Option<PaneId>,
+ #[prost(bool, tag="4")]
+ pub dump_to_stdout: 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 70aa10ac..1d6cd7ac 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -738,9 +738,11 @@ pub enum CliAction {
MovePaneBackwards,
/// Clear all buffers for a focused pane
Clear,
- /// Dumps the viewport and optionally scrollback of a pane to STDOUT
+ /// Dumps the viewport and optionally scrollback of a pane to a file or STDOUT
DumpScreen {
- path: PathBuf,
+ /// File path to dump the pane content to. If omitted, prints to STDOUT.
+ #[clap(long, value_parser)]
+ path: Option<PathBuf>,
/// Dump the pane with full scrollback
#[clap(short, long, value_parser, default_value("false"), takes_value(false))]
diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index 4e770a90..430a1369 100644
--- a/zellij-utils/src/client_server_contract/common_types.proto
+++ b/zellij-utils/src/client_server_contract/common_types.proto
@@ -392,6 +392,7 @@ message DumpScreenAction {
string file_path = 1;
bool include_scrollback = 2;
optional PaneId pane_id = 3;
+ bool dump_to_stdout = 4;
}
message ScrollUpAtAction {
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index ac311480..d56389c8 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -174,9 +174,9 @@ pub enum Action {
MovePaneBackwards,
/// Clear all buffers of a current screen
ClearScreen,
- /// Dumps the screen to a file
+ /// Dumps the screen to a file or STDOUT
DumpScreen {
- file_path: String,
+ file_path: Option<String>,
include_scrollback: bool,
pane_id: Option<PaneId>,
},
@@ -714,7 +714,7 @@ impl Action {
match parsed_pane_id {
Ok(parsed_pane_id) => {
Ok(vec![Action::DumpScreen {
- file_path: path.as_os_str().to_string_lossy().into(),
+ file_path: path.map(|p| p.as_os_str().to_string_lossy().into()),
include_scrollback: full,
pane_id: Some(parsed_pane_id),
}])
@@ -728,7 +728,7 @@ impl Action {
}
},
None => Ok(vec![Action::DumpScreen {
- file_path: path.as_os_str().to_string_lossy().into(),
+ file_path: path.map(|p| p.as_os_str().to_string_lossy().into()),
include_scrollback: full,
pane_id: None,
}]),
diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs
index 3551895d..cc2a3555 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -891,11 +891,15 @@ impl From<crate::input::actions::Action>
file_path,
include_scrollback,
pane_id,
- } => ActionType::DumpScreen(DumpScreenAction {
- file_path,
- include_scrollback,
- pane_id: pane_id.map(|p| p.into()),
- }),
+ } => {
+ let dump_to_stdout = file_path.is_none();
+ ActionType::DumpScreen(DumpScreenAction {
+ file_path: file_path.unwrap_or_default(),
+ include_scrollback,
+ pane_id: pane_id.map(|p| p.into()),
+ dump_to_stdout,
+ })
+ },
crate::input::actions::Action::DumpLayout => {
ActionType::DumpLayout(DumpLayoutAction {})
},
@@ -1569,8 +1573,13 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
},
ActionType::ClearScreen(_) => Ok(crate::input::actions::Action::ClearScreen),
ActionType::DumpScreen(dump_screen_action) => {
+ let file_path = if dump_screen_action.dump_to_stdout {
+ None
+ } else {
+ Some(dump_screen_action.file_path)
+ };
Ok(crate::input::actions::Action::DumpScreen {
- file_path: dump_screen_action.file_path,
+ file_path,
include_scrollback: dump_screen_action.include_scrollback,
pane_id: dump_screen_action.pane_id.and_then(|p| p.try_into().ok()),
})
diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
index 8b018c16..8abd3040 100644
--- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs
+++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
@@ -873,7 +873,7 @@ fn test_client_messages() {
});
test_client_roundtrip!(ClientToServerMsg::Action {
action: Action::DumpScreen {
- file_path: "/path/to/file".to_owned(),
+ file_path: Some("/path/to/file".to_owned()),
include_scrollback: false,
pane_id: None,
},
@@ -883,7 +883,7 @@ fn test_client_messages() {
});
test_client_roundtrip!(ClientToServerMsg::Action {
action: Action::DumpScreen {
- file_path: "/path/to/file".to_owned(),
+ file_path: Some("/path/to/file".to_owned()),
include_scrollback: true,
pane_id: None,
},
@@ -893,7 +893,7 @@ fn test_client_messages() {
});
test_client_roundtrip!(ClientToServerMsg::Action {
action: Action::DumpScreen {
- file_path: "/path/to/file".to_owned(),
+ file_path: Some("/path/to/file".to_owned()),
include_scrollback: true,
pane_id: Some(PaneId::Terminal(5)),
},
@@ -902,6 +902,16 @@ fn test_client_messages() {
is_cli_client: true,
});
test_client_roundtrip!(ClientToServerMsg::Action {
+ action: Action::DumpScreen {
+ file_path: None,
+ include_scrollback: false,
+ pane_id: None,
+ },
+ terminal_id: Some(1),
+ client_id: Some(100),
+ is_cli_client: true,
+ });
+ test_client_roundtrip!(ClientToServerMsg::Action {
action: Action::DumpLayout,
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 0a9a1d75..7408c3a4 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -541,7 +541,7 @@ impl Action {
},
"MovePaneBackwards" => Ok(Action::MovePaneBackwards),
"DumpScreen" => Ok(Action::DumpScreen {
- file_path: string,
+ file_path: Some(string),
include_scrollback: false,
pane_id: None,
}),
@@ -693,7 +693,7 @@ impl Action {
},
Action::MovePaneBackwards => Some(KdlNode::new("MovePaneBackwards")),
Action::DumpScreen {
- file_path: file,
+ file_path: Some(file),
include_scrollback: _,
pane_id: _,
} => {
@@ -701,6 +701,9 @@ impl Action {
node.push(file.clone());
Some(node)
},
+ Action::DumpScreen {
+ file_path: None, ..
+ } => None,
Action::DumpLayout => Some(KdlNode::new("DumpLayout")),
Action::EditScrollback => Some(KdlNode::new("EditScrollback")),
Action::EditScrollbackRaw => Some(KdlNode::new("EditScrollbackRaw")),
diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto
index 169bc24f..57442e6c 100644
--- a/zellij-utils/src/plugin_api/action.proto
+++ b/zellij-utils/src/plugin_api/action.proto
@@ -382,6 +382,7 @@ message DumpScreenPayload {
string file_path = 1;
bool include_scrollback = 2;
optional PaneId pane_id = 3;
+ bool dump_to_stdout = 4;
}
enum ActionName {
diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs
index 5ccd349c..aef9d881 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -211,7 +211,11 @@ impl TryFrom<ProtobufAction> for Action {
},
Some(ProtobufActionName::DumpScreen) => match protobuf_action.optional_payload {
Some(OptionalPayload::DumpScreenPayload(payload)) => {
- let file_path = payload.file_path;
+ let file_path = if payload.dump_to_stdout {
+ None
+ } else {
+ Some(payload.file_path)
+ };
let include_scrollback = payload.include_scrollback;
let pane_id = payload.pane_id.and_then(|p| p.try_into().ok());
Ok(Action::DumpScreen {
@@ -1149,14 +1153,20 @@ impl TryFrom<Action> for ProtobufAction {
file_path,
include_scrollback,
pane_id,
- } => Ok(ProtobufAction {
- name: ProtobufActionName::DumpScreen as i32,
- optional_payload: Some(OptionalPayload::DumpScreenPayload(DumpScreenPayload {
- file_path,
- include_scrollback,
- pane_id: pane_id.and_then(|p| p.try_into().ok()),
- })),
- }),
+ } => {
+ let dump_to_stdout = file_path.is_none();
+ Ok(ProtobufAction {
+ name: ProtobufActionName::DumpScreen as i32,
+ optional_payload: Some(OptionalPayload::DumpScreenPayload(
+ DumpScreenPayload {
+ file_path: file_path.unwrap_or_default(),
+ include_scrollback,
+ pane_id: pane_id.and_then(|p| p.try_into().ok()),
+ dump_to_stdout,
+ },
+ )),
+ })
+ },
Action::EditScrollback => Ok(ProtobufAction {
name: ProtobufActionName::EditScrollback as i32,
optional_payload: None,