Skip to content

Commit ab0fd66

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 ab0fd66

File tree

6 files changed

+366
-1
lines changed

6 files changed

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

src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@
6666
//! [`EnableLineWrap`](terminal/struct.EnableLineWrap.html)
6767
//! - Alternate screen - [`EnterAlternateScreen`](terminal/struct.EnterAlternateScreen.html),
6868
//! [`LeaveAlternateScreen`](terminal/struct.LeaveAlternateScreen.html)
69+
//! - Module [`clipboard`](clipboard/index.html) (requires
70+
//! [`feature = "clipboard"`](#optional-features))
71+
//! - Clipboard - [`CopyToClipboard`](clipboard/struct.CopyToClipboard.html)
6972
//!
7073
//! ### Command Execution
7174
//!
@@ -246,6 +249,10 @@ pub mod terminal;
246249
/// A module to query if the current instance is a tty.
247250
pub mod tty;
248251

252+
/// A module for clipboard interaction
253+
#[cfg(feature = "clipboard")]
254+
pub mod clipboard;
255+
249256
#[cfg(windows)]
250257
/// A module that exposes one function to check if the current terminal supports ANSI sequences.
251258
pub mod ansi_support;

0 commit comments

Comments
 (0)