aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-10-09 11:29:55 +0200
committerGitHub <noreply@github.com>2025-10-09 11:29:55 +0200
commit7e66ae3125bf5031b6c013dee8038c838b42bbc5 (patch)
tree061c2311461cbe52b40b8ad433023085cfae67bf
parentbuild(cargo): Update Rust toolchain to 1.90.0 (#4457) (diff)
downloadsl0ppij-7e66ae3125bf5031b6c013dee8038c838b42bbc5.tar.gz
sl0ppij-7e66ae3125bf5031b6c013dee8038c838b42bbc5.tar.xz
sl0ppij-7e66ae3125bf5031b6c013dee8038c838b42bbc5.zip
Give plugins the ability to read pane contents (#4465)
* initial implementation * add Selection and better types * event to plugins * only send viewport diff * simplify data structure sent over the wire * cleanups * rustfmt * remove dead code and fix tests * commit tests * get pane scrollback plugin command without implementation * add lines above and lines below to pane contents * implement get scrollback plugin command * selection => selected_text * add convenience method to get the selected text * move Selection back to zellij-server * rustfmt * bring back client id to get contents command * rustfmt * add pr
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock1
-rw-r--r--zellij-server/Cargo.toml1
-rw-r--r--zellij-server/src/output/mod.rs19
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs22
-rw-r--r--zellij-server/src/panes/grid.rs31
-rw-r--r--zellij-server/src/panes/mod.rs1
-rw-r--r--zellij-server/src/panes/plugin_pane.rs11
-rw-r--r--zellij-server/src/panes/search.rs2
-rw-r--r--zellij-server/src/panes/terminal_pane.rs8
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs19
-rw-r--r--zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs9
-rw-r--r--zellij-server/src/plugins/mod.rs8
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs71
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs73
-rw-r--r--zellij-server/src/screen.rs52
-rw-r--r--zellij-server/src/tab/mod.rs6
-rw-r--r--zellij-tile/src/shim.rs41
-rw-r--r--zellij-utils/assets/prost/api.event.rs58
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs15
-rw-r--r--zellij-utils/assets/prost/api.plugin_permission.rs3
-rw-r--r--zellij-utils/src/data.rs229
-rw-r--r--zellij-utils/src/errors.rs2
-rw-r--r--zellij-utils/src/plugin_api/event.proto30
-rw-r--r--zellij-utils/src/plugin_api/event.rs175
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto7
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs40
-rw-r--r--zellij-utils/src/plugin_api/plugin_permission.proto1
-rw-r--r--zellij-utils/src/plugin_api/plugin_permission.rs2
29 files changed, 906 insertions, 32 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ce11a9f..745ad0e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* fix: properly serialize/resurrect layouts with one-line split panes (https://github.com/zellij-org/zellij/pull/4442)
* feat: allow attaching to remote Zellij sessions over https (eg. `zellij attach https://example.com/my-cool-session`) (https://github.com/zellij-org/zellij/pull/4460)
* build: Update Rust toolchain to 1.90.0 (https://github.com/zellij-org/zellij/pull/4457)
+* feat: allow plugins to read pane scrollback (https://github.com/zellij-org/zellij/pull/4465)
## [0.43.1] - 2025-08-08
* fix: pane rename backspace regression (https://github.com/zellij-org/zellij/pull/4346)
diff --git a/Cargo.lock b/Cargo.lock
index 6cc6550c..66619051 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5905,6 +5905,7 @@ dependencies = [
"cassowary",
"chrono",
"close_fds",
+ "crossbeam",
"daemonize",
"highway",
"insta",
diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml
index 44e5d14c..9b70dce7 100644
--- a/zellij-server/Cargo.toml
+++ b/zellij-server/Cargo.toml
@@ -21,6 +21,7 @@ bytes = { version = "1.6.0", default-features = false, features = ["std"] }
cassowary = { version = "0.3.0", default-features = false }
chrono = { version = "0.4.19", default-features = false, features = ["std", "clock"] }
close_fds = { version = "0.3.2", default-features = false }
+crossbeam = { version = "0.8", default-features = false, features = ["crossbeam-channel", "std"] }
daemonize = { workspace = true }
highway = { version = "0.6.4", default-features = false, features = ["std"] }
interprocess = { workspace = true }
diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs
index 537e4cf2..3ed8db69 100644
--- a/zellij-server/src/output/mod.rs
+++ b/zellij-server/src/output/mod.rs
@@ -1,12 +1,12 @@
use std::collections::VecDeque;
-use crate::panes::selection::Selection;
use crate::panes::Row;
+use crate::panes::Selection;
use crate::{
panes::sixel::SixelImageStore,
panes::terminal_character::{AnsiCode, CharacterStyles},
- panes::{LinkHandler, TerminalCharacter, DEFAULT_STYLES, EMPTY_TERMINAL_CHARACTER},
+ panes::{LinkHandler, PaneId, TerminalCharacter, DEFAULT_STYLES, EMPTY_TERMINAL_CHARACTER},
ClientId,
};
use std::cell::RefCell;
@@ -16,6 +16,7 @@ use std::{
collections::{HashMap, HashSet},
str,
};
+use zellij_utils::data::{PaneContents, PaneRenderReport};
use zellij_utils::errors::prelude::*;
use zellij_utils::pane_size::PaneGeom;
use zellij_utils::pane_size::SizeInPixels;
@@ -246,6 +247,7 @@ pub struct Output {
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
floating_panes_stack: Option<FloatingPanesStack>,
styled_underlines: bool,
+ pane_render_report: PaneRenderReport,
}
impl Output {
@@ -454,6 +456,19 @@ impl Output {
.map(|s| s.cursor_is_visible(cursor_x, cursor_y))
.unwrap_or(true)
}
+ pub fn add_pane_contents(
+ &mut self,
+ client_ids: &[ClientId],
+ pane_id: PaneId,
+ pane_contents: PaneContents,
+ ) {
+ self.pane_render_report
+ .add_pane_contents(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)
+ }
}
// this struct represents the geometry of a group of floating panes
diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs
index d8ce0c7c..a4361728 100644
--- a/zellij-server/src/panes/floating_panes/mod.rs
+++ b/zellij-server/src/panes/floating_panes/mod.rs
@@ -374,6 +374,28 @@ impl FloatingPanes {
} else {
Default::default()
};
+
+ for (kind, pane) in &self.panes {
+ match kind {
+ PaneId::Terminal(_) => {
+ output.add_pane_contents(
+ &connected_clients,
+ pane.pid(),
+ pane.pane_contents(None, false),
+ );
+ },
+ PaneId::Plugin(_) => {
+ for client_id in &connected_clients {
+ output.add_pane_contents(
+ &[*client_id],
+ pane.pid(),
+ pane.pane_contents(Some(*client_id), false),
+ );
+ }
+ },
+ }
+ }
+
let mut floating_panes: Vec<_> = if self.panes_are_visible() {
self.panes.iter_mut().collect()
} else if self.has_pinned_panes() {
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs
index c706c445..5bf26d08 100644
--- a/zellij-server/src/panes/grid.rs
+++ b/zellij-server/src/panes/grid.rs
@@ -33,12 +33,13 @@ use crate::panes::alacritty_functions::{parse_number, xparse_color};
use crate::panes::hyperlink_tracker::HyperlinkTracker;
use crate::panes::link_handler::LinkHandler;
use crate::panes::search::SearchResult;
-use crate::panes::selection::Selection;
use crate::panes::terminal_character::{
AnsiCode, CharsetIndex, Cursor, CursorShape, RcCharacterStyles, StandardCharset,
TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
};
+use crate::panes::Selection;
use crate::ui::components::UiComponentParser;
+use zellij_utils::data::PaneContents;
fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> {
let mut index_of_last_non_canonical_row = None;
@@ -2444,6 +2445,34 @@ impl Grid {
pub fn has_selection(&self) -> bool {
!self.selection.is_empty()
}
+ pub fn pane_contents(&self, get_full_scrollback: bool) -> PaneContents {
+ let mut viewport: Vec<String> = Vec::with_capacity(self.viewport.len());
+ for row in &self.viewport {
+ let s: String = (&row.columns).into_iter().map(|x| x.character).collect();
+ viewport.push(s);
+ }
+ if get_full_scrollback {
+ let mut lines_above_viewport: Vec<String> = Vec::with_capacity(self.lines_above.len());
+ for row in &self.lines_above {
+ let s: String = (&row.columns).into_iter().map(|x| x.character).collect();
+ lines_above_viewport.push(s);
+ }
+ let mut lines_below_viewport: Vec<String> = Vec::with_capacity(self.lines_below.len());
+ for row in &self.lines_below {
+ let s: String = (&row.columns).into_iter().map(|x| x.character).collect();
+ lines_below_viewport.push(s);
+ }
+ 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/mod.rs b/zellij-server/src/panes/mod.rs
index bbb74faa..86dab304 100644
--- a/zellij-server/src/panes/mod.rs
+++ b/zellij-server/src/panes/mod.rs
@@ -19,6 +19,7 @@ pub use floating_panes::*;
pub use grid::*;
pub use link_handler::*;
pub(crate) use plugin_pane::*;
+pub use selection::Selection;
pub use sixel::*;
pub(crate) use terminal_character::*;
pub use terminal_pane::*;
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index be980a2b..65372ffd 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -19,6 +19,7 @@ use crate::ClientId;
use std::cell::RefCell;
use std::rc::Rc;
use vte;
+use zellij_utils::data::PaneContents;
use zellij_utils::data::{
BareKey, KeyWithModifier, PermissionStatus, PermissionType, PluginPermission,
};
@@ -851,6 +852,16 @@ impl Pane for PluginPane {
}
}
}
+ fn pane_contents(
+ &self,
+ client_id: Option<ClientId>,
+ get_full_scrollback: bool,
+ ) -> PaneContents {
+ client_id
+ .and_then(|c| self.grids.get(&c))
+ .map(|g| g.pane_contents(get_full_scrollback))
+ .unwrap_or_else(Default::default)
+ }
}
impl PluginPane {
diff --git a/zellij-server/src/panes/search.rs b/zellij-server/src/panes/search.rs
index 573322dd..5c410ad6 100644
--- a/zellij-server/src/panes/search.rs
+++ b/zellij-server/src/panes/search.rs
@@ -1,4 +1,4 @@
-use crate::panes::selection::Selection;
+use super::Selection;
use crate::panes::terminal_character::TerminalCharacter;
use crate::panes::{Grid, Row};
use std::borrow::Cow;
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs
index fe69556f..21cdd64e 100644
--- a/zellij-server/src/panes/terminal_pane.rs
+++ b/zellij-server/src/panes/terminal_pane.rs
@@ -14,6 +14,7 @@ use std::fmt::Debug;
use std::rc::Rc;
use std::time::{self, Instant};
use vte;
+use zellij_utils::data::PaneContents;
use zellij_utils::input::command::RunCommand;
use zellij_utils::input::mouse::{MouseEvent, MouseEventType};
use zellij_utils::pane_size::Offset;
@@ -881,6 +882,13 @@ impl Pane for TerminalPane {
fn reset_logical_position(&mut self) {
self.geom.logical_position = None;
}
+ fn pane_contents(
+ &self,
+ _client_id: Option<ClientId>,
+ get_full_scrollback: bool,
+ ) -> PaneContents {
+ self.grid.pane_contents(get_full_scrollback)
+ }
}
impl TerminalPane {
diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs
index e877026f..e0388dd9 100644
--- a/zellij-server/src/panes/tiled_panes/mod.rs
+++ b/zellij-server/src/panes/tiled_panes/mod.rs
@@ -1020,6 +1020,25 @@ impl TiledPanes {
.with_context(err_context)?
};
for (kind, pane) in self.panes.iter_mut() {
+ match kind {
+ PaneId::Terminal(_) => {
+ output.add_pane_contents(
+ &connected_clients,
+ pane.pid(),
+ pane.pane_contents(None, false),
+ );
+ },
+ PaneId::Plugin(_) => {
+ for client_id in &connected_clients {
+ output.add_pane_contents(
+ &[*client_id],
+ pane.pid(),
+ pane.pane_contents(Some(*client_id), false),
+ );
+ }
+ },
+ }
+
if !self.panes_to_hide.contains(&pane.pid()) {
let pane_is_stacked_under =
stacked_pane_ids_under_flexible_pane.contains(&pane.pid());
diff --git a/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs b/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs
index 7cc67608..6b4edf40 100644
--- a/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs
+++ b/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs
@@ -15,7 +15,7 @@ use crate::{
ClientId,
};
use std::time::Instant;
-use zellij_utils::data::{InputMode, PaletteColor};
+use zellij_utils::data::{InputMode, PaletteColor, PaneContents};
macro_rules! mock_pane {
($pane_id:expr, $dimension:expr, $inner:expr, $x:expr, $y:expr, $logical_position:expr, $mock_panes:expr) => {
@@ -1236,4 +1236,11 @@ impl Pane for MockPane {
fn custom_title(&self) -> Option<String> {
unimplemented!()
}
+ fn pane_contents(
+ &self,
+ _client_id: Option<ClientId>,
+ _get_full_scrollback: bool,
+ ) -> PaneContents {
+ unimplemented!()
+ }
}
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index 6149ac97..14fbf345 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -18,6 +18,7 @@ use crate::panes::PaneId;
use crate::screen::ScreenInstruction;
use crate::session_layout_metadata::SessionLayoutMetadata;
use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
+use zellij_utils::data::PaneRenderReport;
pub use wasm_bridge::PluginRenderAsset;
use wasm_bridge::WasmBridge;
@@ -166,6 +167,7 @@ pub enum PluginInstruction {
ChangePluginHostDir(PathBuf, PluginId, ClientId),
WebServerStarted(String), // String -> the base url of the web server
FailedToStartWebServer(String),
+ PaneRenderReport(PaneRenderReport),
Exit,
}
@@ -215,6 +217,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::ChangePluginHostDir(..) => PluginContext::ChangePluginHostDir,
PluginInstruction::WebServerStarted(..) => PluginContext::WebServerStarted,
PluginInstruction::FailedToStartWebServer(..) => PluginContext::FailedToStartWebServer,
+ PluginInstruction::PaneRenderReport(..) => PluginContext::PaneRenderReport,
}
}
}
@@ -961,6 +964,11 @@ pub(crate) fn plugin_thread_main(
.update_plugins(updates, shutdown_send.clone())
.non_fatal();
},
+ PluginInstruction::PaneRenderReport(pane_render_report) => {
+ wasm_bridge
+ .handle_pane_render_report(pane_render_report, shutdown_send.clone())
+ .non_fatal();
+ },
PluginInstruction::Exit => {
break;
},
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs
index fbb67a5e..ca9837a7 100644
--- a/zellij-server/src/plugins/wasm_bridge.rs
+++ b/zellij-server/src/plugins/wasm_bridge.rs
@@ -22,7 +22,8 @@ use std::{
use wasmtime::{Engine, Module};
use zellij_utils::consts::{ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR};
use zellij_utils::data::{
- FloatingPaneCoordinates, InputMode, PermissionStatus, PermissionType, PipeMessage, PipeSource,
+ FloatingPaneCoordinates, InputMode, PaneContents, PaneRenderReport, PermissionStatus,
+ PermissionType, PipeMessage, PipeSource,
};
use zellij_utils::downloader::Downloader;
use zellij_utils::input::keybinds::Keybinds;
@@ -111,6 +112,7 @@ pub struct WasmBridge {
keybinds: HashMap<ClientId, Keybinds>,
base_modes: HashMap<ClientId, InputMode>,
downloader: Downloader,
+ previous_pane_render_report: Option<PaneRenderReport>,
}
impl WasmBridge {
@@ -163,6 +165,7 @@ impl WasmBridge {
keybinds: HashMap::new(),
base_modes: HashMap::new(),
downloader,
+ previous_pane_render_report: None,
}
}
pub fn load_plugin(
@@ -1033,7 +1036,72 @@ impl WasmBridge {
.lock()
.unwrap()
.retain(|c| c != &client_id);
+
+ // Remove client from cached pane render report
+ if let Some(ref mut prev_report) = self.previous_pane_render_report {
+ prev_report.all_pane_contents.remove(&client_id);
+ }
}
+
+ fn get_changed_panes_per_client(
+ &self,
+ new_report: &PaneRenderReport,
+ ) -> 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 {
+ result.insert(*client_id, panes.clone());
+ }
+ return result;
+ };
+
+ // Compare each client's panes
+ for (client_id, new_panes) in &new_report.all_pane_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
+ .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
+ })
+ .unwrap_or(true); // New pane - treat as changed
+
+ if has_changed {
+ client_panes.insert(*pane_id, new_contents.clone());
+ }
+ }
+
+ if !client_panes.is_empty() {
+ result.insert(*client_id, client_panes);
+ }
+ }
+
+ result
+ }
+
+ pub fn handle_pane_render_report(
+ &mut self,
+ pane_render_report: PaneRenderReport,
+ shutdown_sender: Sender<()>,
+ ) -> Result<()> {
+ let changed_panes_per_client = self.get_changed_panes_per_client(&pane_render_report);
+ 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())?;
+ }
+ self.previous_pane_render_report = Some(pane_render_report);
+ Ok(())
+ }
+
pub fn cleanup(&mut self) {
for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() {
drop(loading_plugin_task.cancel());
@@ -1603,6 +1671,7 @@ fn check_event_permission(
| Event::CommandPaneReRun(..)
| Event::InputReceived => PermissionType::ReadApplicationState,
Event::WebServerStatus(..) => PermissionType::StartWebServer,
+ Event::PaneRenderReport(..) => PermissionType::ReadPaneContents,
_ => return (PermissionStatus::Granted, None),
};
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index ee76fa05..7c287b0d 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -21,7 +21,8 @@ use std::{
use wasmtime::{Caller, Linker};
use zellij_utils::data::{
CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, KeyWithModifier, LayoutInfo,
- MessageToPlugin, OriginatingPlugin, PermissionStatus, PermissionType, PluginPermission,
+ MessageToPlugin, OriginatingPlugin, PaneScrollbackResponse, PermissionStatus, PermissionType,
+ PluginPermission,
};
use zellij_utils::input::permission::PermissionCache;
use zellij_utils::ipc::{ClientToServerMsg, IpcSenderWithContext};
@@ -48,6 +49,7 @@ use zellij_utils::{
layout::{Layout, RunPluginOrAlias},
},
plugin_api::{
+ event::ProtobufPaneScrollbackResponse,
plugin_command::ProtobufPluginCommand,
plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion},
},
@@ -328,6 +330,10 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
PluginCommand::EditScrollbackForPaneWithId(pane_id) => {
edit_scrollback_for_pane_with_id(env, pane_id.into())
},
+ PluginCommand::GetPaneScrollback {
+ pane_id,
+ get_full_scrollback,
+ } => get_pane_scrollback(env, pane_id.into(), get_full_scrollback),
PluginCommand::WriteToPaneId(bytes, pane_id) => {
write_to_pane_id(env, bytes, pane_id.into())
},
@@ -2122,6 +2128,70 @@ fn edit_scrollback_for_pane_with_id(env: &PluginEnv, pane_id: PaneId) {
.send_to_screen(ScreenInstruction::EditScrollbackForPaneWithId(pane_id));
}
+fn get_pane_scrollback(env: &PluginEnv, pane_id: PaneId, get_full_scrollback: bool) {
+ use crossbeam::channel::RecvTimeoutError;
+ use std::time::Duration;
+
+ let err_context = || {
+ format!(
+ "failed to get pane scrollback for pane {:?} from plugin {}",
+ pane_id,
+ env.name()
+ )
+ };
+
+ // Create oneshot channel for response
+ let (response_sender, response_receiver) = crossbeam::channel::bounded(1);
+
+ // Send request to screen thread
+ env.senders
+ .send_to_screen(ScreenInstruction::GetPaneScrollback {
+ pane_id,
+ client_id: env.client_id,
+ get_full_scrollback,
+ response_channel: response_sender,
+ })
+ .with_context(err_context)
+ .non_fatal();
+
+ // Block waiting for response with 5 second timeout
+ let response = match response_receiver.recv_timeout(Duration::from_secs(5)) {
+ Ok(response) => response,
+ Err(RecvTimeoutError::Timeout) => {
+ log::error!(
+ "GetPaneScrollback timed out after 5s for plugin {} requesting pane {:?}",
+ env.plugin_id,
+ pane_id
+ );
+ PaneScrollbackResponse::Err(format!(
+ "Timeout retrieving scrollback for pane {:?}",
+ pane_id
+ ))
+ },
+ Err(RecvTimeoutError::Disconnected) => {
+ log::error!(
+ "GetPaneScrollback channel disconnected for plugin {} requesting pane {:?}",
+ env.plugin_id,
+ pane_id
+ );
+ PaneScrollbackResponse::Err(format!(
+ "Channel disconnected while retrieving scrollback for pane {:?}",
+ pane_id
+ ))
+ },
+ };
+
+ // Convert to protobuf and write back to plugin
+ ProtobufPaneScrollbackResponse::try_from(response)
+ .map_err(|e| anyhow!("Failed to serialize pane scrollback response: {}", e))
+ .and_then(|serialized| {
+ wasi_write_object(env, &serialized.encode_to_vec())?;
+ Ok(())
+ })
+ .with_context(err_context)
+ .non_fatal();
+}
+
fn write_to_pane_id(env: &PluginEnv, bytes: Vec<u8>, pane_id: PaneId) {
let _ = env
.senders
@@ -2764,6 +2834,7 @@ fn check_command_permission(
PluginCommand::InterceptKeyPresses | PluginCommand::ClearKeyPressesIntercepts => {
PermissionType::InterceptInput
},
+ PluginCommand::GetPaneScrollback { .. } => PermissionType::ReadPaneContents,
_ => return (PermissionStatus::Granted, None),
};
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index e90e2cbd..efc5f6bd 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -10,8 +10,9 @@ use std::time::Duration;
use log::{debug, warn};
use zellij_utils::data::{
- Direction, FloatingPaneCoordinates, KeyWithModifier, PaneManifest, PluginPermission, Resize,
- ResizeStrategy, SessionInfo, Styling, WebSharing,
+ Direction, FloatingPaneCoordinates, KeyWithModifier, PaneContents, PaneManifest,
+ PaneScrollbackResponse, PluginPermission, Resize, ResizeStrategy, SessionInfo, Styling,
+ WebSharing,
};
use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
@@ -188,6 +189,12 @@ pub enum ScreenInstruction {
// shell
DumpLayoutToPlugin(PluginId),
EditScrollback(ClientId),
+ GetPaneScrollback {
+ pane_id: PaneId,
+ client_id: ClientId,
+ get_full_scrollback: bool,
+ response_channel: crossbeam::channel::Sender<PaneScrollbackResponse>,
+ },
ScrollUp(ClientId),
ScrollUpAt(Position, ClientId),
ScrollDown(ClientId),
@@ -490,6 +497,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::DumpLayout(..) => ScreenContext::DumpLayout,
ScreenInstruction::DumpLayoutToPlugin(..) => ScreenContext::DumpLayoutToPlugin,
ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback,
+ ScreenInstruction::GetPaneScrollback { .. } => ScreenContext::GetPaneScrollback,
ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp,
ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown,
ScreenInstruction::ScrollToBottom(..) => ScreenContext::ScrollToBottom,
@@ -1326,6 +1334,13 @@ impl Screen {
.context(err_context)
.non_fatal();
}
+
+ let pane_render_report = output.drain_pane_render_report();
+ let _ = self
+ .bus
+ .senders
+ .send_to_plugin(PluginInstruction::PaneRenderReport(pane_render_report));
+
if output.is_dirty() {
let serialized_output = output.serialize().context(err_context)?;
let _ = self
@@ -3837,6 +3852,39 @@ pub(crate) fn screen_thread_main(
screen.render(None)?;
screen.log_and_report_session_state()?;
},
+ ScreenInstruction::GetPaneScrollback {
+ pane_id,
+ client_id,
+ get_full_scrollback,
+ response_channel,
+ } => {
+ let mut pane_contents: Option<PaneContents> = None;
+ for tab in screen.get_tabs_mut().values() {
+ if let Some(pane) = tab.get_pane_with_id(pane_id) {
+ pane_contents =
+ Some(pane.pane_contents(Some(client_id), get_full_scrollback));
+ break;
+ }
+ }
+ // Send response back through channel
+ let response = match pane_contents {
+ Some(contents) => PaneScrollbackResponse::Ok(contents),
+ None => {
+ log::warn!(
+ "Plugin requested scrollback for pane {:?} but pane was not found",
+ pane_id
+ );
+ PaneScrollbackResponse::Err(format!("Pane {:?} not found", pane_id))
+ },
+ };
+ if let Err(_) = response_channel.send(response) {
+ // the plugin likely timed out and dropped the receiver
+ log::debug!(
+ "Plugin timed out before pane scrollback response was sent for pane {:?}",
+ pane_id
+ );
+ }
+ },
ScreenInstruction::ScrollUp(client_id) => {
active_tab_and_connected_client_id!(
screen,
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 3f98554f..f1232594 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -12,6 +12,7 @@ use std::env::temp_dir;
use std::net::IpAddr;
use std::path::PathBuf;
use uuid::Uuid;
+use zellij_utils::data::PaneContents;
use zellij_utils::data::{
Direction, KeyWithModifier, PaneInfo, PermissionStatus, PermissionType, PluginPermission,
ResizeStrategy, WebSharing,
@@ -626,6 +627,11 @@ pub trait Pane {
fn set_pinned(&mut self, _should_be_pinned: bool) {}
fn reset_logical_position(&mut self) {}
fn set_mouse_selection_support(&mut self, _selection_support: bool) {}
+ fn pane_contents(
+ &self,
+ client_id: Option<ClientId>,
+ _get_full_scrollback: bool,
+ ) -> PaneContents;
}
#[derive(Clone, Debug)]
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index 7afe0186..6030dcc7 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -8,6 +8,7 @@ use zellij_utils::data::*;
use zellij_utils::errors::prelude::*;
use zellij_utils::input::actions::Action;
pub use zellij_utils::plugin_api;
+use zellij_utils::plugin_api::event::ProtobufPaneScrollbackResponse;
use zellij_utils::plugin_api::plugin_command::{
CreateTokenResponse, ListTokensResponse, ProtobufPluginCommand, RenameWebTokenResponse,
RevokeAllWebTokensResponse, RevokeTokenResponse,
@@ -1045,6 +1046,46 @@ pub fn edit_scrollback_for_pane_with_id(pane_id: PaneId) {
unsafe { host_run_plugin_command() };
}
+/// Retrieves the scrollback contents from the specified pane
+///
+/// # Arguments
+/// * `pane_id` - The ID of the pane to get scrollback from
+/// * `get_full_scrollback` - Whether to retrieve the full scrollback buffer (including lines above and below viewport)
+///
+/// # Returns
+/// * `Ok(PaneContents)` - The pane contents if successful
+/// * `Err(String)` - An error message if the pane was not found, timed out, or another error occurred
+pub fn get_pane_scrollback(
+ pane_id: PaneId,
+ get_full_scrollback: bool,
+) -> Result<PaneContents, String> {
+ let plugin_command = PluginCommand::GetPaneScrollback {
+ pane_id,
+ get_full_scrollback,
+ };
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+
+ // Read response from stdin
+ let response_bytes =
+ bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
+
+ // Decode protobuf response
+ let protobuf_response = ProtobufPaneScrollbackResponse::decode(response_bytes.as_slice())
+ .map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
+
+ // Convert to Rust type
+ let response = PaneScrollbackResponse::try_from(protobuf_response)
+ .map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
+
+ // Convert Result enum to actual Result type
+ match response {
+ PaneScrollbackResponse::Ok(contents) => Ok(contents),
+ PaneScrollbackResponse::Err(error_msg) => Err(error_msg),
+ }
+}
+
/// Write bytes to the `STDIN` of the specified pane
pub fn write_to_pane_id(bytes: Vec<u8>, pane_id: PaneId) {
let plugin_command = PluginCommand::WriteToPaneId(bytes, pane_id);
diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs
index a1240693..7ce91771 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")]
+ #[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")]
pub payload: ::core::option::Option<event::Payload>,
}
/// Nested message and enum types in `Event`.
@@ -73,6 +73,8 @@ pub mod event {
FailedToStartWebServerPayload(super::FailedToStartWebServerPayload),
#[prost(message, tag="29")]
InterceptedKeyPayload(super::super::key::Key),
+ #[prost(message, tag="30")]
+ PaneRenderReportPayload(super::PaneRenderReportPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -516,6 +518,57 @@ pub struct KeyBind {
#[prost(message, repeated, tag="2")]
pub action: ::prost::alloc::vec::Vec<super::action::Action>,
}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PaneRenderReportPayload {
+ #[prost(message, repeated, tag="1")]
+ pub pane_contents: ::prost::alloc::vec::Vec<PaneContentsEntry>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PaneContentsEntry {
+ #[prost(message, optional, tag="1")]
+ pub pane_id: ::core::option::Option<PaneId>,
+ #[prost(message, optional, tag="2")]
+ pub pane_contents: ::core::option::Option<PaneContents>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PaneContents {
+ #[prost(string, repeated, tag="1")]
+ pub viewport: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+ #[prost(message, optional, tag="2")]
+ pub selected_text: ::core::option::Option<SelectedText>,
+ #[prost(string, repeated, tag="3")]
+ pub lines_above_viewport: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+ #[prost(string, repeated, tag="4")]
+ pub lines_below_viewport: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PaneScrollbackResponse {
+ #[prost(oneof="pane_scrollback_response::Response", tags="1, 2")]
+ pub response: ::core::option::Option<pane_scrollback_response::Response>,
+}
+/// Nested message and enum types in `PaneScrollbackResponse`.
+pub mod pane_scrollback_response {
+ #[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum Response {
+ #[prost(message, tag="1")]
+ Ok(super::PaneContents),
+ #[prost(string, tag="2")]
+ Err(::prost::alloc::string::String),
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct SelectedText {
+ #[prost(message, optional, tag="1")]
+ pub start: ::core::option::Option<super::action::Position>,
+ #[prost(message, optional, tag="2")]
+ pub end: ::core::option::Option<super::action::Position>,
+}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum EventType {
@@ -569,6 +622,7 @@ pub enum EventType {
BeforeClose = 32,
FailedToStartWebServer = 34,
InterceptedKeyPress = 35,
+ PaneRenderReport = 36,
}
impl EventType {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -612,6 +666,7 @@ impl EventType {
EventType::BeforeClose => "BeforeClose",
EventType::FailedToStartWebServer => "FailedToStartWebServer",
EventType::InterceptedKeyPress => "InterceptedKeyPress",
+ EventType::PaneRenderReport => "PaneRenderReport",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -652,6 +707,7 @@ impl EventType {
"BeforeClose" => Some(Self::BeforeClose),
"FailedToStartWebServer" => Some(Self::FailedToStartWebServer),
"InterceptedKeyPress" => Some(Self::InterceptedKeyPress),
+ "PaneRenderReport" => Some(Self::PaneRenderReport),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs
index b30f2838..b33d0b8e 100644
--- a/zellij-utils/assets/prost/api.plugin_command.rs
+++ b/zellij-utils/assets/prost/api.plugin_command.rs
@@ -3,7 +3,7 @@
pub struct PluginCommand {
#[prost(enumeration="CommandName", tag="1")]
pub name: i32,
- #[prost(oneof="plugin_command::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, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 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, 110, 111, 112")]
+ #[prost(oneof="plugin_command::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, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 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, 110, 111, 112, 113")]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
/// Nested message and enum types in `PluginCommand`.
@@ -215,6 +215,8 @@ pub mod plugin_command {
ReplacePaneWithExistingPanePayload(super::ReplacePaneWithExistingPanePayload),
#[prost(message, tag="112")]
NewTabPayload(super::NewTabPayload),
+ #[prost(message, tag="113")]
+ GetPaneScrollbackPayload(super::GetPaneScrollbackPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -889,6 +891,14 @@ pub struct RenameWebTokenResponse {
#[prost(string, optional, tag="2")]
pub error: ::core::option::Option<::prost::alloc::string::String>,
}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct GetPaneScrollbackPayload {
+ #[prost(message, optional, tag="1")]
+ pub pane_id: ::core::option::Option<PaneId>,
+ #[prost(bool, tag="2")]
+ pub get_full_scrollback: bool,
+}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum CommandName {
@@ -1038,6 +1048,7 @@ pub enum CommandName {
InterceptKeyPresses = 143,
ClearKeyPressesIntercepts = 144,
ReplacePaneWithExistingPane = 155,
+ GetPaneScrollback = 163,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -1192,6 +1203,7 @@ impl CommandName {
CommandName::InterceptKeyPresses => "InterceptKeyPresses",
CommandName::ClearKeyPressesIntercepts => "ClearKeyPressesIntercepts",
CommandName::ReplacePaneWithExistingPane => "ReplacePaneWithExistingPane",
+ CommandName::GetPaneScrollback => "GetPaneScrollback",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -1343,6 +1355,7 @@ impl CommandName {
"InterceptKeyPresses" => Some(Self::InterceptKeyPresses),
"ClearKeyPressesIntercepts" => Some(Self::ClearKeyPressesIntercepts),
"ReplacePaneWithExistingPane" => Some(Self::ReplacePaneWithExistingPane),
+ "GetPaneScrollback" => Some(Self::GetPaneScrollback),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost/api.plugin_permission.rs b/zellij-utils/assets/prost/api.plugin_permission.rs
index 44d0330a..643f5b99 100644
--- a/zellij-utils/assets/prost/api.plugin_permission.rs
+++ b/zellij-utils/assets/prost/api.plugin_permission.rs
@@ -14,6 +14,7 @@ pub enum PermissionType {
FullHdAccess = 10,
StartWebServer = 11,
InterceptInput = 12,
+ ReadPaneContents = 13,
}
impl PermissionType {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -35,6 +36,7 @@ impl PermissionType {
PermissionType::FullHdAccess => "FullHdAccess",
PermissionType::StartWebServer => "StartWebServer",
PermissionType::InterceptInput => "InterceptInput",
+ PermissionType::ReadPaneContents => "ReadPaneContents",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -53,6 +55,7 @@ impl PermissionType {
"FullHdAccess" => Some(Self::FullHdAccess),
"StartWebServer" => Some(Self::StartWebServer),
"InterceptInput" => Some(Self::InterceptInput),
+ "ReadPaneContents" => Some(Self::ReadPaneContents),
_ => None,
}
}
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 3328e1de..8632c7a0 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -4,6 +4,7 @@ use crate::input::config::ConversionError;
use crate::input::keybinds::Keybinds;
use crate::input::layout::{RunPlugin, SplitSize};
use crate::pane_size::PaneGeom;
+use crate::position::Position;
use crate::shared::{colors as default_colors, eightbit_to_rgb};
use clap::ArgEnum;
use serde::{Deserialize, Serialize};
@@ -15,6 +16,7 @@ use std::path::{Path, PathBuf};
use std::str::{self, FromStr};
use std::time::Duration;
use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString};
+use unicode_width::UnicodeWidthChar;
#[cfg(not(target_family = "wasm"))]
use termwiz::{
@@ -944,6 +946,7 @@ pub enum Event {
FailedToStartWebServer(String),
BeforeClose,
InterceptedKeyPress(KeyWithModifier),
+ PaneRenderReport(HashMap<PaneId, PaneContents>),
}
#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, ToString, Serialize, Deserialize)]
@@ -984,6 +987,7 @@ pub enum Permission {
FullHdAccess,
StartWebServer,
InterceptInput,
+ ReadPaneContents,
}
impl PermissionType {
@@ -1010,6 +1014,9 @@ impl PermissionType {
"Start a local web server to serve Zellij sessions".to_owned()
},
PermissionType::InterceptInput => "Intercept Input (keyboard & mouse)".to_owned(),
+ PermissionType::ReadPaneContents => {
+ "Read pane contents (viewport and selection)".to_owned()
+ },
}
}
}
@@ -1864,6 +1871,224 @@ impl ClientInfo {
}
}
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
+pub struct PaneRenderReport {
+ pub all_pane_contents: HashMap<ClientId, HashMap<PaneId, PaneContents>>,
+}
+
+impl PaneRenderReport {
+ pub fn add_pane_contents(
+ &mut self,
+ client_ids: &[ClientId],
+ pane_id: PaneId,
+ pane_contents: PaneContents,
+ ) {
+ for client_id in client_ids {
+ let p = self
+ .all_pane_contents
+ .entry(*client_id)
+ .or_insert_with(|| HashMap::new());
+ p.insert(pane_id, pane_contents.clone());
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
+pub struct PaneContents {
+ // NOTE: both lines_above_viewport and lines_below_viewport are only populated if explicitly
+ // requested (eg. with get_full_scrollback true in the plugin command) this is for performance
+ // reasons
+ pub lines_above_viewport: Vec<String>,
+ pub lines_below_viewport: Vec<String>,
+ pub viewport: Vec<String>,
+ pub selected_text: Option<SelectedText>,
+}
+
+/// Extract text from a line between two column positions, accounting for wide characters
+fn extract_text_by_columns(line: &str, start_col: usize, end_col: usize) -> String {
+ let mut current_col = 0;
+ let mut result = String::new();
+ let mut capturing = false;
+
+ for ch in line.chars() {
+ let char_width = ch.width().unwrap_or(0);
+
+ // Start capturing when we reach start_col
+ if current_col >= start_col && !capturing {
+ capturing = true;
+ }
+
+ // Stop if we've reached or passed end_col
+ if current_col >= end_col {
+ break;
+ }
+
+ // Capture character if we're in the range
+ if capturing {
+ result.push(ch);
+ }
+
+ current_col += char_width;
+ }
+
+ result
+}
+
+/// Extract text from a line starting at a column position, accounting for wide characters
+fn extract_text_from_column(line: &str, start_col: usize) -> String {
+ let mut current_col = 0;
+ let mut result = String::new();
+ let mut capturing = false;
+
+ for ch in line.chars() {
+ let char_width = ch.width().unwrap_or(0);
+
+ if current_col >= start_col {
+ capturing = true;
+ }
+
+ if capturing {
+ result.push(ch);
+ }
+
+ current_col += char_width;
+ }
+
+ result
+}
+
+/// Extract text from a line up to a column position, accounting for wide characters
+fn extract_text_to_column(line: &str, end_col: usize) -> String {
+ let mut current_col = 0;
+ let mut result = String::new();
+
+ for ch in line.chars() {
+ let char_width = ch.width().unwrap_or(0);
+
+ if current_col >= end_col {
+ break;
+ }
+
+ result.push(ch);
+ current_col += char_width;
+ }
+
+ result
+}
+
+impl PaneContents {
+ pub fn new(viewport: Vec<String>, selection_start: Position, selection_end: Position) -> Self {
+ PaneContents {
+ viewport,
+ selected_text: SelectedText::from_positions(selection_start, selection_end),
+ ..Default::default()
+ }
+ }
+ pub fn new_with_scrollback(
+ viewport: Vec<String>,
+ selection_start: Position,
+ selection_end: Position,
+ lines_above_viewport: Vec<String>,
+ lines_below_viewport: Vec<String>,
+ ) -> Self {
+ PaneContents {
+ viewport,
+ selected_text: SelectedText::from_positions(selection_start, selection_end),
+ lines_above_viewport,
+ lines_below_viewport,
+ }
+ }
+
+ /// Returns the actual text content of the selection, if any exists.
+ /// Selection only occurs within the viewport.
+ pub fn get_selected_text(&self) -> Option<String> {
+ let selected_text = self.selected_text?;
+
+ let start_line = selected_text.start.line() as usize;
+ let start_col = selected_text.start.column();
+ let end_line = selected_text.end.line() as usize;
+ let end_col = selected_text.end.column();
+
+ // Handle out of bounds
+ if start_line >= self.viewport.len() || end_line >= self.viewport.len() {
+ return None;
+ }
+
+ if start_line == end_line {
+ // Single line selection
+ let line = &self.viewport[start_line];
+ Some(extract_text_by_columns(line, start_col, end_col))
+ } else {
+ // Multi-line selection
+ let mut result = String::new();
+
+ // First line - from start column to end of line
+ let first_line = &self.viewport[start_line];
+ result.push_str(&extract_text_from_column(first_line, start_col));
+ result.push('\n');
+
+ // Middle lines - complete lines
+ for i in (start_line + 1)..end_line {
+ result.push_str(&self.viewport[i]);
+ result.push('\n');
+ }
+
+ // Last line - from start to end column
+ let last_line = &self.viewport[end_line];
+ result.push_str(&extract_text_to_column(last_line, end_col));
+
+ Some(result)
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum PaneScrollbackResponse {
+ Ok(PaneContents),
+ Err(String),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SelectedText {
+ pub start: Position,
+ pub end: Position,
+}
+
+impl SelectedText {
+ pub fn new(start: Position, end: Position) -> Self {
+ // Normalize: ensure start <= end
+ let (normalized_start, normalized_end) = if start <= end {
+ (start, end)
+ } else {
+ (end, start)
+ };
+
+ // Normalize negative line values to 0
+ // (column is already usize so can't be negative)
+ let normalized_start = Position::new(
+ normalized_start.line().max(0) as i32,
+ normalized_start.column() as u16,
+ );
+ let normalized_end = Position::new(
+ normalized_end.line().max(0) as i32,
+ normalized_end.column() as u16,
+ );
+
+ SelectedText {
+ start: normalized_start,
+ end: normalized_end,
+ }
+ }
+
+ pub fn from_positions(start: Position, end: Position) -> Option<Self> {
+ if start == end {
+ None
+ } else {
+ Some(Self::new(start, end))
+ }
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub struct PluginIds {
pub plugin_id: u32,
@@ -2467,6 +2692,10 @@ pub enum PluginCommand {
RerunCommandPane(u32), // u32 - terminal pane id
ResizePaneIdWithDirection(ResizeStrategy, PaneId),
EditScrollbackForPaneWithId(PaneId),
+ GetPaneScrollback {
+ pane_id: PaneId,
+ get_full_scrollback: bool,
+ },
WriteToPaneId(Vec<u8>, PaneId),
WriteCharsToPaneId(String, PaneId),
MovePaneWithPaneId(PaneId),
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 4980724a..624ef414 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -260,6 +260,7 @@ pub enum ScreenContext {
DumpScreen,
DumpLayout,
EditScrollback,
+ GetPaneScrollback,
ScrollUp,
ScrollUpAt,
ScrollDown,
@@ -454,6 +455,7 @@ pub enum PluginContext {
ChangePluginHostDir,
WebServerStarted,
FailedToStartWebServer,
+ PaneRenderReport,
}
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto
index 52c1801c..f5059131 100644
--- a/zellij-utils/src/plugin_api/event.proto
+++ b/zellij-utils/src/plugin_api/event.proto
@@ -58,6 +58,7 @@ enum EventType {
BeforeClose = 32;
FailedToStartWebServer = 34;
InterceptedKeyPress = 35;
+ PaneRenderReport = 36;
}
message EventNameList {
@@ -95,6 +96,7 @@ message Event {
WebServerStatusPayload web_server_status_payload = 27;
FailedToStartWebServerPayload failed_to_start_web_server_payload = 28;
key.Key intercepted_key_payload = 29;
+ PaneRenderReportPayload pane_render_report_payload = 30;
}
}
@@ -388,3 +390,31 @@ message KeyBind {
key.Key key = 1;
repeated action.Action action = 2;
}
+
+message PaneRenderReportPayload {
+ repeated PaneContentsEntry pane_contents = 1;
+}
+
+message PaneContentsEntry {
+ PaneId pane_id = 1;
+ PaneContents pane_contents = 2;
+}
+
+message PaneContents {
+ repeated string viewport = 1;
+ optional SelectedText selected_text = 2;
+ repeated string lines_above_viewport = 3;
+ repeated string lines_below_viewport = 4;
+}
+
+message PaneScrollbackResponse {
+ oneof response {
+ PaneContents ok = 1;
+ string err = 2;
+ }
+}
+
+message SelectedText {
+ action.Position start = 1;
+ action.Position end = 2;
+}
diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs
index fe8822e4..02ef1842 100644
--- a/zellij-utils/src/plugin_api/event.rs
+++ b/zellij-utils/src/plugin_api/event.rs
@@ -1,18 +1,21 @@
pub use super::generated_api::api::{
action::{Action as ProtobufAction, Position as ProtobufPosition},
event::{
- event::Payload as ProtobufEventPayload, ClientInfo as ProtobufClientInfo,
- ClientTabHistory as ProtobufClientTabHistory, CopyDestination as ProtobufCopyDestination,
- Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
- EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata,
- InputModeKeybinds as ProtobufInputModeKeybinds, KeyBind as ProtobufKeyBind,
- LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload,
- PaneId as ProtobufPaneId, PaneInfo as ProtobufPaneInfo,
- PaneManifest as ProtobufPaneManifest, PaneType as ProtobufPaneType,
+ event::Payload as ProtobufEventPayload, pane_scrollback_response,
+ ClientInfo as ProtobufClientInfo, ClientTabHistory as ProtobufClientTabHistory,
+ CopyDestination as ProtobufCopyDestination, Event as ProtobufEvent,
+ EventNameList as ProtobufEventNameList, EventType as ProtobufEventType,
+ FileMetadata as ProtobufFileMetadata, InputModeKeybinds as ProtobufInputModeKeybinds,
+ KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
+ ModeUpdatePayload as ProtobufModeUpdatePayload, PaneContents as ProtobufPaneContents,
+ PaneContentsEntry as ProtobufPaneContentsEntry, PaneId as ProtobufPaneId,
+ PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
+ PaneRenderReportPayload as ProtobufPaneRenderReportPayload,
+ PaneScrollbackResponse as ProtobufPaneScrollbackResponse, PaneType as ProtobufPaneType,
PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession,
- SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo,
- WebServerStatusPayload as ProtobufWebServerStatusPayload, WebSharing as ProtobufWebSharing,
- *,
+ SelectedText as ProtobufSelectedText, SessionManifest as ProtobufSessionManifest,
+ TabInfo as ProtobufTabInfo, WebServerStatusPayload as ProtobufWebServerStatusPayload,
+ WebSharing as ProtobufWebSharing, *,
},
input_mode::InputMode as ProtobufInputMode,
key::Key as ProtobufKey,
@@ -21,8 +24,9 @@ pub use super::generated_api::api::{
#[allow(hidden_glob_reexports)]
use crate::data::{
ClientInfo, CopyDestination, Event, EventType, FileMetadata, InputMode, KeyWithModifier,
- LayoutInfo, ModeInfo, Mouse, PaneId, PaneInfo, PaneManifest, PermissionStatus,
- PluginCapabilities, PluginInfo, SessionInfo, Style, TabInfo, WebServerStatus, WebSharing,
+ LayoutInfo, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo, PaneManifest,
+ PaneScrollbackResponse, PermissionStatus, PluginCapabilities, PluginInfo, SelectedText,
+ SessionInfo, Style, TabInfo, WebServerStatus, WebSharing,
};
use crate::errors::prelude::*;
@@ -387,6 +391,12 @@ impl TryFrom<ProtobufEvent> for Event {
},
_ => Err("Malformed payload for the InterceptedKeyPress Event"),
},
+ Some(ProtobufEventType::PaneRenderReport) => match protobuf_event.payload {
+ Some(ProtobufEventPayload::PaneRenderReportPayload(protobuf_payload)) => {
+ Ok(Event::PaneRenderReport(protobuf_payload.try_into()?))
+ },
+ _ => Err("Malformed payload for the PaneRenderReport Event"),
+ },
None => Err("Unknown Protobuf Event"),
}
}
@@ -781,6 +791,12 @@ impl TryFrom<Event> for ProtobufEvent {
name: ProtobufEventType::InterceptedKeyPress as i32,
payload: Some(event::Payload::KeyPayload(key.try_into()?)),
}),
+ Event::PaneRenderReport(pane_contents_map) => Ok(ProtobufEvent {
+ name: ProtobufEventType::PaneRenderReport as i32,
+ payload: Some(event::Payload::PaneRenderReportPayload(
+ pane_contents_map.try_into()?,
+ )),
+ }),
}
}
}
@@ -1459,6 +1475,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::BeforeClose => EventType::BeforeClose,
ProtobufEventType::FailedToStartWebServer => EventType::FailedToStartWebServer,
ProtobufEventType::InterceptedKeyPress => EventType::InterceptedKeyPress,
+ ProtobufEventType::PaneRenderReport => EventType::PaneRenderReport,
})
}
}
@@ -1502,6 +1519,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::BeforeClose => ProtobufEventType::BeforeClose,
EventType::FailedToStartWebServer => ProtobufEventType::FailedToStartWebServer,
EventType::InterceptedKeyPress => ProtobufEventType::InterceptedKeyPress,
+ EventType::PaneRenderReport => ProtobufEventType::PaneRenderReport,
})
}
}
@@ -2243,3 +2261,134 @@ impl TryFrom<ProtobufWebServerStatusPayload> for WebServerStatus {
}
}
}
+
+impl TryFrom<ProtobufPaneRenderReportPayload> for HashMap<PaneId, PaneContents> {
+ type Error = &'static str;
+ fn try_from(protobuf_payload: ProtobufPaneRenderReportPayload) -> Result<Self, &'static str> {
+ let mut pane_contents_map = HashMap::new();
+
+ for entry in protobuf_payload.pane_contents {
+ let pane_id = entry
+ .pane_id
+ .ok_or("Missing pane_id in PaneContentsEntry")?
+ .try_into()?;
+ let pane_contents = entry
+ .pane_contents
+ .ok_or("Missing pane_contents in PaneContentsEntry")?
+ .try_into()?;
+ pane_contents_map.insert(pane_id, pane_contents);
+ }
+
+ Ok(pane_contents_map)
+ }
+}
+
+impl TryFrom<HashMap<PaneId, PaneContents>> for ProtobufPaneRenderReportPayload {
+ type Error = &'static str;
+ fn try_from(pane_contents_map: HashMap<PaneId, PaneContents>) -> Result<Self, &'static str> {
+ let mut pane_contents_vec = vec![];
+
+ for (pane_id, pane_contents) in pane_contents_map {
+ pane_contents_vec.push(ProtobufPaneContentsEntry {
+ pane_id: Some(pane_id.try_into()?),
+ pane_contents: Some(pane_contents.try_into()?),
+ });
+ }
+
+ Ok(ProtobufPaneRenderReportPayload {
+ pane_contents: pane_contents_vec,
+ })
+ }
+}
+
+impl TryFrom<ProtobufPaneContents> for PaneContents {
+ type Error = &'static str;
+ fn try_from(protobuf_contents: ProtobufPaneContents) -> Result<Self, &'static str> {
+ let selected_text = protobuf_contents
+ .selected_text
+ .map(|st| st.try_into())
+ .transpose()?;
+
+ Ok(PaneContents {
+ viewport: protobuf_contents.viewport,
+ selected_text,
+ lines_above_viewport: protobuf_contents.lines_above_viewport,
+ lines_below_viewport: protobuf_contents.lines_below_viewport,
+ })
+ }
+}
+
+impl TryFrom<PaneContents> for ProtobufPaneContents {
+ type Error = &'static str;
+ fn try_from(pane_contents: PaneContents) -> Result<Self, &'static str> {
+ let selected_text = pane_contents
+ .selected_text
+ .map(|st| st.try_into())
+ .transpose()?;
+
+ Ok(ProtobufPaneContents {
+ viewport: pane_contents.viewport,
+ selected_text,
+ lines_above_viewport: pane_contents.lines_above_viewport,
+ lines_below_viewport: pane_contents.lines_below_viewport,
+ })
+ }
+}
+
+impl TryFrom<ProtobufPaneScrollbackResponse> for PaneScrollbackResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufPaneScrollbackResponse) -> Result<Self, &'static str> {
+ match protobuf_response.response {
+ Some(pane_scrollback_response::Response::Ok(pane_contents)) => {
+ Ok(PaneScrollbackResponse::Ok(pane_contents.try_into()?))
+ },
+ Some(pane_scrollback_response::Response::Err(error_msg)) => {
+ Ok(PaneScrollbackResponse::Err(error_msg))
+ },
+ None => Err("PaneScrollbackResponse missing response field"),
+ }
+ }
+}
+
+impl TryFrom<PaneScrollbackResponse> for ProtobufPaneScrollbackResponse {
+ type Error = &'static str;
+ fn try_from(response: PaneScrollbackResponse) -> Result<Self, &'static str> {
+ let response_field = match response {
+ PaneScrollbackResponse::Ok(pane_contents) => {
+ pane_scrollback_response::Response::Ok(pane_contents.try_into()?)
+ },
+ PaneScrollbackResponse::Err(error_msg) => {
+ pane_scrollback_response::Response::Err(error_msg)
+ },
+ };
+ Ok(ProtobufPaneScrollbackResponse {
+ response: Some(response_field),
+ })
+ }
+}
+
+impl TryFrom<ProtobufSelectedText> for SelectedText {
+ type Error = &'static str;
+ fn try_from(protobuf_selected_text: ProtobufSelectedText) -> Result<Self, &'static str> {
+ Ok(SelectedText {
+ start: protobuf_selected_text
+ .start
+ .ok_or("Missing start in SelectedText")?
+ .try_into()?,
+ end: protobuf_selected_text
+ .end
+ .ok_or("Missing end in SelectedText")?
+ .try_into()?,
+ })
+ }
+}
+
+impl TryFrom<SelectedText> for ProtobufSelectedText {
+ type Error = &'static str;
+ fn try_from(selected_text: SelectedText) -> Result<Self, &'static str> {
+ Ok(ProtobufSelectedText {
+ start: Some(selected_text.start.try_into()?),
+ end: Some(selected_text.end.try_into()?),
+ })
+ }
+}
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index 49ab98c6..7ffe2fc1 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -159,6 +159,7 @@ enum CommandName {
InterceptKeyPresses = 143;
ClearKeyPressesIntercepts = 144;
ReplacePaneWithExistingPane = 155;
+ GetPaneScrollback = 163;
}
message PluginCommand {
@@ -266,6 +267,7 @@ message PluginCommand {
RenameWebLoginTokenPayload rename_web_login_token_payload = 110;
ReplacePaneWithExistingPanePayload replace_pane_with_existing_pane_payload = 111;
NewTabPayload new_tab_payload = 112;
+ GetPaneScrollbackPayload get_pane_scrollback_payload = 113;
}
}
@@ -703,3 +705,8 @@ message RenameWebTokenResponse {
bool successfully_renamed = 1;
optional string error = 2;
}
+
+message GetPaneScrollbackPayload {
+ PaneId pane_id = 1;
+ bool get_full_scrollback = 2;
+}
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index 4fc9d939..c1ba877e 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -12,14 +12,14 @@ pub use super::generated_api::api::{
FixedOrPercent as ProtobufFixedOrPercent,
FixedOrPercentValue as ProtobufFixedOrPercentValue, FloatMultiplePanesPayload,
FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, GenerateWebLoginTokenPayload,
- GroupAndUngroupPanesPayload, HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload,
- HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload,
- ListTokensResponse, LoadNewPluginPayload, MessageToPluginPayload,
- MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, MovePayload,
- NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload, NewTabsWithLayoutInfoPayload,
- OpenCommandPaneFloatingNearPluginPayload, OpenCommandPaneInPlaceOfPluginPayload,
- OpenCommandPaneNearPluginPayload, OpenCommandPanePayload,
- OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload,
+ GetPaneScrollbackPayload, GroupAndUngroupPanesPayload, HidePaneWithIdPayload,
+ HighlightAndUnhighlightPanesPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName,
+ KeyToRebind, KeyToUnbind, KillSessionsPayload, ListTokensResponse, LoadNewPluginPayload,
+ MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload,
+ MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload,
+ NewTabsWithLayoutInfoPayload, OpenCommandPaneFloatingNearPluginPayload,
+ OpenCommandPaneInPlaceOfPluginPayload, OpenCommandPaneNearPluginPayload,
+ OpenCommandPanePayload, OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload,
OpenFileNearPluginPayload, OpenFilePayload, OpenTerminalFloatingNearPluginPayload,
OpenTerminalInPlaceOfPluginPayload, OpenTerminalNearPluginPayload,
PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId,
@@ -1109,6 +1109,18 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
},
_ => Err("Mismatched payload for EditScrollback"),
},
+ Some(CommandName::GetPaneScrollback) => match protobuf_plugin_command.payload {
+ Some(Payload::GetPaneScrollbackPayload(get_pane_scrollback_payload)) => {
+ match get_pane_scrollback_payload.pane_id {
+ Some(pane_id) => Ok(PluginCommand::GetPaneScrollback {
+ pane_id: pane_id.try_into()?,
+ get_full_scrollback: get_pane_scrollback_payload.get_full_scrollback,
+ }),
+ _ => Err("Malformed get_pane_scrollback_payload"),
+ }
+ },
+ _ => Err("Mismatched payload for GetPaneScrollback"),
+ },
Some(CommandName::WriteToPaneId) => match protobuf_plugin_command.payload {
Some(Payload::WriteToPaneIdPayload(write_to_pane_id_payload)) => {
match write_to_pane_id_payload.pane_id {
@@ -2373,6 +2385,18 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
},
)),
}),
+ PluginCommand::GetPaneScrollback {
+ pane_id,
+ get_full_scrollback,
+ } => Ok(ProtobufPluginCommand {
+ name: CommandName::GetPaneScrollback as i32,
+ payload: Some(Payload::GetPaneScrollbackPayload(
+ GetPaneScrollbackPayload {
+ pane_id: Some(pane_id.try_into()?),
+ get_full_scrollback,
+ },
+ )),
+ }),
PluginCommand::WriteToPaneId(bytes_to_write, pane_id) => Ok(ProtobufPluginCommand {
name: CommandName::WriteToPaneId as i32,
payload: Some(Payload::WriteToPaneIdPayload(WriteToPaneIdPayload {
diff --git a/zellij-utils/src/plugin_api/plugin_permission.proto b/zellij-utils/src/plugin_api/plugin_permission.proto
index 0f53bb55..85126830 100644
--- a/zellij-utils/src/plugin_api/plugin_permission.proto
+++ b/zellij-utils/src/plugin_api/plugin_permission.proto
@@ -16,4 +16,5 @@ enum PermissionType {
FullHdAccess = 10;
StartWebServer = 11;
InterceptInput = 12;
+ ReadPaneContents = 13;
}
diff --git a/zellij-utils/src/plugin_api/plugin_permission.rs b/zellij-utils/src/plugin_api/plugin_permission.rs
index 926f993f..307d3576 100644
--- a/zellij-utils/src/plugin_api/plugin_permission.rs
+++ b/zellij-utils/src/plugin_api/plugin_permission.rs
@@ -28,6 +28,7 @@ impl TryFrom<ProtobufPermissionType> for PermissionType {
ProtobufPermissionType::FullHdAccess => Ok(PermissionType::FullHdAccess),
ProtobufPermissionType::StartWebServer => Ok(PermissionType::StartWebServer),
ProtobufPermissionType::InterceptInput => Ok(PermissionType::InterceptInput),
+ ProtobufPermissionType::ReadPaneContents => Ok(PermissionType::ReadPaneContents),
}
}
}
@@ -57,6 +58,7 @@ impl TryFrom<PermissionType> for ProtobufPermissionType {
PermissionType::FullHdAccess => Ok(ProtobufPermissionType::FullHdAccess),
PermissionType::StartWebServer => Ok(ProtobufPermissionType::StartWebServer),
PermissionType::InterceptInput => Ok(ProtobufPermissionType::InterceptInput),
+ PermissionType::ReadPaneContents => Ok(ProtobufPermissionType::ReadPaneContents),
}
}
}