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}