Skip to content

Commit 5751c03

Browse files
author
Naseschwarz
committed
Add copying to clipboard using OSC52
Many terminal emulators support copying text to clipboard using ANSI OSC Ps; PT ST with Ps = 5 2, see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands This enables copying even through SSH and terminal multiplexers.
1 parent cb08b4d commit 5751c03

File tree

6 files changed

+380
-1
lines changed

6 files changed

+380
-1
lines changed

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ use-dev-tty = ["filedescriptor", "rustix/process"]
4747
## Enables `is_*` helper functions for event enums.
4848
derive-more = ["dep:derive_more"]
4949

50+
## Enables interacting with a host clipboard via [`clipboard`](clipboard/index.html)
51+
clipboard = ["dep:base64"]
52+
5053
[dependencies]
54+
base64 = { version = "0.22", optional = true }
5155
bitflags = { version = "2.3" }
5256
derive_more = { version = "1.0.0", features = ["is_variant"], optional = true }
5357
document-features = "0.2.10"
@@ -112,3 +116,7 @@ required-features = ["events"]
112116
[[example]]
113117
name = "key-display"
114118
required-features = ["events"]
119+
120+
[[example]]
121+
name = "copy-to-clipboard"
122+
required-features = ["clipboard"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ features = ["event-stream"]
154154
| `events` | Reading input/system events (enabled by default) |
155155
| `filedescriptor` | Use raw filedescriptor for all events rather then mio dependency |
156156
| `derive-more` | Adds `is_*` helper functions for event types |
157+
| `clipboard` | Enables crossterm::clipboard |
157158

158159

159160
To use crossterm as a very thin layer you can disable the `events` feature or use `filedescriptor` feature.
@@ -172,6 +173,7 @@ This can disable `mio` / `signal-hook` / `signal-hook-mio` dependencies.
172173
| `futures-core` | For async stream of events | only with `event-stream` feature flag |
173174
| `serde` | ***ser***ializing and ***de***serializing of events | only with `serde` feature flag |
174175
| `derive_more` | Adds `is_*` helper functions for event types | optional (`derive-more` feature), included by default |
176+
| `base64` | Encoding clipboard data for OSC52 sequences in crossterm::clipboard | only with `clipboard` feature flag |
175177

176178
### Other Resources
177179

examples/copy-to-clipboard.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//! Demonstrates copying a string to clipboard
2+
//!
3+
//! This example uses OSC control sequence `Pr = 5 2` (See
4+
//! <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands)>
5+
//! to copy data to the terminal host clipboard.
6+
//!
7+
//! This only works if it is enabled on the respective terminal emulator. If a terminal multiplexer
8+
//! is used, the multiplexer will likely need to support it, too.
9+
//!
10+
//! ```no_run
11+
//! cargo run --example copy-to-clipboard -- --clipboard "Some String"
12+
//! cargo run --example copy-to-clipboard -- --primary "Some String"
13+
//! cargo run --example copy-to-clipboard -- "Some String"
14+
//! ```
15+
16+
use std::io;
17+
18+
use crossterm::clipboard;
19+
use crossterm::execute;
20+
21+
fn main() -> io::Result<()> {
22+
let mut stdout = io::stdout();
23+
let mut args = std::env::args();
24+
args.next(); // Skip to first argument
25+
26+
let default_text = String::from("Example text");
27+
let (text, dest) = match args.next().as_deref() {
28+
Some("--clipboard") => (
29+
args.next().unwrap_or(default_text),
30+
clipboard::ClipboardType::Clipboard,
31+
),
32+
Some("--primary") => (
33+
args.next().unwrap_or(default_text),
34+
clipboard::ClipboardType::Primary,
35+
),
36+
Some(text) => (text.to_owned(), clipboard::ClipboardType::Clipboard),
37+
None => (default_text, clipboard::ClipboardType::Clipboard),
38+
};
39+
execute!(
40+
stdout,
41+
clipboard::CopyToClipboard {
42+
content: text,
43+
destination: clipboard::ClipboardSelection(vec![dest])
44+
}
45+
)
46+
}

src/clipboard.rs

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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)
8+
use base64::prelude::{Engine, BASE64_STANDARD};
9+
10+
use std::fmt;
11+
use std::str::FromStr;
12+
13+
use 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)]
22+
pub 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+
39+
impl 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+
49+
impl 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)]
61+
pub 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+
72+
impl 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+
80+
impl 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+
86+
impl 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)]
142+
pub 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+
152+
impl<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+
173+
impl<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)]
210+
mod 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+
}

0 commit comments

Comments
 (0)