mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-12 02:27:24 +00:00
feat: allow opting-in to reading paste buffer (default false) (#5472)
* feat: allow opting-in to reading paste buffer (default false) * add pr
This commit is contained in:
@@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
* fix: properly unbundle plugin wasm executables when stripping them (to assist distro packagers) (https://github.com/zellij-org/zellij/pull/5433)
|
||||
* feat: support OSC133, triple-mouse-click to mark whole command, configurable double-click word boundaries (https://github.com/zellij-org/zellij/pull/5460)
|
||||
* fix(scroll): keep a pane's scroll position when leaving scroll mode, and re-enter scroll mode when focusing a scrolled pane (https://github.com/zellij-org/zellij/pull/5299)
|
||||
* feat: allow opting-in to reading paste buffer (default false) (https://github.com/zellij-org/zellij/pull/5472)
|
||||
|
||||
## [0.44.3] - 2026-05-13
|
||||
* fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139)
|
||||
|
||||
@@ -186,6 +186,7 @@ pub(crate) enum ClientInstruction {
|
||||
ForwardQueryToHost {
|
||||
token: u32,
|
||||
query_bytes: Vec<u8>,
|
||||
resolve_async: bool,
|
||||
},
|
||||
EmitNestedSessionFrame(Vec<u8>),
|
||||
}
|
||||
@@ -210,8 +211,14 @@ impl From<ServerToClientMsg> for ClientInstruction {
|
||||
ServerToClientMsg::StartWebServer => ClientInstruction::StartWebServer,
|
||||
ServerToClientMsg::RenamedSession { name } => ClientInstruction::RenamedSession(name),
|
||||
ServerToClientMsg::ConfigFileUpdated => ClientInstruction::ConfigFileUpdated,
|
||||
ServerToClientMsg::ForwardQueryToHost { token, query_bytes } => {
|
||||
ClientInstruction::ForwardQueryToHost { token, query_bytes }
|
||||
ServerToClientMsg::ForwardQueryToHost {
|
||||
token,
|
||||
query_bytes,
|
||||
resolve_async,
|
||||
} => ClientInstruction::ForwardQueryToHost {
|
||||
token,
|
||||
query_bytes,
|
||||
resolve_async,
|
||||
},
|
||||
ServerToClientMsg::EmitNestedSessionFrame { payload_bytes } => {
|
||||
ClientInstruction::EmitNestedSessionFrame(payload_bytes)
|
||||
@@ -1435,7 +1442,55 @@ pub fn start_client(
|
||||
},
|
||||
}
|
||||
},
|
||||
ClientInstruction::ForwardQueryToHost { token, query_bytes } => {
|
||||
ClientInstruction::ForwardQueryToHost {
|
||||
token,
|
||||
query_bytes,
|
||||
resolve_async: true,
|
||||
} => {
|
||||
{
|
||||
let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap();
|
||||
if let Some((stale_token, stale_reply_bytes)) =
|
||||
stdin_ansi_parser.take_active_clipboard_forward()
|
||||
{
|
||||
log::warn!(
|
||||
"clipboard forward slot for token {} was still open when token {} was \
|
||||
dispatched ({} accumulated bytes); closing it out",
|
||||
stale_token,
|
||||
token,
|
||||
stale_reply_bytes.len(),
|
||||
);
|
||||
let _ = send_input_instructions.send(
|
||||
InputInstruction::ForwardedReplyFromHostComplete {
|
||||
token: stale_token,
|
||||
reply_bytes: stale_reply_bytes,
|
||||
},
|
||||
);
|
||||
}
|
||||
stdin_ansi_parser.open_clipboard_forward(token);
|
||||
}
|
||||
let runtime = stdin_ansi_parser::forward_timeout_runtime();
|
||||
let parser_for_timer = stdin_ansi_parser.clone();
|
||||
let sender_for_timer = send_input_instructions.clone();
|
||||
stdin_ansi_parser::schedule_clipboard_forward_timeout(
|
||||
runtime.handle(),
|
||||
parser_for_timer,
|
||||
token,
|
||||
std::time::Duration::from_millis(
|
||||
stdin_ansi_parser::CLIENT_CLIPBOARD_FORWARD_TIMEOUT_MS,
|
||||
),
|
||||
move |token, reply_bytes| {
|
||||
let _ = sender_for_timer.send(
|
||||
InputInstruction::ForwardedReplyFromHostComplete { token, reply_bytes },
|
||||
);
|
||||
},
|
||||
);
|
||||
let mut out = os_input.get_stdout_writer();
|
||||
let _ = out.write_all(&query_bytes);
|
||||
let _ = out.flush();
|
||||
},
|
||||
ClientInstruction::ForwardQueryToHost {
|
||||
token, query_bytes, ..
|
||||
} => {
|
||||
// 1. Open a forwarding window on the parser so any reply
|
||||
// events that arrive before the barrier are captured.
|
||||
// A slot may still be open here: the server's backstop
|
||||
|
||||
@@ -187,6 +187,13 @@ pub struct ForwardSlot {
|
||||
pub reply_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardForwardSlot {
|
||||
pub token: u32,
|
||||
}
|
||||
|
||||
pub const CLIENT_CLIPBOARD_FORWARD_TIMEOUT_MS: u64 = 30_000;
|
||||
|
||||
/// Return value of `feed()`.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ParseOutput {
|
||||
@@ -197,6 +204,7 @@ pub struct ParseOutput {
|
||||
/// single feed would indicate the host emitted two barriers, in which
|
||||
/// case only the first is honored.
|
||||
pub completed_forward: Option<(u32, Vec<u8>)>,
|
||||
pub completed_clipboard_forward: Option<(u32, Vec<u8>)>,
|
||||
/// OSC 99 notification-response payloads (one per OSC 99 found in
|
||||
/// the chunk). Routed by the caller as
|
||||
/// `InputInstruction::DesktopNotificationResponse`. Lives here, not
|
||||
@@ -215,6 +223,15 @@ pub struct ParseOutput {
|
||||
pub has_partial_state: bool,
|
||||
}
|
||||
|
||||
fn is_user_input(event: &InputEvent) -> bool {
|
||||
match event {
|
||||
InputEvent::Key(_) | InputEvent::Paste(_) => true,
|
||||
InputEvent::Mouse(mouse_event) => !mouse_event.mouse_buttons.is_empty(),
|
||||
InputEvent::PixelMouse(mouse_event) => !mouse_event.mouse_buttons.is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Cap on the size of an in-flight partial OSC/CSI buffer. Sized to
|
||||
/// pass legitimate OSC 52 clipboard payloads, which carry the entire
|
||||
/// clipboard base64-encoded and have no protocol-level limit (images,
|
||||
@@ -285,6 +302,7 @@ pub struct StdinAnsiParser {
|
||||
/// Active forwarding slot: `Some` while a forwarded query is in
|
||||
/// flight, `None` otherwise.
|
||||
active_forward: Option<ForwardSlot>,
|
||||
active_clipboard_forward: Option<ClipboardForwardSlot>,
|
||||
/// Bytes of an OSC sequence whose terminator hasn't arrived yet.
|
||||
/// Carried across feed() calls so the next chunk can complete it.
|
||||
partial_osc: Vec<u8>,
|
||||
@@ -312,6 +330,7 @@ impl StdinAnsiParser {
|
||||
StdinAnsiParser {
|
||||
inner: InputParser::new(),
|
||||
active_forward: None,
|
||||
active_clipboard_forward: None,
|
||||
partial_osc: Vec::new(),
|
||||
partial_csi: Vec::new(),
|
||||
partial_paste: Vec::new(),
|
||||
@@ -383,6 +402,37 @@ impl StdinAnsiParser {
|
||||
.map(|slot| (slot.token, slot.reply_bytes))
|
||||
}
|
||||
|
||||
pub fn open_clipboard_forward(&mut self, token: u32) {
|
||||
debug_assert!(
|
||||
self.active_clipboard_forward.is_none(),
|
||||
"open_clipboard_forward({}) called while slot for token {:?} is still active",
|
||||
token,
|
||||
self.active_clipboard_forward.as_ref().map(|s| s.token),
|
||||
);
|
||||
self.active_clipboard_forward = Some(ClipboardForwardSlot { token });
|
||||
}
|
||||
|
||||
pub fn close_clipboard_forward_on_timeout(&mut self, token: u32) -> Option<(u32, Vec<u8>)> {
|
||||
match &self.active_clipboard_forward {
|
||||
Some(slot) if slot.token == token => {
|
||||
let slot = self.active_clipboard_forward.take().unwrap();
|
||||
Some((slot.token, Vec::new()))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_active_clipboard_forward(&mut self) -> Option<(u32, Vec<u8>)> {
|
||||
self.active_clipboard_forward
|
||||
.take()
|
||||
.map(|slot| (slot.token, Vec::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn active_clipboard_forward_token(&self) -> Option<u32> {
|
||||
self.active_clipboard_forward.as_ref().map(|s| s.token)
|
||||
}
|
||||
|
||||
/// Currently-open slot's token, if any. Test-only inspector;
|
||||
/// production code drives slot lifecycle through `open_forward`,
|
||||
/// `close_forward_on_timeout`, and `feed()` directly.
|
||||
@@ -428,6 +478,15 @@ impl StdinAnsiParser {
|
||||
} else if let Some(reply) = HostReply::from_osc_payload(&payload) {
|
||||
out.replies.push(reply);
|
||||
}
|
||||
if payload.starts_with(b"52;") {
|
||||
if let Some(slot) = self.active_clipboard_forward.take() {
|
||||
let mut bytes = b"\x1b]".to_vec();
|
||||
bytes.extend_from_slice(&payload);
|
||||
bytes.extend_from_slice(b"\x1b\\");
|
||||
out.completed_clipboard_forward = Some((slot.token, bytes));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(slot) = self.active_forward.as_mut() {
|
||||
// Re-serialize so the pane's pty sees a legal OSC.
|
||||
// Terminators vary by host; ST (ESC \) is always safe.
|
||||
@@ -479,7 +538,16 @@ impl StdinAnsiParser {
|
||||
// input bytes that are NOT part of a classified reply. To
|
||||
// produce that residue deterministically, we re-scan the
|
||||
// buffer a second time below.
|
||||
_ => {},
|
||||
other => {
|
||||
if self.active_clipboard_forward.is_some()
|
||||
&& is_user_input(&other)
|
||||
&& out.completed_clipboard_forward.is_none()
|
||||
{
|
||||
if let Some(slot) = self.active_clipboard_forward.take() {
|
||||
out.completed_clipboard_forward = Some((slot.token, Vec::new()));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// Produce the residue: replay the input through a scratch parser
|
||||
@@ -877,6 +945,27 @@ pub fn schedule_forward_timeout<F>(
|
||||
});
|
||||
}
|
||||
|
||||
pub fn schedule_clipboard_forward_timeout<F>(
|
||||
runtime: &tokio::runtime::Handle,
|
||||
parser: Arc<Mutex<StdinAnsiParser>>,
|
||||
token: u32,
|
||||
deadline: std::time::Duration,
|
||||
on_timeout: F,
|
||||
) where
|
||||
F: FnOnce(u32, Vec<u8>) + Send + 'static,
|
||||
{
|
||||
runtime.spawn(async move {
|
||||
tokio::time::sleep(deadline).await;
|
||||
let payload = parser
|
||||
.lock()
|
||||
.unwrap()
|
||||
.close_clipboard_forward_on_timeout(token);
|
||||
if let Some((t, bytes)) = payload {
|
||||
on_timeout(t, bytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "stdin_ansi_parser_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1512,3 +1512,114 @@ fn two_nested_frames_in_one_chunk_are_both_extracted() {
|
||||
assert_eq!(out.nested_frames, vec![payload.clone(), payload]);
|
||||
assert!(out.residue.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_forward_is_closed_by_the_hosts_osc_52_reply() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(9);
|
||||
let out = parser.feed(b"\x1b]52;c;aGVsbG8=\x1b\\");
|
||||
assert_eq!(
|
||||
out.completed_clipboard_forward,
|
||||
Some((9, b"\x1b]52;c;aGVsbG8=\x1b\\".to_vec())),
|
||||
"the host's reply must be relayed verbatim (re-serialized with ST)"
|
||||
);
|
||||
assert!(out.residue.is_empty(), "clipboard data must never be typed");
|
||||
assert!(parser.active_clipboard_forward_token().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_forward_is_closed_by_the_first_key_press() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(4);
|
||||
let out = parser.feed(b"a");
|
||||
assert_eq!(out.completed_clipboard_forward, Some((4, Vec::new())));
|
||||
assert_eq!(out.residue, b"a", "the key itself must still reach the app");
|
||||
assert!(parser.active_clipboard_forward_token().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_forward_survives_host_reports_and_resizes() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(11);
|
||||
let out = parser.feed(b"\x1b[4;720;1280t");
|
||||
assert!(out.completed_clipboard_forward.is_none());
|
||||
let out = parser.feed(b"\x1b]11;rgb:0000/0000/0000\x1b\\");
|
||||
assert!(out.completed_clipboard_forward.is_none());
|
||||
let out = parser.feed(b"\x1b[?62;52c");
|
||||
assert!(
|
||||
out.completed_clipboard_forward.is_none(),
|
||||
"the Primary-DA barrier must not close a clipboard window"
|
||||
);
|
||||
assert_eq!(parser.active_clipboard_forward_token(), Some(11));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_arriving_before_the_keypress_wins() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(2);
|
||||
let mut input = Vec::new();
|
||||
input.extend_from_slice(b"\x1b]52;c;d29ybGQ=\x1b\\");
|
||||
input.extend_from_slice(b"x");
|
||||
let out = parser.feed(&input);
|
||||
assert_eq!(
|
||||
out.completed_clipboard_forward,
|
||||
Some((2, b"\x1b]52;c;d29ybGQ=\x1b\\".to_vec()))
|
||||
);
|
||||
assert_eq!(out.residue, b"x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_and_barrier_forwards_are_independent() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(1);
|
||||
parser.open_forward(2);
|
||||
let out = parser.feed(b"\x1b]11;rgb:ffff/ffff/ffff\x1b\\\x1b[?62;52c");
|
||||
assert_eq!(
|
||||
out.completed_forward,
|
||||
Some((2, b"\x1b]11;rgb:ffff/ffff/ffff\x1b\\".to_vec()))
|
||||
);
|
||||
assert!(out.completed_clipboard_forward.is_none());
|
||||
assert_eq!(parser.active_clipboard_forward_token(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_reply_is_not_accumulated_into_a_barrier_slot() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(1);
|
||||
parser.open_forward(2);
|
||||
let out = parser.feed(b"\x1b]52;c;aGk=\x1b\\");
|
||||
assert_eq!(
|
||||
out.completed_clipboard_forward,
|
||||
Some((1, b"\x1b]52;c;aGk=\x1b\\".to_vec()))
|
||||
);
|
||||
let out = parser.feed(b"\x1b[?62;52c");
|
||||
assert_eq!(
|
||||
out.completed_forward,
|
||||
Some((2, Vec::new())),
|
||||
"the clipboard payload must not leak into the barrier slot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn taking_the_clipboard_slot_hands_the_token_over() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(6);
|
||||
assert_eq!(
|
||||
parser.take_active_clipboard_forward(),
|
||||
Some((6, Vec::new()))
|
||||
);
|
||||
assert!(parser.active_clipboard_forward_token().is_none());
|
||||
assert_eq!(parser.take_active_clipboard_forward(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_timeout_only_fires_for_the_open_token() {
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
parser.open_clipboard_forward(3);
|
||||
assert_eq!(parser.close_clipboard_forward_on_timeout(4), None);
|
||||
assert_eq!(
|
||||
parser.close_clipboard_forward_on_timeout(3),
|
||||
Some((3, Vec::new()))
|
||||
);
|
||||
assert_eq!(parser.close_clipboard_forward_on_timeout(3), None);
|
||||
}
|
||||
|
||||
@@ -142,6 +142,15 @@ pub(crate) fn stdin_loop(
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some((token, reply_bytes)) = parse_output.completed_clipboard_forward
|
||||
{
|
||||
let _ = send_input_instructions.send(
|
||||
InputInstruction::ForwardedReplyFromHostComplete {
|
||||
token,
|
||||
reply_bytes,
|
||||
},
|
||||
);
|
||||
}
|
||||
for payload in parse_output.desktop_notifications {
|
||||
let _ = send_input_instructions
|
||||
.send(InputInstruction::DesktopNotificationResponse(payload));
|
||||
|
||||
@@ -77,7 +77,9 @@ fn search_input_jumps_to_a_match() {
|
||||
zellij.send_stdin(b"NEEDLE");
|
||||
|
||||
let grid_snapshot = zellij.wait_until("scrolled to the typed match", |grid_snapshot| {
|
||||
grid_snapshot.contains("line05") && grid_snapshot.contains("ENTERING SEARCH TERM")
|
||||
grid_snapshot.contains("line05")
|
||||
&& grid_snapshot.contains("ENTERING SEARCH TERM")
|
||||
&& grid_snapshot.contains("SCROLL 15/19")
|
||||
});
|
||||
assert_snapshot!(normalized(&grid_snapshot));
|
||||
zellij.quit();
|
||||
@@ -94,7 +96,9 @@ fn search_up_to_previous_match() {
|
||||
|
||||
let grid_snapshot =
|
||||
zellij.wait_until("upper match revealed by searching up", |grid_snapshot| {
|
||||
grid_snapshot.contains("NEEDLE upper") && grid_snapshot.contains("PgDn|PgUp")
|
||||
grid_snapshot.contains("NEEDLE upper")
|
||||
&& grid_snapshot.contains("PgDn|PgUp")
|
||||
&& grid_snapshot.contains("SCROLL 15/19")
|
||||
});
|
||||
assert_snapshot!(normalized(&grid_snapshot));
|
||||
zellij.quit();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: zellij-integration-tests/tests/search.rs
|
||||
expression: normalized(&grid_snapshot)
|
||||
---
|
||||
Zellij (test) Tab #1 + [ SCROLL 0/19 ]
|
||||
Zellij (test) Tab #1 + [ SCROLL 15/19 ]
|
||||
line05 NEEDLE
|
||||
line06
|
||||
line07
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: zellij-integration-tests/tests/search.rs
|
||||
expression: normalized(&grid_snapshot)
|
||||
---
|
||||
Zellij (test) Tab #1 + [ SCROLL 0/19 ]
|
||||
Zellij (test) Tab #1 + [ SCROLL 15/19 ]
|
||||
line05 NEEDLE upper
|
||||
line06
|
||||
line07
|
||||
|
||||
@@ -56,6 +56,10 @@ pub enum HostQuery {
|
||||
index: u8,
|
||||
terminator: OscTerminator,
|
||||
},
|
||||
ClipboardContent {
|
||||
selection: char,
|
||||
terminator: OscTerminator,
|
||||
},
|
||||
/// `CSI ? 996 n` — query the host terminal's color-palette theme
|
||||
/// mode (light or dark). NOT forwarded to the host. Zellij has
|
||||
/// already queried the host once at startup (via the client's
|
||||
@@ -90,6 +94,14 @@ impl HostQuery {
|
||||
v.extend_from_slice(terminator.as_bytes());
|
||||
v
|
||||
},
|
||||
HostQuery::ClipboardContent {
|
||||
selection,
|
||||
terminator,
|
||||
} => {
|
||||
let mut v = format!("\x1b]52;{};?", selection).into_bytes();
|
||||
v.extend_from_slice(terminator.as_bytes());
|
||||
v
|
||||
},
|
||||
// `ColorPaletteMode` is answered locally by Zellij and never
|
||||
// sent on the wire. Returning empty bytes keeps the call
|
||||
// total without dirtying the wire format. Callers that
|
||||
@@ -98,6 +110,20 @@ impl HostQuery {
|
||||
HostQuery::ColorPaletteMode => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_reply_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
HostQuery::ClipboardContent {
|
||||
selection,
|
||||
terminator,
|
||||
} => {
|
||||
let mut v = format!("\x1b]52;{};", selection).into_bytes();
|
||||
v.extend_from_slice(terminator.as_bytes());
|
||||
v
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -166,6 +192,56 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_content_carries_selection_and_terminator() {
|
||||
assert_eq!(
|
||||
HostQuery::ClipboardContent {
|
||||
selection: 'c',
|
||||
terminator: OscTerminator::Bel,
|
||||
}
|
||||
.to_query_bytes(),
|
||||
b"\x1b]52;c;?\x07",
|
||||
);
|
||||
assert_eq!(
|
||||
HostQuery::ClipboardContent {
|
||||
selection: 'p',
|
||||
terminator: OscTerminator::St,
|
||||
}
|
||||
.to_query_bytes(),
|
||||
b"\x1b]52;p;?\x1b\\",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_content_empty_reply_mirrors_the_query_shape() {
|
||||
assert_eq!(
|
||||
HostQuery::ClipboardContent {
|
||||
selection: 'c',
|
||||
terminator: OscTerminator::Bel,
|
||||
}
|
||||
.empty_reply_bytes(),
|
||||
b"\x1b]52;c;\x07",
|
||||
);
|
||||
assert_eq!(
|
||||
HostQuery::ClipboardContent {
|
||||
selection: 'p',
|
||||
terminator: OscTerminator::St,
|
||||
}
|
||||
.empty_reply_bytes(),
|
||||
b"\x1b]52;p;\x1b\\",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_clipboard_queries_have_no_empty_reply() {
|
||||
assert!(HostQuery::TextAreaPixelSize.empty_reply_bytes().is_empty());
|
||||
assert!(HostQuery::DefaultBackground {
|
||||
terminator: OscTerminator::St,
|
||||
}
|
||||
.empty_reply_bytes()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_bell_terminated_maps_bool_to_variant() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -138,8 +138,7 @@ pub enum ServerInstruction {
|
||||
ClearMouseHelpText(ClientId),
|
||||
/// Relay a forwarded-query dispatch from Screen to the server main
|
||||
/// loop. The main loop writes `ServerToClientMsg::ForwardQueryToHost`
|
||||
/// to any connected regular client.
|
||||
ForwardQueryToHost(u32, Vec<u8>),
|
||||
ForwardQueryToHost(u32, Vec<u8>, bool),
|
||||
KeyPassthroughChanged(ClientId, PaneId, PaneId, bool, Option<Direction>, bool),
|
||||
EmitNestedSessionFrameToClient(ClientId, Vec<u8>),
|
||||
}
|
||||
@@ -475,6 +474,10 @@ impl SessionMetaData {
|
||||
.options
|
||||
.osc133_command_selection
|
||||
.unwrap_or(true),
|
||||
dangerously_enable_paste_buffer_read: new_config
|
||||
.options
|
||||
.dangerously_enable_paste_buffer_read
|
||||
.unwrap_or(false),
|
||||
word_separators: new_config
|
||||
.options
|
||||
.word_separators
|
||||
@@ -1892,7 +1895,7 @@ pub fn start_server_impl(
|
||||
.send_to_screen(ScreenInstruction::ClearMouseHelpText(client_id))
|
||||
.unwrap();
|
||||
},
|
||||
ServerInstruction::ForwardQueryToHost(token, query_bytes) => {
|
||||
ServerInstruction::ForwardQueryToHost(token, query_bytes, resolve_async) => {
|
||||
// Pick a regular (non-watcher) client to carry the
|
||||
// forward. Preference is the most recently active
|
||||
// client (whichever last sent input); falls back to
|
||||
@@ -1912,7 +1915,11 @@ pub fn start_server_impl(
|
||||
send_to_client!(
|
||||
client_id,
|
||||
os_input,
|
||||
ServerToClientMsg::ForwardQueryToHost { token, query_bytes },
|
||||
ServerToClientMsg::ForwardQueryToHost {
|
||||
token,
|
||||
query_bytes,
|
||||
resolve_async
|
||||
},
|
||||
session_state,
|
||||
session_data
|
||||
);
|
||||
|
||||
@@ -731,6 +731,7 @@ pub struct Grid {
|
||||
pub pending_nested_session_messages: Vec<NestedSessionMessage>,
|
||||
ui_component_bytes: Option<Vec<u8>>,
|
||||
nested_frame_bytes: Option<Vec<u8>>,
|
||||
xtgettcap_bytes: Option<Vec<u8>>,
|
||||
style: Style,
|
||||
debug: bool,
|
||||
arrow_fonts: bool,
|
||||
@@ -806,6 +807,30 @@ fn osc_color_reply_body((r, g, b): (u8, u8, u8)) -> String {
|
||||
format!("rgb:{:04x}/{:04x}/{:04x}", expand(r), expand(g), expand(b))
|
||||
}
|
||||
|
||||
fn decode_hex_ascii(hex: &[u8]) -> Option<String> {
|
||||
if hex.is_empty() || hex.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
let mut out = String::with_capacity(hex.len() / 2);
|
||||
for pair in hex.chunks(2) {
|
||||
let high = (pair[0] as char).to_digit(16)?;
|
||||
let low = (pair[1] as char).to_digit(16)?;
|
||||
let byte = (high * 16 + low) as u8;
|
||||
if !byte.is_ascii_graphic() {
|
||||
return None;
|
||||
}
|
||||
out.push(byte as char);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn encode_hex_ascii(value: &str) -> String {
|
||||
value
|
||||
.bytes()
|
||||
.map(|byte| format!("{:02X}", byte))
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
/// A compiled highlight entry for one plugin/pattern combination.
|
||||
#[derive(Clone)]
|
||||
pub struct CompiledHighlight {
|
||||
@@ -1085,6 +1110,7 @@ impl Grid {
|
||||
pending_nested_session_messages: Vec::new(),
|
||||
ui_component_bytes: None,
|
||||
nested_frame_bytes: None,
|
||||
xtgettcap_bytes: None,
|
||||
style,
|
||||
debug,
|
||||
arrow_fonts,
|
||||
@@ -3906,6 +3932,19 @@ impl Grid {
|
||||
self.pending_messages_to_pty
|
||||
.push(format!("\u{1b}[?997;{}n", code).into_bytes());
|
||||
}
|
||||
fn answer_xtgettcap(&mut self, payload: &[u8]) {
|
||||
for name_hex in payload.split(|byte| *byte == b';') {
|
||||
let reply = match decode_hex_ascii(name_hex).as_deref() {
|
||||
Some("Ms") => format!(
|
||||
"\u{1b}P1+r{}={}\u{1b}\\",
|
||||
encode_hex_ascii("Ms"),
|
||||
encode_hex_ascii("\u{1b}]52;%p1%s;%p2%s\u{7}"),
|
||||
),
|
||||
_ => "\u{1b}P0+r\u{1b}\\".to_owned(),
|
||||
};
|
||||
self.pending_messages_to_pty.push(reply.into_bytes());
|
||||
}
|
||||
}
|
||||
pub fn lock_renders(&mut self) {
|
||||
self.lock_renders = true;
|
||||
}
|
||||
@@ -4100,7 +4139,9 @@ impl Perform for Grid {
|
||||
}
|
||||
|
||||
fn hook(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, c: char) {
|
||||
if c == 'q' {
|
||||
if c == 'q' && intermediates.get(0) == Some(&b'+') {
|
||||
self.xtgettcap_bytes = Some(vec![]);
|
||||
} else if c == 'q' && intermediates.is_empty() {
|
||||
// we only process sixel images if we know the pixel size of each character cell,
|
||||
// otherwise we can't reliably display them
|
||||
if self.current_cursor_pixel_coordinates().is_some() {
|
||||
@@ -4138,6 +4179,8 @@ impl Perform for Grid {
|
||||
ui_component_bytes.push(byte);
|
||||
} else if let Some(nested_frame_bytes) = self.nested_frame_bytes.as_mut() {
|
||||
nested_frame_bytes.push(byte);
|
||||
} else if let Some(xtgettcap_bytes) = self.xtgettcap_bytes.as_mut() {
|
||||
xtgettcap_bytes.push(byte);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4157,6 +4200,8 @@ impl Perform for Grid {
|
||||
{
|
||||
self.pending_nested_session_messages.push(message);
|
||||
}
|
||||
} else if let Some(xtgettcap_bytes) = self.xtgettcap_bytes.take() {
|
||||
self.answer_xtgettcap(&xtgettcap_bytes);
|
||||
}
|
||||
self.mark_for_rerender();
|
||||
}
|
||||
@@ -4354,10 +4399,17 @@ impl Perform for Grid {
|
||||
return;
|
||||
}
|
||||
|
||||
let _clipboard = params[1].get(0).unwrap_or(&b'c');
|
||||
let clipboard = *params[1].get(0).unwrap_or(&b'c');
|
||||
match params[2] {
|
||||
b"?" => {
|
||||
// TBD: paste from own clipboard - currently unsupported
|
||||
self.pending_forwarded_queries.push(
|
||||
crate::host_query::HostQuery::ClipboardContent {
|
||||
selection: clipboard as char,
|
||||
terminator: crate::host_query::OscTerminator::from_bell_terminated(
|
||||
bell_terminated,
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
base64 => {
|
||||
if let Ok(bytes) = BASE64_DECODER.decode(base64) {
|
||||
|
||||
@@ -8674,3 +8674,101 @@ fn xtsmgraphics_geometry_reports_failure_when_host_does_not_support_sixel() {
|
||||
vte_parser.advance(&mut grid, b"\x1b[?2;1S");
|
||||
assert_eq!(grid.pending_messages_to_pty, vec![b"\x1b[?2;3;0S".to_vec()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_52_read_is_forwarded_to_the_host() {
|
||||
let mut grid = new_grid_for_forwarding_test();
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1b]52;c;?\x07");
|
||||
assert!(
|
||||
grid.pending_messages_to_pty.is_empty(),
|
||||
"clipboard reads must never be answered locally"
|
||||
);
|
||||
assert_eq!(
|
||||
grid.pending_forwarded_queries,
|
||||
vec![crate::host_query::HostQuery::ClipboardContent {
|
||||
selection: 'c',
|
||||
terminator: crate::host_query::OscTerminator::Bel,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_52_read_keeps_the_selection_and_terminator_of_the_query() {
|
||||
let mut grid = new_grid_for_forwarding_test();
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1b]52;p;?\x1b\\");
|
||||
assert_eq!(
|
||||
grid.pending_forwarded_queries,
|
||||
vec![crate::host_query::HostQuery::ClipboardContent {
|
||||
selection: 'p',
|
||||
terminator: crate::host_query::OscTerminator::St,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_52_write_is_not_forwarded() {
|
||||
let mut grid = new_grid_for_forwarding_test();
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1b]52;c;aGVsbG8=\x07");
|
||||
assert!(
|
||||
grid.pending_forwarded_queries.is_empty(),
|
||||
"copying to the clipboard must not go through the forward path"
|
||||
);
|
||||
assert_eq!(grid.pending_clipboard_update.as_deref(), Some("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xtgettcap_answers_the_ms_capability() {
|
||||
let mut grid = create_grid_with_content("");
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1bP+q4d73\x1b\\");
|
||||
assert_eq!(
|
||||
grid.pending_messages_to_pty,
|
||||
vec![b"\x1bP1+r4D73=1B5D35323B25703125733B257032257307\x1b\\".to_vec()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xtgettcap_rejects_unknown_capabilities() {
|
||||
let mut grid = create_grid_with_content("");
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1bP+q62656c\x1b\\");
|
||||
assert_eq!(
|
||||
grid.pending_messages_to_pty,
|
||||
vec![b"\x1bP0+r\x1b\\".to_vec()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xtgettcap_answers_each_requested_capability() {
|
||||
let mut grid = create_grid_with_content("");
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1bP+q62656c;4d73\x1b\\");
|
||||
assert_eq!(grid.pending_messages_to_pty.len(), 2);
|
||||
assert_eq!(grid.pending_messages_to_pty[0], b"\x1bP0+r\x1b\\".to_vec());
|
||||
assert!(String::from_utf8_lossy(&grid.pending_messages_to_pty[1]).starts_with("\x1bP1+r4D73="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xtgettcap_rejects_malformed_hex() {
|
||||
let mut grid = create_grid_with_content("");
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1bP+q4d7\x1b\\");
|
||||
assert_eq!(
|
||||
grid.pending_messages_to_pty,
|
||||
vec![b"\x1bP0+r\x1b\\".to_vec()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sixel_dcs_is_not_confused_with_xtgettcap() {
|
||||
let mut grid = new_grid_for_forwarding_test();
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, b"\x1bPq#0;2;0;0;0\x1b\\");
|
||||
assert!(
|
||||
grid.pending_messages_to_pty.is_empty(),
|
||||
"a sixel image must not produce an XTGETTCAP reply"
|
||||
);
|
||||
}
|
||||
|
||||
+122
-1
@@ -790,6 +790,7 @@ pub enum ScreenInstruction {
|
||||
osc133_command_selection: bool,
|
||||
word_separators: String,
|
||||
nested_session_handling: NestedSessionHandling,
|
||||
dangerously_enable_paste_buffer_read: bool,
|
||||
},
|
||||
RerunCommandPane(u32, Option<NotificationEnd>), // u32 - terminal pane id
|
||||
ResizePaneWithId(ResizeStrategy, PaneId),
|
||||
@@ -1543,6 +1544,10 @@ pub(crate) struct Screen {
|
||||
pending_forwarded_queries: HashMap<u32, PendingForwardEntry>,
|
||||
forward_queue: VecDeque<PendingForward>,
|
||||
forward_in_flight_token: Option<u32>,
|
||||
pending_clipboard_forwards: HashMap<u32, PendingForwardEntry>,
|
||||
clipboard_forward_queue: VecDeque<PendingForward>,
|
||||
clipboard_forward_in_flight_token: Option<u32>,
|
||||
paste_buffer_read_enabled: bool,
|
||||
nested_guest_tracker: NestedGuestTracker,
|
||||
nested_ancestry: Vec<String>,
|
||||
nested_via_client_id: Option<ClientId>,
|
||||
@@ -1609,6 +1614,8 @@ const STARTUP_SENTINEL_TOKEN: u32 = 0;
|
||||
/// network-pathological cases ever see this fire.
|
||||
const SERVER_FORWARD_TIMEOUT_MS: u64 = 1000;
|
||||
|
||||
const SERVER_CLIPBOARD_FORWARD_TIMEOUT_MS: u64 = 35_000;
|
||||
|
||||
impl Screen {
|
||||
/// Creates and returns a new [`Screen`].
|
||||
pub fn new(
|
||||
@@ -1727,6 +1734,10 @@ impl Screen {
|
||||
pending_forwarded_queries: HashMap::new(),
|
||||
forward_queue: VecDeque::new(),
|
||||
forward_in_flight_token: None,
|
||||
pending_clipboard_forwards: HashMap::new(),
|
||||
clipboard_forward_queue: VecDeque::new(),
|
||||
clipboard_forward_in_flight_token: None,
|
||||
paste_buffer_read_enabled: false,
|
||||
nested_guest_tracker: NestedGuestTracker::default(),
|
||||
nested_ancestry: vec![],
|
||||
nested_via_client_id: None,
|
||||
@@ -2667,6 +2678,13 @@ impl Screen {
|
||||
self.answer_color_palette_mode_query_locally(pane_id);
|
||||
return STARTUP_SENTINEL_TOKEN; // sentinel: no real forward happened
|
||||
}
|
||||
if let crate::host_query::HostQuery::ClipboardContent { .. } = query {
|
||||
if !self.paste_buffer_read_enabled {
|
||||
let _ = self.resume_pane_after_forward(pane_id, Vec::new());
|
||||
return STARTUP_SENTINEL_TOKEN;
|
||||
}
|
||||
return self.enqueue_clipboard_forward(pane_id, query);
|
||||
}
|
||||
let token = self.next_forward_token;
|
||||
// Skip over the reserved sentinel (0) on wrap; allocate a fresh
|
||||
// u32 for every forward.
|
||||
@@ -2686,6 +2704,85 @@ impl Screen {
|
||||
token
|
||||
}
|
||||
|
||||
fn enqueue_clipboard_forward(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
query: crate::host_query::HostQuery,
|
||||
) -> u32 {
|
||||
let token = self.next_forward_token;
|
||||
self.next_forward_token = self.next_forward_token.wrapping_add(1);
|
||||
if self.next_forward_token == STARTUP_SENTINEL_TOKEN {
|
||||
self.next_forward_token = 1;
|
||||
}
|
||||
if self.clipboard_forward_in_flight_token.is_some() {
|
||||
self.clipboard_forward_queue.push_back(PendingForward {
|
||||
token,
|
||||
pane_id,
|
||||
query,
|
||||
});
|
||||
} else {
|
||||
self.dispatch_clipboard_forward(token, pane_id, query);
|
||||
}
|
||||
token
|
||||
}
|
||||
|
||||
fn dispatch_clipboard_forward(
|
||||
&mut self,
|
||||
token: u32,
|
||||
pane_id: PaneId,
|
||||
query: crate::host_query::HostQuery,
|
||||
) {
|
||||
let query_bytes = query.to_query_bytes();
|
||||
self.pending_clipboard_forwards
|
||||
.insert(token, PendingForwardEntry { pane_id, query });
|
||||
self.clipboard_forward_in_flight_token = Some(token);
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::ForwardQueryToHost(
|
||||
token,
|
||||
query_bytes,
|
||||
true,
|
||||
));
|
||||
let senders = self.bus.senders.clone();
|
||||
crate::global_async_runtime::get_tokio_runtime().spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(
|
||||
SERVER_CLIPBOARD_FORWARD_TIMEOUT_MS,
|
||||
))
|
||||
.await;
|
||||
let _ = senders.send_to_screen(ScreenInstruction::ForwardedReplyFromHost {
|
||||
token,
|
||||
reply_bytes: Vec::new(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_clipboard_reply(&mut self, token: u32, reply_bytes: Vec<u8>) -> Result<()> {
|
||||
if let Some(PendingForwardEntry { pane_id, query }) =
|
||||
self.pending_clipboard_forwards.remove(&token)
|
||||
{
|
||||
let payload = if reply_bytes.is_empty() {
|
||||
query.empty_reply_bytes()
|
||||
} else {
|
||||
reply_bytes
|
||||
};
|
||||
self.resume_pane_after_forward(pane_id, payload)?;
|
||||
}
|
||||
self.clipboard_forward_in_flight_token = None;
|
||||
while let Some(next) = self.clipboard_forward_queue.pop_front() {
|
||||
if !self.pane_exists(&next.pane_id) {
|
||||
continue;
|
||||
}
|
||||
self.dispatch_clipboard_forward(next.token, next.pane_id, next.query);
|
||||
break;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pane_exists(&self, pane_id: &PaneId) -> bool {
|
||||
self.tabs.values().any(|tab| tab.has_pane_with_pid(pane_id))
|
||||
}
|
||||
|
||||
fn answer_color_palette_mode_query_locally(&mut self, pane_id: PaneId) {
|
||||
if matches!(pane_id, PaneId::Plugin(_)) {
|
||||
return;
|
||||
@@ -2729,7 +2826,11 @@ impl Screen {
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::ForwardQueryToHost(token, query_bytes));
|
||||
.send_to_server(ServerInstruction::ForwardQueryToHost(
|
||||
token,
|
||||
query_bytes,
|
||||
false,
|
||||
));
|
||||
let senders = self.bus.senders.clone();
|
||||
crate::global_async_runtime::get_tokio_runtime().spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(SERVER_FORWARD_TIMEOUT_MS)).await;
|
||||
@@ -2757,6 +2858,17 @@ impl Screen {
|
||||
token: u32,
|
||||
reply_bytes: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
if self.clipboard_forward_in_flight_token == Some(token) {
|
||||
return self.handle_clipboard_reply(token, reply_bytes);
|
||||
}
|
||||
if self.pending_clipboard_forwards.contains_key(&token) {
|
||||
log::debug!(
|
||||
"Dropping stale clipboard reply (token={}, len={} bytes)",
|
||||
token,
|
||||
reply_bytes.len(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
// Stale-reply guard. Both the real `ForwardedReplyFromHost`
|
||||
// path and the server-side timeout path land here. If a real
|
||||
// reply landed first (releasing the slot AND dispatching the
|
||||
@@ -3882,6 +3994,7 @@ impl Screen {
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
HostQuery::ClipboardContent { .. } => query.empty_reply_bytes(),
|
||||
// Should not reach here: ColorPaletteMode short-circuits in
|
||||
// `forward_host_query` before any cache-fallback path runs.
|
||||
HostQuery::ColorPaletteMode => match self.effective_host_terminal_theme_mode() {
|
||||
@@ -6415,6 +6528,7 @@ impl Screen {
|
||||
osc133_command_selection: bool,
|
||||
word_separators: String,
|
||||
nested_session_handling: NestedSessionHandling,
|
||||
dangerously_enable_paste_buffer_read: bool,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
let should_support_arrow_fonts = !simplified_ui;
|
||||
@@ -6444,6 +6558,7 @@ impl Screen {
|
||||
self.osc133_command_selection = osc133_command_selection;
|
||||
self.word_separators = word_separators;
|
||||
self.nested_session_handling = nested_session_handling;
|
||||
self.paste_buffer_read_enabled = dangerously_enable_paste_buffer_read;
|
||||
self.default_mode_info
|
||||
.update_arrow_fonts(should_support_arrow_fonts);
|
||||
self.default_mode_info
|
||||
@@ -7627,6 +7742,9 @@ pub(crate) fn screen_thread_main(
|
||||
let focus_follows_mouse = config_options.focus_follows_mouse.unwrap_or(false);
|
||||
let mouse_click_through = config_options.mouse_click_through.unwrap_or(false);
|
||||
let nested_session_handling = config_options.nested_session_handling.unwrap_or_default();
|
||||
let dangerously_enable_paste_buffer_read = config_options
|
||||
.dangerously_enable_paste_buffer_read
|
||||
.unwrap_or(false);
|
||||
|
||||
let thread_senders = bus.senders.clone();
|
||||
let mut screen = Screen::new(
|
||||
@@ -7679,6 +7797,7 @@ pub(crate) fn screen_thread_main(
|
||||
);
|
||||
screen.host_theme_dark_styling = host_theme_dark_styling;
|
||||
screen.host_theme_light_styling = host_theme_light_styling;
|
||||
screen.paste_buffer_read_enabled = dangerously_enable_paste_buffer_read;
|
||||
|
||||
let mut pending_tab_ids: HashSet<usize> = HashSet::new();
|
||||
let mut pending_tab_switches: HashSet<(usize, ClientId)> = HashSet::new(); // usize is the
|
||||
@@ -10987,6 +11106,7 @@ pub(crate) fn screen_thread_main(
|
||||
osc133_command_selection,
|
||||
word_separators,
|
||||
nested_session_handling,
|
||||
dangerously_enable_paste_buffer_read,
|
||||
} => {
|
||||
screen.host_theme_dark_styling = host_theme_dark;
|
||||
screen.host_theme_light_styling = host_theme_light;
|
||||
@@ -11016,6 +11136,7 @@ pub(crate) fn screen_thread_main(
|
||||
osc133_command_selection,
|
||||
word_separators,
|
||||
nested_session_handling,
|
||||
dangerously_enable_paste_buffer_read,
|
||||
client_id,
|
||||
)
|
||||
.non_fatal();
|
||||
|
||||
@@ -8687,13 +8687,23 @@ impl ForwardCapture {
|
||||
fn drain_forward_queries(&self) -> Vec<(u32, Vec<u8>)> {
|
||||
let mut out = Vec::new();
|
||||
while let Ok((instr, _ctx)) = self.server_rx.try_recv() {
|
||||
if let ServerInstruction::ForwardQueryToHost(token, bytes) = instr {
|
||||
if let ServerInstruction::ForwardQueryToHost(token, bytes, _) = instr {
|
||||
out.push((token, bytes));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn drain_forward_queries_with_async(&self) -> Vec<(u32, Vec<u8>, bool)> {
|
||||
let mut out = Vec::new();
|
||||
while let Ok((instr, _ctx)) = self.server_rx.try_recv() {
|
||||
if let ServerInstruction::ForwardQueryToHost(token, bytes, resolve_async) = instr {
|
||||
out.push((token, bytes, resolve_async));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Drain every pending `PtyWriteInstruction::Write`, returning
|
||||
/// `(bytes, terminal_id)` — the two fields the reply path sets.
|
||||
fn drain_pty_writes(&self) -> Vec<(Vec<u8>, u32)> {
|
||||
@@ -8942,6 +8952,178 @@ fn handle_reply_dispatches_next_queued_forward() {
|
||||
);
|
||||
}
|
||||
|
||||
fn clipboard_query() -> HostQuery {
|
||||
HostQuery::ClipboardContent {
|
||||
selection: 'c',
|
||||
terminator: OscTerminator::St,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_read_is_dropped_when_the_option_is_off() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
|
||||
screen.forward_host_query(PaneId::Terminal(3), clipboard_query());
|
||||
|
||||
assert!(
|
||||
capture.drain_forward_queries().is_empty(),
|
||||
"an opted-out clipboard read must never reach the host terminal"
|
||||
);
|
||||
assert!(screen.clipboard_forward_in_flight_token.is_none());
|
||||
assert!(screen.pending_clipboard_forwards.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_read_is_forwarded_asynchronously_when_enabled() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
screen.paste_buffer_read_enabled = true;
|
||||
let pane_id = PaneId::Terminal(5);
|
||||
|
||||
let token = screen.forward_host_query(pane_id, clipboard_query());
|
||||
|
||||
let forwards = capture.drain_forward_queries_with_async();
|
||||
assert_eq!(
|
||||
forwards,
|
||||
vec![(token, b"\x1b]52;c;?\x1b\\".to_vec(), true)],
|
||||
"the query must go out marked as async so the client omits its barrier"
|
||||
);
|
||||
assert_eq!(screen.clipboard_forward_in_flight_token, Some(token));
|
||||
assert_eq!(
|
||||
screen
|
||||
.pending_clipboard_forwards
|
||||
.get(&token)
|
||||
.map(|e| e.pane_id),
|
||||
Some(pane_id)
|
||||
);
|
||||
assert!(
|
||||
screen.forward_in_flight_token.is_none(),
|
||||
"the barrier-serialized slot must stay free"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pending_clipboard_read_does_not_block_other_host_queries() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
screen.paste_buffer_read_enabled = true;
|
||||
let clipboard_token = screen.forward_host_query(PaneId::Terminal(1), clipboard_query());
|
||||
let colour_token = screen.forward_host_query(PaneId::Terminal(2), bg_query());
|
||||
|
||||
let forwards = capture.drain_forward_queries();
|
||||
assert_eq!(
|
||||
forwards.len(),
|
||||
2,
|
||||
"a colour query must dispatch while a clipboard prompt is pending"
|
||||
);
|
||||
assert_eq!(forwards[0].0, clipboard_token);
|
||||
assert_eq!(forwards[1].0, colour_token);
|
||||
assert_eq!(screen.forward_in_flight_token, Some(colour_token));
|
||||
assert_eq!(
|
||||
screen.clipboard_forward_in_flight_token,
|
||||
Some(clipboard_token)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_real_clipboard_reply_is_written_to_the_pane_verbatim() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
screen.paste_buffer_read_enabled = true;
|
||||
let token = screen.forward_host_query(PaneId::Terminal(8), clipboard_query());
|
||||
let _ = capture.drain_forward_queries();
|
||||
|
||||
let reply = b"\x1b]52;c;aGVsbG8=\x1b\\".to_vec();
|
||||
screen
|
||||
.handle_forwarded_reply_from_host(token, reply.clone())
|
||||
.expect("handler must not fail");
|
||||
|
||||
assert_eq!(capture.drain_pty_writes(), vec![(reply, 8)]);
|
||||
assert!(screen.clipboard_forward_in_flight_token.is_none());
|
||||
assert!(screen.pending_clipboard_forwards.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unanswered_clipboard_read_resolves_as_an_empty_clipboard() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
screen.paste_buffer_read_enabled = true;
|
||||
let token = screen.forward_host_query(PaneId::Terminal(9), clipboard_query());
|
||||
let _ = capture.drain_forward_queries();
|
||||
|
||||
screen
|
||||
.handle_forwarded_reply_from_host(token, Vec::new())
|
||||
.expect("handler must not fail");
|
||||
|
||||
assert_eq!(
|
||||
capture.drain_pty_writes(),
|
||||
vec![(b"\x1b]52;c;\x1b\\".to_vec(), 9)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_late_clipboard_reply_is_discarded() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
screen.paste_buffer_read_enabled = true;
|
||||
let token = screen.forward_host_query(PaneId::Terminal(9), clipboard_query());
|
||||
let _ = capture.drain_forward_queries();
|
||||
screen
|
||||
.handle_forwarded_reply_from_host(token, Vec::new())
|
||||
.expect("ok");
|
||||
let _ = capture.drain_pty_writes();
|
||||
|
||||
screen
|
||||
.handle_forwarded_reply_from_host(token, b"\x1b]52;c;bGF0ZQ==\x1b\\".to_vec())
|
||||
.expect("ok");
|
||||
|
||||
assert!(
|
||||
capture.drain_pty_writes().is_empty(),
|
||||
"clipboard data arriving after the window closed must never reach the pane"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_clipboard_read_waits_for_the_first() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
screen.paste_buffer_read_enabled = true;
|
||||
let first_token = screen.forward_host_query(PaneId::Terminal(1), clipboard_query());
|
||||
let second_token = screen.forward_host_query(PaneId::Terminal(2), clipboard_query());
|
||||
|
||||
let forwards = capture.drain_forward_queries();
|
||||
assert_eq!(
|
||||
forwards.len(),
|
||||
1,
|
||||
"a host can only show one consent prompt at a time"
|
||||
);
|
||||
assert_eq!(forwards[0].0, first_token);
|
||||
assert_eq!(screen.clipboard_forward_queue.len(), 1);
|
||||
assert_eq!(screen.clipboard_forward_queue[0].token, second_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_queued_clipboard_read_for_a_closed_pane_is_skipped() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
let (mut screen, capture) = create_new_screen_with_forward_capture(size);
|
||||
screen.paste_buffer_read_enabled = true;
|
||||
let first_token = screen.forward_host_query(PaneId::Terminal(1), clipboard_query());
|
||||
screen.forward_host_query(PaneId::Terminal(2), clipboard_query());
|
||||
let _ = capture.drain_forward_queries();
|
||||
|
||||
screen
|
||||
.handle_forwarded_reply_from_host(first_token, Vec::new())
|
||||
.expect("ok");
|
||||
|
||||
assert!(
|
||||
capture.drain_forward_queries().is_empty(),
|
||||
"the queued read belongs to a pane no tab owns"
|
||||
);
|
||||
assert!(screen.clipboard_forward_queue.is_empty());
|
||||
assert!(screen.clipboard_forward_in_flight_token.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_nested_guest_does_not_notify_guest_focus_lost_on_teardown() {
|
||||
let size = Size { cols: 80, rows: 20 };
|
||||
|
||||
@@ -523,6 +523,14 @@ load_plugins {
|
||||
//
|
||||
// stacked_resize false
|
||||
|
||||
// Whether to let programs running inside panes read the paste buffer (clipboard)
|
||||
// with the OSC 52 escape sequence. When enabled, any program in any pane -
|
||||
// including one running on a remote machine over SSH - can read the clipboard
|
||||
// without the user being asked.
|
||||
// Default: false
|
||||
//
|
||||
// dangerously_enable_paste_buffer_read true
|
||||
|
||||
// Whether stacked panes display as a list with the expanded pane pinned to the bottom
|
||||
// Default: true
|
||||
//
|
||||
|
||||
@@ -2030,6 +2030,8 @@ pub struct Options {
|
||||
pub osc133_command_selection: ::core::option::Option<bool>,
|
||||
#[prost(string, optional, tag="57")]
|
||||
pub word_separators: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(bool, optional, tag="58")]
|
||||
pub dangerously_enable_paste_buffer_read: ::core::option::Option<bool>,
|
||||
}
|
||||
/// Pane-targeting action messages
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
@@ -3424,6 +3426,8 @@ pub struct ForwardQueryToHostMsg {
|
||||
pub token: u32,
|
||||
#[prost(bytes="vec", tag="2")]
|
||||
pub query_bytes: ::prost::alloc::vec::Vec<u8>,
|
||||
#[prost(bool, tag="3")]
|
||||
pub resolve_async: bool,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
|
||||
@@ -1211,6 +1211,7 @@ message Options {
|
||||
optional bool support_kitty_graphics_protocol = 55;
|
||||
optional bool osc133_command_selection = 56;
|
||||
optional string word_separators = 57;
|
||||
optional bool dangerously_enable_paste_buffer_read = 58;
|
||||
}
|
||||
|
||||
enum OnForceClose {
|
||||
|
||||
@@ -96,6 +96,7 @@ message SubscribedPaneClosedMsg {
|
||||
message ForwardQueryToHostMsg {
|
||||
uint32 token = 1;
|
||||
bytes query_bytes = 2;
|
||||
bool resolve_async = 3;
|
||||
}
|
||||
|
||||
message SetSoftKeyboardMsg {
|
||||
|
||||
@@ -400,6 +400,10 @@ pub struct Options {
|
||||
#[clap(long, value_enum, hide_possible_values = true, value_parser)]
|
||||
#[serde(default)]
|
||||
pub nested_session_handling: Option<NestedSessionHandling>,
|
||||
|
||||
#[clap(long, value_parser)]
|
||||
#[serde(default)]
|
||||
pub dangerously_enable_paste_buffer_read: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
|
||||
@@ -521,6 +525,9 @@ impl Options {
|
||||
let nested_session_handling = other
|
||||
.nested_session_handling
|
||||
.or(self.nested_session_handling);
|
||||
let dangerously_enable_paste_buffer_read = other
|
||||
.dangerously_enable_paste_buffer_read
|
||||
.or(self.dangerously_enable_paste_buffer_read);
|
||||
|
||||
Options {
|
||||
simplified_ui,
|
||||
@@ -577,6 +584,7 @@ impl Options {
|
||||
post_command_discovery_hook,
|
||||
client_async_worker_tasks,
|
||||
nested_session_handling,
|
||||
dangerously_enable_paste_buffer_read,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,6 +686,9 @@ impl Options {
|
||||
let nested_session_handling = other
|
||||
.nested_session_handling
|
||||
.or(self.nested_session_handling);
|
||||
let dangerously_enable_paste_buffer_read = other
|
||||
.dangerously_enable_paste_buffer_read
|
||||
.or(self.dangerously_enable_paste_buffer_read);
|
||||
|
||||
Options {
|
||||
simplified_ui,
|
||||
@@ -734,6 +745,7 @@ impl Options {
|
||||
post_command_discovery_hook,
|
||||
client_async_worker_tasks,
|
||||
nested_session_handling,
|
||||
dangerously_enable_paste_buffer_read,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,7 @@ pub enum ServerToClientMsg {
|
||||
ForwardQueryToHost {
|
||||
token: u32,
|
||||
query_bytes: Vec<u8>,
|
||||
resolve_async: bool,
|
||||
},
|
||||
EmitNestedSessionFrame {
|
||||
payload_bytes: Vec<u8>,
|
||||
|
||||
@@ -428,12 +428,15 @@ impl From<ServerToClientMsg> for ProtoServerToClientMsg {
|
||||
pane_id: Some(pane_id.into()),
|
||||
})
|
||||
},
|
||||
ServerToClientMsg::ForwardQueryToHost { token, query_bytes } => {
|
||||
server_to_client_msg::Message::ForwardQueryToHost(ForwardQueryToHostMsg {
|
||||
token,
|
||||
query_bytes,
|
||||
})
|
||||
},
|
||||
ServerToClientMsg::ForwardQueryToHost {
|
||||
token,
|
||||
query_bytes,
|
||||
resolve_async,
|
||||
} => server_to_client_msg::Message::ForwardQueryToHost(ForwardQueryToHostMsg {
|
||||
token,
|
||||
query_bytes,
|
||||
resolve_async,
|
||||
}),
|
||||
ServerToClientMsg::SetSoftKeyboard { on } => {
|
||||
server_to_client_msg::Message::SetSoftKeyboard(SetSoftKeyboardMsg { on })
|
||||
},
|
||||
@@ -674,6 +677,7 @@ impl TryFrom<ProtoServerToClientMsg> for ServerToClientMsg {
|
||||
Ok(ServerToClientMsg::ForwardQueryToHost {
|
||||
token: msg.token,
|
||||
query_bytes: msg.query_bytes,
|
||||
resolve_async: msg.resolve_async,
|
||||
})
|
||||
},
|
||||
Some(server_to_client_msg::Message::SetSoftKeyboard(msg)) => {
|
||||
@@ -921,6 +925,7 @@ impl From<crate::input::options::Options>
|
||||
}),
|
||||
stacked_resize: options.stacked_resize,
|
||||
stacked_pane_list: options.stacked_pane_list,
|
||||
dangerously_enable_paste_buffer_read: options.dangerously_enable_paste_buffer_read,
|
||||
show_startup_tips: options.show_startup_tips,
|
||||
show_release_notes: options.show_release_notes,
|
||||
advanced_mouse_actions: options.advanced_mouse_actions,
|
||||
@@ -1046,6 +1051,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Options>
|
||||
.transpose()?,
|
||||
stacked_resize: options.stacked_resize,
|
||||
stacked_pane_list: options.stacked_pane_list,
|
||||
dangerously_enable_paste_buffer_read: options.dangerously_enable_paste_buffer_read,
|
||||
show_startup_tips: options.show_startup_tips,
|
||||
show_release_notes: options.show_release_notes,
|
||||
advanced_mouse_actions: options.advanced_mouse_actions,
|
||||
|
||||
@@ -499,6 +499,7 @@ fn test_client_messages() {
|
||||
osc133_command_selection: Some(false),
|
||||
word_separators: Some("[]{}<>():".to_owned()),
|
||||
nested_session_handling: Some(NestedSessionHandling::Fullscreen),
|
||||
dangerously_enable_paste_buffer_read: Some(true),
|
||||
}),
|
||||
layout: None,
|
||||
terminal_window_size: Size { rows: 80, cols: 42 },
|
||||
@@ -3871,14 +3872,22 @@ fn test_server_messages() {
|
||||
test_server_roundtrip!(ServerToClientMsg::ForwardQueryToHost {
|
||||
token: 0,
|
||||
query_bytes: vec![],
|
||||
resolve_async: false,
|
||||
});
|
||||
test_server_roundtrip!(ServerToClientMsg::ForwardQueryToHost {
|
||||
token: 7,
|
||||
query_bytes: b"\x1b[14t".to_vec(),
|
||||
resolve_async: false,
|
||||
});
|
||||
test_server_roundtrip!(ServerToClientMsg::ForwardQueryToHost {
|
||||
token: u32::MAX,
|
||||
query_bytes: (0u8..=255u8).collect(),
|
||||
resolve_async: false,
|
||||
});
|
||||
test_server_roundtrip!(ServerToClientMsg::ForwardQueryToHost {
|
||||
token: 12,
|
||||
query_bytes: b"\x1b]52;c;?\x1b\\".to_vec(),
|
||||
resolve_async: true,
|
||||
});
|
||||
test_server_roundtrip!(ServerToClientMsg::EmitNestedSessionFrame {
|
||||
payload_bytes: vec![],
|
||||
|
||||
@@ -2934,6 +2934,11 @@ impl Options {
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let dangerously_enable_paste_buffer_read = kdl_property_first_arg_as_bool_or_error!(
|
||||
kdl_options,
|
||||
"dangerously_enable_paste_buffer_read"
|
||||
)
|
||||
.map(|(v, _)| v);
|
||||
|
||||
Ok(Options {
|
||||
simplified_ui,
|
||||
@@ -2990,6 +2995,7 @@ impl Options {
|
||||
post_command_discovery_hook,
|
||||
client_async_worker_tasks,
|
||||
nested_session_handling,
|
||||
dangerously_enable_paste_buffer_read,
|
||||
})
|
||||
}
|
||||
pub fn from_string(stringified_keybindings: &String) -> Result<Self, ConfigError> {
|
||||
@@ -4548,6 +4554,35 @@ impl Options {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn dangerously_enable_paste_buffer_read_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
|
||||
let comment_text = format!(
|
||||
"{}\n{}\n{}\n{}\n{}\n{}",
|
||||
" ",
|
||||
"// Whether to let programs running inside panes read the paste buffer",
|
||||
"// (clipboard) with the OSC 52 escape sequence. When enabled, any program",
|
||||
"// in any pane - including one running on a remote machine over SSH - can",
|
||||
"// read the clipboard without the user being asked.",
|
||||
"// Default: false",
|
||||
);
|
||||
let create_node = |node_value: bool| -> KdlNode {
|
||||
let mut node = KdlNode::new("dangerously_enable_paste_buffer_read");
|
||||
node.push(KdlValue::Bool(node_value));
|
||||
node
|
||||
};
|
||||
if let Some(value) = self.dangerously_enable_paste_buffer_read {
|
||||
let mut node = create_node(value);
|
||||
if add_comments {
|
||||
node.set_leading(format!("{}\n", comment_text));
|
||||
}
|
||||
Some(node)
|
||||
} else if add_comments {
|
||||
let mut node = create_node(false);
|
||||
node.set_leading(format!("{}\n// ", comment_text));
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn client_async_worker_tasks_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
|
||||
let comment_text = r#"
|
||||
// Number of async worker tasks to spawn per active client.
|
||||
@@ -4746,6 +4781,11 @@ impl Options {
|
||||
{
|
||||
nodes.push(client_async_worker_tasks);
|
||||
}
|
||||
if let Some(dangerously_enable_paste_buffer_read) =
|
||||
self.dangerously_enable_paste_buffer_read_to_kdl(add_comments)
|
||||
{
|
||||
nodes.push(dangerously_enable_paste_buffer_read);
|
||||
}
|
||||
if let Some(nested_session_handling) = self.nested_session_handling_to_kdl(add_comments) {
|
||||
nodes.push(nested_session_handling);
|
||||
}
|
||||
|
||||
+7
@@ -619,6 +619,13 @@ web_client {
|
||||
// Note: This only applies to web clients at the moment.
|
||||
// client_async_worker_tasks 4
|
||||
|
||||
// Whether to let programs running inside panes read the paste buffer
|
||||
// (clipboard) with the OSC 52 escape sequence. When enabled, any program
|
||||
// in any pane - including one running on a remote machine over SSH - can
|
||||
// read the clipboard without the user being asked.
|
||||
// Default: false
|
||||
// dangerously_enable_paste_buffer_read false
|
||||
|
||||
// How to handle a nested Zellij session detected inside a pane.
|
||||
// Options:
|
||||
// - "ask" (Default — prompt with a modal)
|
||||
|
||||
+7
@@ -334,6 +334,13 @@ web_sharing "disabled"
|
||||
// Note: This only applies to web clients at the moment.
|
||||
// client_async_worker_tasks 4
|
||||
|
||||
// Whether to let programs running inside panes read the paste buffer
|
||||
// (clipboard) with the OSC 52 escape sequence. When enabled, any program
|
||||
// in any pane - including one running on a remote machine over SSH - can
|
||||
// read the clipboard without the user being asked.
|
||||
// Default: false
|
||||
// dangerously_enable_paste_buffer_read false
|
||||
|
||||
// How to handle a nested Zellij session detected inside a pane.
|
||||
// Options:
|
||||
// - "ask" (Default — prompt with a modal)
|
||||
|
||||
+1
@@ -59,4 +59,5 @@ Options {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
}
|
||||
|
||||
+1
@@ -59,4 +59,5 @@ Options {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
}
|
||||
|
||||
+1
@@ -57,4 +57,5 @@ Options {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
}
|
||||
|
||||
+1
@@ -6082,6 +6082,7 @@ Config {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
},
|
||||
themes: {},
|
||||
plugins: PluginAliases {
|
||||
|
||||
+1
@@ -6082,6 +6082,7 @@ Config {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
},
|
||||
themes: {},
|
||||
plugins: PluginAliases {
|
||||
|
||||
+1
@@ -144,6 +144,7 @@ Config {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
},
|
||||
themes: {},
|
||||
plugins: PluginAliases {
|
||||
|
||||
+1
@@ -59,4 +59,5 @@ Options {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
}
|
||||
|
||||
+1
@@ -6082,6 +6082,7 @@ Config {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
},
|
||||
themes: {
|
||||
"other-theme-from-config": Theme {
|
||||
|
||||
+1
@@ -6082,6 +6082,7 @@ Config {
|
||||
post_command_discovery_hook: None,
|
||||
client_async_worker_tasks: None,
|
||||
nested_session_handling: None,
|
||||
dangerously_enable_paste_buffer_read: None,
|
||||
},
|
||||
themes: {},
|
||||
plugins: PluginAliases {
|
||||
|
||||
Reference in New Issue
Block a user