diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-03-08 21:18:05 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-08 21:18:05 +0100 |
| commit | a8d99b64a3fe73284b0954da7daabf04da1c432d (patch) | |
| tree | c03e80df9f2673253d402cc9077741694390c97e | |
| parent | Allow subscribing to pane viewport updates (renders) from the CLI (#4814) (diff) | |
| download | sl0ppij-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.md | 1 | ||||
| -rw-r--r-- | zellij-server/src/route.rs | 14 | ||||
| -rw-r--r-- | zellij-server/src/screen.rs | 27 | ||||
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 29 | ||||
| -rw-r--r-- | zellij-utils/assets/prost_ipc/client_server_contract.rs | 12 | ||||
| -rw-r--r-- | zellij-utils/src/cli.rs | 7 | ||||
| -rw-r--r-- | zellij-utils/src/client_server_contract/common_types.proto | 6 | ||||
| -rw-r--r-- | zellij-utils/src/errors.rs | 1 | ||||
| -rw-r--r-- | zellij-utils/src/input/actions.rs | 28 | ||||
| -rw-r--r-- | zellij-utils/src/ipc/protobuf_conversion.rs | 37 | ||||
| -rw-r--r-- | zellij-utils/src/plugin_api/action.rs | 3 |
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()?; |
