aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-08 21:18:05 +0100
committerGitHub <noreply@github.com>2026-03-08 21:18:05 +0100
commita8d99b64a3fe73284b0954da7daabf04da1c432d (patch)
treec03e80df9f2673253d402cc9077741694390c97e
parentAllow subscribing to pane viewport updates (renders) from the CLI (#4814) (diff)
downloadsl0ppij-a8d99b64a3fe73284b0954da7daabf04da1c432d.tar.gz
sl0ppij-a8d99b64a3fe73284b0954da7daabf04da1c432d.tar.xz
sl0ppij-a8d99b64a3fe73284b0954da7daabf04da1c432d.zip
Add cli paste action (#4817)
* add cli paste action * add pr
-rw-r--r--CHANGELOG.md1
-rw-r--r--zellij-server/src/route.rs14
-rw-r--r--zellij-server/src/screen.rs27
-rw-r--r--zellij-server/src/tab/mod.rs29
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs12
-rw-r--r--zellij-utils/src/cli.rs7
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto6
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/input/actions.rs28
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs37
-rw-r--r--zellij-utils/src/plugin_api/action.rs3
11 files changed, 150 insertions, 15 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86f472c1..8dcc6894 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -73,6 +73,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: click to open file-paths in viewport, allow plugins to highlight viewport text (https://github.com/zellij-org/zellij/pull/4802 and https://github.com/zellij-org/zellij/pull/4809)
* fix: better handle STDIN segmentation with latency (eg. over SSH) (https://github.com/zellij-org/zellij/pull/4623)
* feat: `zellij subscribe` -> allow to subscribe to pane viewport/scrollback updates from the cli (https://github.com/zellij-org/zellij/pull/4814)
+* feat: `zellij action paste` for pasting large buffers into panes from the command line (https://github.com/zellij-org/zellij/pull/4817)
## [0.43.1] - 2025-08-08
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index 540f398f..4289cb6d 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -277,6 +277,20 @@ pub(crate) fn route_action(
))
.with_context(err_context)?;
},
+ Action::Paste { chars, pane_id } => {
+ senders
+ .send_to_screen(ScreenInstruction::ClearScroll(client_id))
+ .with_context(err_context)?;
+ let bytes = chars.into_bytes();
+ senders
+ .send_to_screen(ScreenInstruction::Paste(
+ bytes,
+ pane_id.map(|p| p.into()),
+ client_id,
+ Some(NotificationEnd::new(completion_tx)),
+ ))
+ .with_context(err_context)?;
+ },
Action::SetPaneColor { pane_id, fg, bg } => {
senders
.send_to_screen(ScreenInstruction::SetPaneColor(
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index c907cfd6..8ebe10c5 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -602,6 +602,7 @@ pub enum ScreenInstruction {
ResizePaneWithId(ResizeStrategy, PaneId),
EditScrollbackForPaneWithId(PaneId, Option<NotificationEnd>),
WriteToPaneId(Vec<u8>, PaneId, Option<NotificationEnd>),
+ Paste(Vec<u8>, Option<PaneId>, ClientId, Option<NotificationEnd>),
SetPaneColor(
PaneId,
Option<String>,
@@ -891,6 +892,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenContext::EditScrollbackForPaneWithId
},
ScreenInstruction::WriteToPaneId(..) => ScreenContext::WriteToPaneId,
+ ScreenInstruction::Paste(..) => ScreenContext::Paste,
ScreenInstruction::SetPaneColor(..) => ScreenContext::SetPaneColor,
ScreenInstruction::WriteKeyToPaneId(..) => ScreenContext::WriteKeyToPaneId,
ScreenInstruction::CopyTextToClipboard(..) => ScreenContext::CopyTextToClipboard,
@@ -7395,6 +7397,31 @@ pub(crate) fn screen_thread_main(
}
screen.render(None)?;
},
+ ScreenInstruction::Paste(bytes, pane_id, client_id, _completion) => {
+ match pane_id {
+ Some(pane_id) => {
+ let all_tabs = screen.get_tabs_mut();
+ for tab in all_tabs.values_mut() {
+ if tab.has_pane_with_pid(&pane_id) {
+ tab.paste_to_pane_id(bytes, pane_id, _completion)
+ .non_fatal();
+ break;
+ }
+ }
+ },
+ None => {
+ active_tab_and_connected_client_id!(
+ screen,
+ client_id,
+ |tab: &mut Tab, _client_id: ClientId| {
+ tab.paste_to_active_terminal(bytes, client_id, _completion)
+ .non_fatal();
+ }
+ );
+ },
+ }
+ screen.render(None)?;
+ },
ScreenInstruction::SetPaneColor(pane_id, fg, bg, _completion) => {
let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() {
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 7ade6b68..fba48de1 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -2803,6 +2803,35 @@ impl Tab {
Ok(())
}
+ pub fn paste_to_pane_id(
+ &mut self,
+ bytes: Vec<u8>,
+ pane_id: PaneId,
+ completion: Option<NotificationEnd>,
+ ) -> Result<()> {
+ let bracketed_paste_begin = vec![27, 91, 50, 48, 48, 126];
+ let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126];
+
+ self.write_to_pane_id(&None, bracketed_paste_begin, false, pane_id, None, None)?;
+ self.write_to_pane_id(&None, bytes, false, pane_id, None, None)?;
+ self.write_to_pane_id(&None, bracketed_paste_end, false, pane_id, None, completion)?;
+ Ok(())
+ }
+
+ pub fn paste_to_active_terminal(
+ &mut self,
+ bytes: Vec<u8>,
+ client_id: ClientId,
+ completion: Option<NotificationEnd>,
+ ) -> Result<()> {
+ let err_context = || format!("failed to paste to active terminal for client {client_id}");
+ let active_pane_id = self
+ .get_active_pane_id(client_id)
+ .ok_or_else(|| anyhow!("no active pane for client {client_id}"))
+ .with_context(err_context)?;
+ self.paste_to_pane_id(bytes, active_pane_id, completion)
+ }
+
pub fn write_to_pane_id(
&mut self,
key_with_modifier: &Option<KeyWithModifier>,
diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs
index e7fdd27d..cb420225 100644
--- a/zellij-utils/assets/prost_ipc/client_server_contract.rs
+++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs
@@ -119,7 +119,7 @@ pub struct RgbColor {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Action {
- #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108")]
+ #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109")]
pub action_type: ::core::option::Option<action::ActionType>,
}
/// Nested message and enum types in `Action`.
@@ -343,6 +343,8 @@ pub mod action {
HideFloatingPanes(super::HideFloatingPanesAction),
#[prost(message, tag="108")]
SetPaneColor(super::SetPaneColorAction),
+ #[prost(message, tag="109")]
+ Paste(super::PasteAction),
}
}
// Action message definitions (all 92 variants)
@@ -641,6 +643,14 @@ pub struct WriteCharsToPaneIdAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PasteAction {
+ #[prost(string, tag="1")]
+ pub chars: ::prost::alloc::string::String,
+ #[prost(message, optional, tag="2")]
+ pub pane_id: ::core::option::Option<PaneId>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SwitchToModeAction {
#[prost(enumeration="InputMode", tag="1")]
pub input_mode: i32,
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 63c7777f..4d382477 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -694,6 +694,13 @@ pub enum CliAction {
#[clap(short, long, value_parser)]
pane_id: Option<String>,
},
+ /// Paste text to the terminal (using bracketed paste mode).
+ Paste {
+ chars: String,
+ /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3)
+ #[clap(short, long, value_parser)]
+ pane_id: Option<String>,
+ },
/// Send one or more keys to the terminal (e.g., "Ctrl a", "F1", "Alt Shift b")
SendKeys {
/// Keys to send as space-separated strings
diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index e5e661a9..d872d8a6 100644
--- a/zellij-utils/src/client_server_contract/common_types.proto
+++ b/zellij-utils/src/client_server_contract/common_types.proto
@@ -244,6 +244,7 @@ message Action {
ShowFloatingPanesAction show_floating_panes = 106;
HideFloatingPanesAction hide_floating_panes = 107;
SetPaneColorAction set_pane_color = 108;
+ PasteAction paste = 109;
}
}
@@ -357,6 +358,11 @@ message WriteCharsToPaneIdAction {
string chars = 2;
}
+message PasteAction {
+ string chars = 1;
+ optional PaneId pane_id = 2;
+}
+
message SwitchToModeAction {
InputMode input_mode = 1;
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 221a7b96..82016f7c 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -372,6 +372,7 @@ pub enum ScreenContext {
ResizePaneWithId,
EditScrollbackForPaneWithId,
WriteToPaneId,
+ Paste,
SetPaneColor,
WriteKeyToPaneId,
CopyTextToClipboard,
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index 5ed8504e..c722e5ff 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -137,6 +137,11 @@ pub enum Action {
chars: String,
pane_id: PaneId,
},
+ /// Paste text using bracketed paste mode, optionally to a specific pane.
+ Paste {
+ chars: String,
+ pane_id: Option<PaneId>,
+ },
/// Switch to the specified input mode.
SwitchToMode {
input_mode: InputMode,
@@ -614,6 +619,29 @@ impl Action {
},
None => Ok(vec![Action::WriteChars { chars }]),
},
+ CliAction::Paste { chars, pane_id } => match pane_id {
+ Some(pane_id_str) => {
+ let parsed_pane_id = PaneId::from_str(&pane_id_str);
+ match parsed_pane_id {
+ Ok(parsed_pane_id) => {
+ Ok(vec![Action::Paste {
+ chars,
+ pane_id: Some(parsed_pane_id),
+ }])
+ },
+ Err(_e) => {
+ Err(format!(
+ "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
+ pane_id_str
+ ))
+ }
+ }
+ },
+ None => Ok(vec![Action::Paste {
+ chars,
+ pane_id: None,
+ }]),
+ },
CliAction::SendKeys { keys, pane_id } => {
let mut actions = Vec::new();
diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs
index d843dc9d..4c8316d2 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -792,19 +792,20 @@ impl From<crate::input::actions::Action>
NewPaneAction, NewStackedPaneAction, NewTabAction, NewTiledPaneAction,
NewTiledPluginPaneAction, NextSwapLayoutAction, NoOpAction, OverrideLayoutAction,
PageScrollDownAction, PageScrollUpAction, PaneIdWithPlugin, PaneNameInputAction,
- PreviousSwapLayoutAction, QueryTabNamesAction, QuitAction, RenamePluginPaneAction,
- RenameSessionAction, RenameTabAction, RenameTabByIdAction, RenameTerminalPaneAction,
- ResizeAction, RunAction, SaveSessionAction, ScrollDownAction, ScrollDownAtAction,
- ScrollToBottomAction, ScrollToTopAction, ScrollUpAction, ScrollUpAtAction,
- SearchAction, SearchInputAction, SearchToggleOptionAction, SetPaneBorderlessAction,
- SetPaneColorAction, ShowFloatingPanesAction, SkipConfirmAction, StackPanesAction,
- StartOrReloadPluginAction, SwitchFocusAction, SwitchModeForAllClientsAction,
- SwitchSessionAction, SwitchToModeAction, TabNameInputAction, ToggleActiveSyncTabAction,
- ToggleFloatingPanesAction, ToggleFocusFullscreenAction, ToggleGroupMarkingAction,
- ToggleMouseModeAction, TogglePaneBorderlessAction, TogglePaneEmbedOrFloatingAction,
- TogglePaneFramesAction, TogglePaneInGroupAction, TogglePanePinnedAction,
- ToggleTabAction, UndoRenamePaneAction, UndoRenameTabAction, WriteAction,
- WriteCharsAction, WriteCharsToPaneIdAction, WriteToPaneIdAction,
+ PasteAction, PreviousSwapLayoutAction, QueryTabNamesAction, QuitAction,
+ RenamePluginPaneAction, RenameSessionAction, RenameTabAction, RenameTabByIdAction,
+ RenameTerminalPaneAction, ResizeAction, RunAction, SaveSessionAction, ScrollDownAction,
+ ScrollDownAtAction, ScrollToBottomAction, ScrollToTopAction, ScrollUpAction,
+ ScrollUpAtAction, SearchAction, SearchInputAction, SearchToggleOptionAction,
+ SetPaneBorderlessAction, SetPaneColorAction, ShowFloatingPanesAction,
+ SkipConfirmAction, StackPanesAction, StartOrReloadPluginAction, SwitchFocusAction,
+ SwitchModeForAllClientsAction, SwitchSessionAction, SwitchToModeAction,
+ TabNameInputAction, ToggleActiveSyncTabAction, ToggleFloatingPanesAction,
+ ToggleFocusFullscreenAction, ToggleGroupMarkingAction, ToggleMouseModeAction,
+ TogglePaneBorderlessAction, TogglePaneEmbedOrFloatingAction, TogglePaneFramesAction,
+ TogglePaneInGroupAction, TogglePanePinnedAction, ToggleTabAction, UndoRenamePaneAction,
+ UndoRenameTabAction, WriteAction, WriteCharsAction, WriteCharsToPaneIdAction,
+ WriteToPaneIdAction,
};
use std::collections::HashMap;
@@ -834,6 +835,12 @@ impl From<crate::input::actions::Action>
chars,
})
},
+ crate::input::actions::Action::Paste { chars, pane_id } => {
+ ActionType::Paste(PasteAction {
+ chars,
+ pane_id: pane_id.map(|p| p.into()),
+ })
+ },
crate::input::actions::Action::SwitchToMode { input_mode } => {
ActionType::SwitchToMode(SwitchToModeAction {
input_mode: input_mode_to_proto_i32(input_mode),
@@ -1513,6 +1520,10 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
.try_into()?,
})
},
+ ActionType::Paste(paste_action) => Ok(crate::input::actions::Action::Paste {
+ chars: paste_action.chars,
+ pane_id: paste_action.pane_id.map(|p| p.try_into()).transpose()?,
+ }),
ActionType::SwitchToMode(switch_mode_action) => {
Ok(crate::input::actions::Action::SwitchToMode {
input_mode: proto_i32_to_input_mode(switch_mode_action.input_mode)?,
diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs
index 81db2c67..b115b86d 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -1056,10 +1056,11 @@ impl TryFrom<Action> for ProtobufAction {
}),
Action::WriteToPaneId { .. }
| Action::WriteCharsToPaneId { .. }
+ | Action::Paste { .. }
| Action::GoToTabById { .. }
| Action::CloseTabById { .. }
| Action::RenameTabById { .. } => {
- Err("WriteToPaneId, WriteCharsToPaneId, GoToTabById, CloseTabById, and RenameTabById are CLI-only actions, not available in keybindings")
+ Err("WriteToPaneId, WriteCharsToPaneId, Paste, GoToTabById, CloseTabById, and RenameTabById are CLI-only actions, not available in keybindings")
},
Action::SwitchToMode { input_mode } => {
let input_mode: ProtobufInputMode = input_mode.try_into()?;