Skip to main content

crossterm/
clipboard.rs

1//! # Clipboard
2//!
3//! The `clipboard` module provides functionality to work with a host clipboard.
4//!
5//! ## Implemented operations:
6//!
7//! - Copy: [`CopyToClipboard`](struct.CopyToClipboard.html)
8use base64::prelude::{Engine, BASE64_STANDARD};
9
10use std::fmt;
11use std::str::FromStr;
12
13use crate::{osc, Command};
14
15/// Different clipboard types
16///
17/// Some operating systems and desktop environments support multiple buffers
18/// for copy/cut/paste. Their details differ between operating systems.
19/// See <https://specifications.freedesktop.org/clipboard-spec/latest/>
20/// for a detailed survey of supported types based on the X window system.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ClipboardType {
23    /// Default clipboard when using Ctrl+C or Ctrl+V
24    Clipboard,
25
26    /// Clipboard on Linux/X/Wayland when using selection and middle mouse button
27    Primary,
28
29    /// Other clipboard type not explicitly supported by crossterm
30    ///
31    /// See
32    /// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands)
33    /// for potential values.
34    ///
35    /// Note that support for these in terminal emulators is very limited.
36    Other(char),
37}
38
39impl From<&ClipboardType> for char {
40    fn from(val: &ClipboardType) -> Self {
41        match val {
42            ClipboardType::Clipboard => 'c',
43            ClipboardType::Primary => 'p',
44            ClipboardType::Other(other) => *other,
45        }
46    }
47}
48
49impl From<char> for ClipboardType {
50    fn from(value: char) -> Self {
51        match value {
52            'c' => ClipboardType::Clipboard,
53            'p' => ClipboardType::Primary,
54            other => ClipboardType::Other(other),
55        }
56    }
57}
58
59/// A sequence of clipboard types
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ClipboardSelection(
62    /// An ordered list of clipboards which will be the destination for the copied selection.
63    ///
64    /// Order matters due to implementations deviating from the
65    /// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands)
66    /// reference. Some terminal emulators may only interpret the first character of this
67    /// parameter. For differences, see
68    /// [`CopyToClipboard` (Terminal Support)](struct.CopyToClipboard.html#terminal-support).
69    pub Vec<ClipboardType>,
70);
71
72impl ClipboardSelection {
73    /// Returns a String corresponsing to the "Pc" parameter of the OSC52
74    /// sequence.
75    fn to_osc52_pc(&self) -> String {
76        self.0.iter().map(Into::<char>::into).collect()
77    }
78}
79
80impl fmt::Display for ClipboardSelection {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        f.write_str(&self.to_osc52_pc())
83    }
84}
85
86impl FromStr for ClipboardSelection {
87    type Err = ();
88    fn from_str(s: &str) -> Result<Self, Self::Err> {
89        Ok(ClipboardSelection(
90            s.chars().map(From::<char>::from).collect(),
91        ))
92    }
93}
94
95/// A command that copies to clipboard
96///
97/// This command uses OSC control sequence `Pr = 5 2` (See
98/// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) )
99/// to copy data to the terminal host clipboard.
100///
101/// This only works if it is enabled on the user's terminal emulator. If a terminal multiplexer
102/// is used, the multiplexer must support it, too.
103///
104/// Commands must be executed/queued for execution otherwise they do nothing.
105///
106/// # Examples
107///
108/// ```no_run
109/// use crossterm::execute;
110/// use crossterm::clipboard::CopyToClipboard;
111/// // Copy foo to clipboard
112/// execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from("foo"));
113/// // Copy bar to primary
114/// execute!(std::io::stdout(), CopyToClipboard::to_primary_from("bar"));
115/// ```
116///
117/// See also examples/copy-to-clipboard.rs.
118///
119/// # Terminal Support
120///
121/// The following table shows what destinations are filled by different terminal emulators when
122/// asked to copy to different destination sequences.
123///
124/// | Terminal (Version)    | dest ''   | dest 'c'  | dest 'p' | dest 'cp'     | dest'pc'      |
125/// | --------------------- | --------- | --------- | -------- | ------------- | ------------- |
126/// | xterm (397) *3        | primary   | clipboard | primary  | clipb., prim. | clipb., prim. |
127/// | Alacritty (0.15.1) *3 | clipboard | clipboard | primary  | clipb.        | prim.         |
128/// | Wezterm (*1) *3       | clipboard | clipboard | primary  | clipb.        | clipb.        |
129/// | Konsole (24.12.3) *3  | clipboard | clipboard | primary  | clipb., prim. | clipb., prim. |
130/// | Kitty (0.40.0) *3     | clipboard | clipboard | primary  | clipb.        | clipb.        |
131/// | foot (1.20.2) *3      | clipboard | clipboard | primary  | clipb., prim. | clipb., prim. |
132/// | tmux (3.5a) *2 *3     | primary   | clipboard | primary  | clipb., prim. | clipb., prim. |
133///
134/// Asterisks:
135/// 1. 20240203-110809-5046fc22
136/// 2. set-clipboard set to [external](https://github.com/tmux/tmux/wiki/Clipboard#how-it-works),
137///    i.e. this is OSC52 pass-through.
138/// 3. This was tested on wayland with the
139///    [primary selection protocol](https://wayland.app/protocols/primary-selection-unstable-v1)
140///    enabled.
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct CopyToClipboard<T> {
143    /// Content to be copied
144    pub content: T,
145    /// Sequence of copy destinations
146    ///
147    /// Not all sequences are equally supported by terminal emulators. See
148    /// [`CopyToClipboard` (Terminal Support)](struct.CopyToClipboard.html#terminal-support).
149    pub destination: ClipboardSelection,
150}
151
152impl<T: AsRef<[u8]>> Command for CopyToClipboard<T> {
153    fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
154        write!(
155            f,
156            osc!("52;{destination};{encoded_text}"),
157            destination = self.destination.to_osc52_pc(),
158            encoded_text = BASE64_STANDARD.encode(&self.content)
159        )
160    }
161
162    #[cfg(windows)]
163    fn execute_winapi(&self) -> std::io::Result<()> {
164        use std::io;
165
166        Err(io::Error::new(
167            io::ErrorKind::Unsupported,
168            "Copying is not implemented for the Windows API.",
169        ))
170    }
171}
172
173impl<T: AsRef<[u8]>> CopyToClipboard<T> {
174    /// Construct a [`CopyToClipboard`] that writes content into the
175    /// "clipboard" (or 'c') clipboard selection.
176    ///
177    /// # Example
178    ///
179    /// ```no_run
180    /// use crossterm::{execute, Command};
181    /// use crossterm::clipboard::CopyToClipboard;
182    /// execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from("foo"));
183    /// ```
184    pub fn to_clipboard_from(content: T) -> CopyToClipboard<T> {
185        CopyToClipboard {
186            content,
187            destination: ClipboardSelection(vec![ClipboardType::Clipboard]),
188        }
189    }
190
191    /// Construct a [`CopyToClipboard`] that writes content into the "primary"
192    /// (or 'p') clipboard selection.
193    ///
194    /// # Example
195    ///
196    /// ```no_run
197    /// use crossterm::execute;
198    /// use crossterm::clipboard::CopyToClipboard;
199    /// execute!(std::io::stdout(), CopyToClipboard::to_primary_from("foo"));
200    /// ```
201    pub fn to_primary_from(content: T) -> CopyToClipboard<T> {
202        CopyToClipboard {
203            content,
204            destination: ClipboardSelection(vec![ClipboardType::Primary]),
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_clipboard_string_to_selection() {
215        assert_eq!(
216            ClipboardSelection::from_str("p").unwrap(),
217            ClipboardSelection(vec![ClipboardType::Primary])
218        );
219        assert_eq!(
220            ClipboardSelection::from_str("").unwrap(),
221            ClipboardSelection(vec![])
222        );
223        assert_eq!(
224            ClipboardSelection::from_str("cp").unwrap(),
225            ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary])
226        );
227    }
228    #[test]
229    fn test_clipboard_selection_to_osc52_pc() {
230        assert_eq!(ClipboardSelection(vec![]).to_osc52_pc(), "");
231        assert_eq!(
232            ClipboardSelection(vec![ClipboardType::Clipboard]).to_osc52_pc(),
233            "c"
234        );
235        assert_eq!(
236            ClipboardSelection(vec![ClipboardType::Primary]).to_osc52_pc(),
237            "p"
238        );
239        assert_eq!(
240            ClipboardSelection(vec![ClipboardType::Primary, ClipboardType::Clipboard])
241                .to_osc52_pc(),
242            "pc"
243        );
244        assert_eq!(
245            ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary])
246                .to_osc52_pc(),
247            "cp"
248        );
249        assert_eq!(
250            ClipboardSelection(vec![ClipboardType::Other('s')]).to_osc52_pc(),
251            "s"
252        );
253    }
254
255    #[test]
256    fn test_clipboard_copy_string_osc52() {
257        let mut buffer = String::new();
258        super::CopyToClipboard {
259            content: "foo",
260            destination: ClipboardSelection(vec![ClipboardType::Clipboard]),
261        }
262        .write_ansi(&mut buffer)
263        .unwrap();
264        assert_eq!(buffer, "\x1b]52;c;Zm9v\x1b\\");
265
266        buffer.clear();
267        super::CopyToClipboard {
268            content: "foo",
269            destination: ClipboardSelection(vec![ClipboardType::Primary]),
270        }
271        .write_ansi(&mut buffer)
272        .unwrap();
273        assert_eq!(buffer, "\x1b]52;p;Zm9v\x1b\\");
274
275        buffer.clear();
276        super::CopyToClipboard {
277            content: "foo",
278            destination: ClipboardSelection(vec![ClipboardType::Primary, ClipboardType::Clipboard]),
279        }
280        .write_ansi(&mut buffer)
281        .unwrap();
282        assert_eq!(buffer, "\x1b]52;pc;Zm9v\x1b\\");
283
284        buffer.clear();
285        super::CopyToClipboard {
286            content: "foo",
287            destination: ClipboardSelection(vec![]),
288        }
289        .write_ansi(&mut buffer)
290        .unwrap();
291        assert_eq!(buffer, "\x1b]52;;Zm9v\x1b\\");
292    }
293
294    #[test]
295    fn test_clipboard_copy_string_osc52_constructor() {
296        let mut buffer = String::new();
297        super::CopyToClipboard::to_clipboard_from("foo")
298            .write_ansi(&mut buffer)
299            .unwrap();
300        assert_eq!(buffer, "\x1b]52;c;Zm9v\x1b\\");
301
302        let mut buffer = String::new();
303        super::CopyToClipboard::to_primary_from("foo")
304            .write_ansi(&mut buffer)
305            .unwrap();
306        assert_eq!(buffer, "\x1b]52;p;Zm9v\x1b\\");
307    }
308}