From e96033a2970c5ac15ff90bf6e9dc6308d91c2116 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 30 Sep 2024 18:58:46 +0400 Subject: [PATCH 1/3] feat: implement opt-in OSC52 clipboard querying - add new config option `enable_osc52_clipboard_reading = false` - add a new trait function: `Clipboard::get_contents(&self, selection: ClipboardSelection, writer: Box)` The clipboard content is read asynchronously by providing a sender channel `Box` The sender `ClipboardReader` is a trait that requires the channel to be the `Send` + `Sync` + `Debug` (for the `MuxNotification` enum) along with the special hackery trait `ClipboardReaderBoxClone` that allows `Box)` to be `Clone` --- codec/src/lib.rs | 16 ++++ config/src/config.rs | 6 ++ config/src/terminal.rs | 4 + .../config/enable_osc52_clipboard_reading.md | 30 +++++++ docs/shell-integration.md | 31 +++++++ mux/src/lib.rs | 22 ++++- term/src/config.rs | 4 + term/src/terminal.rs | 39 ++++++++ term/src/terminalstate/mod.rs | 1 + term/src/terminalstate/performer.rs | 39 +++++++- term/src/test/mod.rs | 88 ++++++++++++++++++- termwiz/src/escape/osc.rs | 16 ++++ wezterm-client/src/pane/clientpane.rs | 51 ++++++++++- wezterm-gui/src/frontend.rs | 37 ++++++++ wezterm-gui/src/termwindow/mod.rs | 4 + wezterm-mux-server-impl/src/dispatch.rs | 47 +++++++++- wezterm-mux-server-impl/src/sessionhandler.rs | 2 + 17 files changed, 431 insertions(+), 6 deletions(-) create mode 100644 docs/config/lua/config/enable_osc52_clipboard_reading.md diff --git a/codec/src/lib.rs b/codec/src/lib.rs index 71767228825..32deabe86b1 100644 --- a/codec/src/lib.rs +++ b/codec/src/lib.rs @@ -461,6 +461,8 @@ pdu! { SendPaste: 13, Resize: 14, SetClipboard: 20, + QueryClipboard: 21, + QueryClipboardResponse: 63, GetLines: 22, GetLinesResponse: 23, GetPaneRenderChanges: 24, @@ -516,6 +518,7 @@ impl Pdu { | Self::SendPaste(_) | Self::Resize(_) | Self::SetClipboard(_) + | Self::QueryClipboard(_) | Self::SetPaneZoomed(_) | Self::SpawnV2(_) => true, _ => false, @@ -594,6 +597,7 @@ impl Pdu { | Pdu::SetPalette(SetPalette { pane_id, .. }) | Pdu::NotifyAlert(NotifyAlert { pane_id, .. }) | Pdu::SetClipboard(SetClipboard { pane_id, .. }) + | Pdu::QueryClipboard(QueryClipboard { pane_id, .. }) | Pdu::PaneFocused(PaneFocused { pane_id }) | Pdu::PaneRemoved(PaneRemoved { pane_id }) => Some(*pane_id), _ => None, @@ -769,6 +773,18 @@ pub struct SetClipboard { pub selection: ClipboardSelection, } +#[derive(Deserialize, Serialize, PartialEq, Debug)] +pub struct QueryClipboard { + pub pane_id: PaneId, + pub selection: ClipboardSelection, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug)] +pub struct QueryClipboardResponse { + pub pane_id: PaneId, + pub content: Option, +} + #[derive(Deserialize, Serialize, PartialEq, Debug)] pub struct SetWindowWorkspace { pub window_id: WindowId, diff --git a/config/src/config.rs b/config/src/config.rs index 70a77d8db6c..dd8f55b718b 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -240,6 +240,12 @@ pub struct Config { #[dynamic(default)] pub enable_kitty_keyboard: bool, + /// Whether the terminal should respond to OSC 52 requests to read the + /// clipboard content. + /// Disabled by default for security concerns. + #[dynamic(default)] + pub enable_osc52_clipboard_reading: bool, + /// Whether the terminal should respond to requests to read the /// title string. /// Disabled by default for security concerns with shells that might diff --git a/config/src/terminal.rs b/config/src/terminal.rs index 305b4851484..eb6c203082b 100644 --- a/config/src/terminal.rs +++ b/config/src/terminal.rs @@ -86,6 +86,10 @@ impl wezterm_term::TerminalConfiguration for TermConfig { self.configuration().enable_kitty_keyboard } + fn enable_osc52_clipboard_reading(&self) -> bool { + self.configuration().enable_osc52_clipboard_reading + } + fn canonicalize_pasted_newlines(&self) -> wezterm_term::config::NewlineCanon { match self.configuration().canonicalize_pasted_newlines { None => wezterm_term::config::NewlineCanon::default(), diff --git a/docs/config/lua/config/enable_osc52_clipboard_reading.md b/docs/config/lua/config/enable_osc52_clipboard_reading.md new file mode 100644 index 00000000000..d14aa717eb0 --- /dev/null +++ b/docs/config/lua/config/enable_osc52_clipboard_reading.md @@ -0,0 +1,30 @@ +--- +tags: + - osc + - clipboard +--- + +# `enable_osc52_clipboard_reading = false` + +{{since('nightly')}} + +When set to `true`, the terminal will allow access to the system clipboard by +terminal applications via `OSC 52` [escape sequence](../../../shell-integration.md#osc-52-clipboard-paste). + +The default for this option is `false`. + +Note that it is not recommended to enable this option due to serious security +implications. + +### Security Concerns + +Clipboards are often used to store sensitive information, and granting any +terminal application (especially from remote machines) access to it poses a +security risk. A malicious server could spam the terminal with OSC 52 paste +sequences to monitor whatever you have on the clipboard, which may occasionally +contain sensitive data. + +Setting clipboard data that contains escape sequences or malicious commands and +reading it back could allow an attacker to inject harmful characters into the +input stream. Although, the risk is somewhat mitigated as the pasted text is +encoded in BASE64. diff --git a/docs/shell-integration.md b/docs/shell-integration.md index e5546f22807..40cc173bb2f 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -3,6 +3,7 @@ wezterm supports integrating with the shell through the following means: * `OSC 7` Escape sequences to advise the terminal of the working directory * `OSC 133` Escape sequence to define Input, Output and Prompt zones * `OSC 1337` Escape sequences to set user vars for tracking additional shell state +* `OSC 52` Escape sequences for writing to and reading from the clipboard `OSC` is escape sequence jargon for *Operating System Command*. @@ -180,3 +181,33 @@ return config Now, rather than just running `cmd.exe` on its own, this will cause `cmd.exe` to self-inject the clink line editor. + +## OSC 52 Clipboard Copy + +The data can be copied to the `System Clipboard` or `Primary Selection` with a +command like this: + +```bash +printf '\033]52;c;%s\033\\' $(base64 <<< "hello world") +``` + +- `c` copies to the system clipboard +- `p` copies to the primary selection buffer + +The second parameter provides the selection data, which is a string encoded in base64 (RFC-4648). + +## OSC 52 Clipboard Paste + +The data can be pasted from the `System Clipboard` or `Primary Selection` with a +command like this: + +```bash +printf "\033]7;c;?\033\\" +``` + +- `c` pastes to the system clipboard +- `p` pastes to the primary selection buffer + +Note that this feature poses a potential security risk and is disabled by +default. It requires enabling via [a configuration option](config/lua/config/enable_osc52_clipboard_reading.md). +You should carefully consider whether you're willing to accept the associated risks before enabling it. diff --git a/mux/src/lib.rs b/mux/src/lib.rs index f46e5ee0896..e2145ce842c 100644 --- a/mux/src/lib.rs +++ b/mux/src/lib.rs @@ -27,7 +27,7 @@ use std::time::{Duration, Instant}; use termwiz::escape::csi::{DecPrivateMode, DecPrivateModeCode, Device, Mode}; use termwiz::escape::{Action, CSI}; use thiserror::*; -use wezterm_term::{Clipboard, ClipboardSelection, DownloadHandler, TerminalSize}; +use wezterm_term::{Clipboard, ClipboardReader, ClipboardSelection, DownloadHandler, TerminalSize}; #[cfg(windows)] use winapi::um::winsock2::{SOL_SOCKET, SO_RCVBUF, SO_SNDBUF}; @@ -71,6 +71,11 @@ pub enum MuxNotification { selection: ClipboardSelection, clipboard: Option, }, + QueryClipboard { + pane_id: PaneId, + selection: ClipboardSelection, + writer: Box, + }, SaveToDownloads { name: Option, data: Arc>, @@ -1435,6 +1440,21 @@ impl Clipboard for MuxClipboard { }); Ok(()) } + fn get_contents( + &self, + selection: ClipboardSelection, + writer: Box, + ) -> anyhow::Result<()> { + let mux = + Mux::try_get().ok_or_else(|| anyhow::anyhow!("MuxClipboard::get_contents: no Mux?"))?; + + mux.notify(MuxNotification::QueryClipboard { + pane_id: self.pane_id, + selection, + writer, + }); + Ok(()) + } } struct MuxDownloader {} diff --git a/term/src/config.rs b/term/src/config.rs index b09fb43ccdd..129eb96aa7b 100644 --- a/term/src/config.rs +++ b/term/src/config.rs @@ -180,6 +180,10 @@ pub trait TerminalConfiguration: Downcast + std::fmt::Debug + Send + Sync { false } + fn enable_osc52_clipboard_reading(&self) -> bool { + false + } + /// The default unicode version to assume. /// This affects how the width of certain sequences is interpreted. /// At the time of writing, we default to 9 even though the current diff --git a/term/src/terminal.rs b/term/src/terminal.rs index 5adbf5b30e4..c17ae66d0ca 100644 --- a/term/src/terminal.rs +++ b/term/src/terminal.rs @@ -10,12 +10,44 @@ pub enum ClipboardSelection { PrimarySelection, } +/// A special trait that defines an interface for reading the clipboard content via [`Clipboard::get_contents`]. +/// Once the content is available, it will be written to the provided [`Box`]. +pub trait ClipboardReader: Send + Sync + std::fmt::Debug + ClipboardReaderBoxClone { + /// Similar to [`std::io::Write`] but receives a full String instead of bytes + fn write(&mut self, contents: String) -> Result<(), std::io::Error>; +} + +/// Allow [`ClipboardReader`] to be [`Clone`] when used within [`Box`] +pub trait ClipboardReaderBoxClone { + fn clone_box(&self) -> Box; +} +// Automatically implement for all `dyn ClipboardReader` types that also implement `Clone` +impl ClipboardReaderBoxClone for T +where + T: 'static + ClipboardReader + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } +} + pub trait Clipboard: Send + Sync { fn set_contents( &self, selection: ClipboardSelection, data: Option, ) -> anyhow::Result<()>; + fn get_contents( + &self, + selection: ClipboardSelection, + writer: Box, + ) -> anyhow::Result<()>; } impl Clipboard for Box { @@ -26,6 +58,13 @@ impl Clipboard for Box { ) -> anyhow::Result<()> { self.as_ref().set_contents(selection, data) } + fn get_contents( + &self, + selection: ClipboardSelection, + writer: Box, + ) -> anyhow::Result<()> { + self.as_ref().get_contents(selection, writer) + } } pub trait DeviceControlHandler: Send + Sync { diff --git a/term/src/terminalstate/mod.rs b/term/src/terminalstate/mod.rs index c9a2d80eee4..ae011a7594a 100644 --- a/term/src/terminalstate/mod.rs +++ b/term/src/terminalstate/mod.rs @@ -446,6 +446,7 @@ fn default_color_map() -> HashMap { /// back-pressure when there is a lot of data to read, /// and we're in control of the write side, which represents /// input from the interactive user, or pastes. +#[derive(Debug, Clone)] struct ThreadedWriter { sender: Sender, } diff --git a/term/src/terminalstate/performer.rs b/term/src/terminalstate/performer.rs index 817b137baf6..b4b71603e64 100644 --- a/term/src/terminalstate/performer.rs +++ b/term/src/terminalstate/performer.rs @@ -26,6 +26,8 @@ use unicode_normalization::{is_nfc_quick, IsNormalized, UnicodeNormalization}; use url::Url; use wezterm_bidi::ParagraphDirectionHint; +use super::{ClipboardReader, ThreadedWriter}; + /// A helper struct for implementing `vtparse::VTActor` while compartmentalizing /// the terminal state and the embedding/host terminal interface pub(crate) struct Performer<'a> { @@ -765,7 +767,26 @@ impl<'a> Performer<'a> { let selection = selection_to_selection(selection); self.set_clipboard_contents(selection, None).ok(); } - OperatingSystemCommand::QuerySelection(_) => {} + OperatingSystemCommand::QuerySelection(selection) => { + if self.config.enable_osc52_clipboard_reading() { + if let Some(clip) = self.clipboard.as_ref() { + let clipboard = selection_to_selection(selection); + if let Err(err) = clip.get_contents( + clipboard, + Box::new(Osc52ResponseSender( + self.writer.get_ref().clone(), + selection, + )), + ) { + error!("failed to get clipboard in response to OSC 52: {:#?}", err); + } + } else { + log::warn!("the clipboard is missing"); + } + } else { + log::warn!("OSC52 clipboard reading is disabled"); + } + } OperatingSystemCommand::SetSelection(selection, selection_data) => { let selection = selection_to_selection(selection); match self.set_clipboard_contents(selection, Some(selection_data)) { @@ -1071,3 +1092,19 @@ fn selection_to_selection(sel: Selection) -> ClipboardSelection { _ => ClipboardSelection::Clipboard, } } + +/// Implement [`ClipboardReader`] and wrap the content in OSC 52 sequence +/// before sending it to the [`ThreadedWriter`] +#[derive(Debug, Clone)] +struct Osc52ResponseSender(ThreadedWriter, Selection); + +impl ClipboardReader for Osc52ResponseSender { + fn write(&mut self, contents: String) -> Result<(), std::io::Error> { + // NOTE: reply should be sent in one single write. Multiple writes + // cause wrong interpretation of the osc sequence, + // for example in neovim or micro editors + let data = format!("{}", OperatingSystemCommand::SetSelection(self.1, contents)); + self.0.write_all(data.as_bytes())?; + self.0.flush() + } +} diff --git a/term/src/test/mod.rs b/term/src/test/mod.rs index e64faf0a283..7bb77d4e03c 100644 --- a/term/src/test/mod.rs +++ b/term/src/test/mod.rs @@ -4,6 +4,7 @@ use super::*; mod c0; use bitflags::bitflags; +use termwiz::escape::osc::Selection; mod c1; mod csi; // mod selection; FIXME: port to render layer @@ -36,6 +37,16 @@ impl Clipboard for LocalClip { *self.clip.lock().unwrap() = clip; Ok(()) } + fn get_contents( + &self, + _selection: ClipboardSelection, + mut writer: Box, + ) -> anyhow::Result<()> { + writer + .write(format!("{:?}", &self.clip.lock().unwrap())) + .ok(); + Ok(()) + } } struct TestTerm { @@ -54,10 +65,22 @@ impl TerminalConfiguration for TestTermConfig { fn color_palette(&self) -> ColorPalette { ColorPalette::default() } + fn enable_osc52_clipboard_reading(&self) -> bool { + true + } } impl TestTerm { fn new(height: usize, width: usize, scrollback: usize) -> Self { + TestTerm::new_with_writer(height, width, scrollback, Box::new(Vec::new())) + } + + fn new_with_writer( + height: usize, + width: usize, + scrollback: usize, + writer: Box, + ) -> Self { let _ = env_logger::Builder::new() .is_test(true) .filter_level(log::LevelFilter::Trace) @@ -74,7 +97,7 @@ impl TestTerm { Arc::new(TestTermConfig { scrollback }), "WezTerm", "O_o", - Box::new(Vec::new()), + writer, ); let clip: Arc = Arc::new(LocalClip::new()); term.set_clipboard(&clip); @@ -494,6 +517,69 @@ fn issue_1161() { ); } +struct TestOsc52Writer(std::sync::mpsc::SyncSender>); + +impl std::io::Write for TestOsc52Writer { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0 + .send(buf.to_vec()) + .map(|()| buf.len()) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::BrokenPipe, err)) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +struct TestOsc52Clip(LocalClip); + +impl Clipboard for TestOsc52Clip { + fn set_contents( + &self, + _selection: ClipboardSelection, + clip: Option, + ) -> anyhow::Result<()> { + assert_eq!(clip, Some(String::from("hello"))); + *self.0.clip.lock().unwrap() = clip; + Ok(()) + } + fn get_contents( + &self, + _selection: ClipboardSelection, + mut writer: Box, + ) -> anyhow::Result<()> { + log::error!("query selection {:?}", writer); + let osc = format!("{}", self.0.clip.lock().unwrap().clone().unwrap()); + let rtn = writer.write(osc); + assert!(rtn.is_ok(), "{:?}", rtn); + Ok(()) + } +} + +#[test] +fn test_osc52() { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let mut term = TestTerm::new_with_writer(5, 10, 0, Box::new(TestOsc52Writer(tx))); + + let clip: Arc = Arc::new(TestOsc52Clip(LocalClip::new())); + term.set_clipboard(&clip); + term.print(format!( + "{}", + OperatingSystemCommand::SetSelection(Selection::CLIPBOARD, "hello".into()) + )); + term.print(format!( + "{}", + OperatingSystemCommand::QuerySelection(Selection::CLIPBOARD) + )); + let result = rx.recv_timeout(std::time::Duration::from_secs(1)); + assert!(result.is_ok(), "{:?}", result); + assert_eq!( + String::from_utf8_lossy(&result.unwrap()), + "\u{1b}]52;c;aGVsbG8=\u{1b}\\" + ); +} + #[test] fn basic_output() { let mut term = TestTerm::new(5, 10, 0); diff --git a/termwiz/src/escape/osc.rs b/termwiz/src/escape/osc.rs index d69b1beb8d9..00d05befaca 100644 --- a/termwiz/src/escape/osc.rs +++ b/termwiz/src/escape/osc.rs @@ -1392,6 +1392,22 @@ mod test { ); } + #[test] + fn selection() { + assert_eq!( + parse(&["52", "c", "?"], "\x1b]52;c;?\x1b\\"), + OperatingSystemCommand::QuerySelection(Selection::CLIPBOARD) + ); + assert_eq!( + parse(&["52", "c"], "\x1b]52;c\x1b\\"), + OperatingSystemCommand::ClearSelection(Selection::CLIPBOARD) + ); + assert_eq!( + parse(&["52", "c", "eA=="], "\x1b]52;c;eA==\x1b\\"), + OperatingSystemCommand::SetSelection(Selection::CLIPBOARD, "x".into()) + ); + } + #[test] fn finalterm() { assert_eq!( diff --git a/wezterm-client/src/pane/clientpane.rs b/wezterm-client/src/pane/clientpane.rs index 5dbb18346b6..4192ee35c15 100644 --- a/wezterm-client/src/pane/clientpane.rs +++ b/wezterm-client/src/pane/clientpane.rs @@ -27,7 +27,7 @@ use url::Url; use wezterm_dynamic::Value; use wezterm_term::color::ColorPalette; use wezterm_term::{ - Alert, Clipboard, KeyCode, KeyModifiers, Line, MouseEvent, StableRowIndex, + Alert, Clipboard, ClipboardReader, KeyCode, KeyModifiers, Line, MouseEvent, StableRowIndex, TerminalConfiguration, TerminalSize, }; @@ -50,6 +50,23 @@ pub struct ClientPane { unseen_output: Mutex, } +/// Implement [`ClipboardReader`] for the [`smol::channel::Sender`] for +/// receiving the clipboard content from [`Clipboard::get_contents`] +/// and redirecting it to the corresponding [`smol::channel::Receiver`] +#[derive(Debug, Clone)] +struct ClientClipboardReader(smol::channel::Sender); + +impl ClipboardReader for ClientClipboardReader { + fn write(&mut self, contents: String) -> Result<(), std::io::Error> { + let tx = self.0.clone(); + promise::spawn::spawn(async move { + let _ = tx.send(contents).await; + }) + .detach(); + Ok(()) + } +} + impl ClientPane { pub fn new( client: &Arc, @@ -166,6 +183,38 @@ impl ClientPane { log::error!("ClientPane: Ignoring SetClipboard request {:?}", clipboard); } }, + Pdu::QueryClipboard(QueryClipboard { pane_id, selection }) => { + log::debug!( + "Pdu::QueryClipboard pane={:?} pane={} remote={} {:?}", + pane_id, + self.local_pane_id, + self.remote_pane_id, + selection, + ); + match self.clipboard.lock().as_ref() { + Some(clip) => { + let client = Arc::clone(&self.client); + let remote_pane_id = self.remote_pane_id; + let (tx, rx) = smol::channel::bounded(1); + clip.get_contents(selection, Box::new(ClientClipboardReader(tx)))?; + + promise::spawn::spawn(async move { + let content = rx.recv().await.ok(); + let _ = client + .client + .send_pdu(Pdu::QueryClipboardResponse(QueryClipboardResponse { + pane_id: remote_pane_id, + content, + })) + .await; + }) + .detach(); + } + None => { + log::error!("ClientPane: Ignoring QueryClipboard request"); + } + } + } Pdu::SetPalette(SetPalette { palette, .. }) => { *self.application_palette.lock() = palette != *self.configured_palette.lock(); diff --git a/wezterm-gui/src/frontend.rs b/wezterm-gui/src/frontend.rs index 66c61e72acb..b64d55e833b 100644 --- a/wezterm-gui/src/frontend.rs +++ b/wezterm-gui/src/frontend.rs @@ -200,6 +200,43 @@ impl GuiFrontEnd { }) .detach(); } + MuxNotification::QueryClipboard { + pane_id, + selection, + mut writer, + } => { + promise::spawn::spawn_into_main_thread(async move { + let fe = crate::frontend::front_end(); + log::trace!("get clipboard in pane {} {:?}", pane_id, selection); + if let Some(window) = fe.known_windows.borrow().keys().next() { + let clipboard = match selection { + ClipboardSelection::Clipboard => Clipboard::Clipboard, + ClipboardSelection::PrimarySelection => Clipboard::PrimarySelection, + }; + let future = window.get_clipboard(clipboard); + promise::spawn::spawn(async move { + let content = future.await; + match content { + Ok(content) => { + if let Err(err) = writer.write(content) { + log::error!( + "Error sending clipboard content {:?}", + err + ); + }; + } + Err(err) => { + log::error!("Error reading clipboard {:?}", err); + } + } + }) + .detach(); + } else { + log::error!("Cannot get clipboard as there are no windows"); + }; + }) + .detach(); + } } true }); diff --git a/wezterm-gui/src/termwindow/mod.rs b/wezterm-gui/src/termwindow/mod.rs index 649bce5499f..deab5b7efda 100644 --- a/wezterm-gui/src/termwindow/mod.rs +++ b/wezterm-gui/src/termwindow/mod.rs @@ -1289,6 +1289,9 @@ impl TermWindow { MuxNotification::AssignClipboard { .. } => { // Handled by frontend } + MuxNotification::QueryClipboard { .. } => { + // Handled by frontend + } MuxNotification::SaveToDownloads { .. } => { // Handled by frontend } @@ -1507,6 +1510,7 @@ impl TermWindow { .. } | MuxNotification::AssignClipboard { .. } + | MuxNotification::QueryClipboard { .. } | MuxNotification::SaveToDownloads { .. } | MuxNotification::WindowCreated(_) | MuxNotification::ActiveWorkspaceChanged(_) diff --git a/wezterm-mux-server-impl/src/dispatch.rs b/wezterm-mux-server-impl/src/dispatch.rs index c3728f1c2dc..9497f6b344c 100644 --- a/wezterm-mux-server-impl/src/dispatch.rs +++ b/wezterm-mux-server-impl/src/dispatch.rs @@ -1,7 +1,7 @@ use crate::sessionhandler::{PduSender, SessionHandler}; use anyhow::Context; use async_ossl::AsyncSslStream; -use codec::{DecodedPdu, Pdu}; +use codec::{DecodedPdu, Pdu, QueryClipboardResponse}; use futures::FutureExt; use mux::{Mux, MuxNotification}; use smol::prelude::*; @@ -64,6 +64,8 @@ where mux.subscribe(move |n| tx.try_send(Item::Notif(n)).is_ok()); } + let mut query_clipboard_tx: Option>> = None; + loop { let rx_msg = item_rx.recv(); let wait_for_read = stream.readable().map(|_| Ok(Item::Readable)); @@ -82,7 +84,25 @@ where return Err(err).context("reading Pdu from client"); } }; - handler.process_one(decoded); + // Check if this is ours + if let Pdu::QueryClipboardResponse(QueryClipboardResponse { content, .. }) = + decoded.pdu + { + query_clipboard_tx + .as_ref() + .expect("must be a valid tx at this point") + .send(content) + .await + .expect("the channel must remain open"); + query_clipboard_tx = None; + // respond to complete the promise on the client side + Pdu::UnitResponse(codec::UnitResponse {}) + .encode_async(&mut stream, decoded.serial) + .await?; + stream.flush().await.context("flushing PDU to client")?; + } else { + handler.process_one(decoded); + } } Ok(Item::WritePdu(decoded)) => { match decoded.pdu.encode_async(&mut stream, decoded.serial).await { @@ -141,6 +161,29 @@ where .await?; stream.flush().await.context("flushing PDU to client")?; } + Ok(Item::Notif(MuxNotification::QueryClipboard { + pane_id, + selection, + mut writer, + })) => { + let (tx, rx) = smol::channel::bounded(1); + query_clipboard_tx = Some(tx); + + Pdu::QueryClipboard(codec::QueryClipboard { pane_id, selection }) + .encode_async(&mut stream, 0) + .await?; + stream.flush().await.context("flushing PDU to client")?; + + promise::spawn::spawn(async move { + if let Some(content) = rx.recv().await.expect("the channel must have the message") + { + if let Err(err) = writer.write(content) { + log::error!("Error writing to the clipboard reader {}", err) + } + } + }) + .detach() + } Ok(Item::Notif(MuxNotification::TabAddedToWindow { tab_id, window_id })) => { Pdu::TabAddedToWindow(codec::TabAddedToWindow { tab_id, window_id }) .encode_async(&mut stream, 0) diff --git a/wezterm-mux-server-impl/src/sessionhandler.rs b/wezterm-mux-server-impl/src/sessionhandler.rs index 5c8e29577f4..e31e8f3bd7b 100644 --- a/wezterm-mux-server-impl/src/sessionhandler.rs +++ b/wezterm-mux-server-impl/src/sessionhandler.rs @@ -991,6 +991,8 @@ impl SessionHandler { Pdu::Pong { .. } | Pdu::ListPanesResponse { .. } | Pdu::SetClipboard { .. } + | Pdu::QueryClipboard { .. } + | Pdu::QueryClipboardResponse { .. } | Pdu::NotifyAlert { .. } | Pdu::SpawnResponse { .. } | Pdu::GetPaneRenderChangesResponse { .. } From 5bb18c72bbd10b4e12851d4049a0e1c073c1cc79 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 8 Nov 2024 12:02:05 +0400 Subject: [PATCH 2/3] Update docs/shell-integration.md Co-authored-by: Len Trigg --- docs/shell-integration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/shell-integration.md b/docs/shell-integration.md index 40cc173bb2f..67a8f34aa20 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -205,8 +205,8 @@ command like this: printf "\033]7;c;?\033\\" ``` -- `c` pastes to the system clipboard -- `p` pastes to the primary selection buffer +- `c` pastes from the system clipboard +- `p` pastes from the primary selection buffer Note that this feature poses a potential security risk and is disabled by default. It requires enabling via [a configuration option](config/lua/config/enable_osc52_clipboard_reading.md). From c42784df2f6e5efe3423041be3fc129179066e0e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 15 Nov 2024 12:02:59 +0400 Subject: [PATCH 3/3] Update docs/shell-integration.md Co-authored-by: Len Trigg --- docs/shell-integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/shell-integration.md b/docs/shell-integration.md index 67a8f34aa20..55f3a261e7d 100644 --- a/docs/shell-integration.md +++ b/docs/shell-integration.md @@ -202,7 +202,7 @@ The data can be pasted from the `System Clipboard` or `Primary Selection` with a command like this: ```bash -printf "\033]7;c;?\033\\" +printf "\033]52;c;?\033\\" ``` - `c` pastes from the system clipboard