Skip to content

Commit 16bc099

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 f4b4806 commit 16bc099

File tree

5 files changed

+136
-2
lines changed

5 files changed

+136
-2
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 copying to terminal via the CopyToClipboard command
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"]

examples/copy.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
//! cargo run --example copy -- --clipboard "Some String"
11+
//!
12+
//! cargo run --example copy -- --primary "Some String"
13+
//!
14+
//! cargo run --example copy -- "Some String"
15+
16+
use std::io;
17+
18+
use crossterm::execute;
19+
use crossterm::terminal::{ClipboardDestination, CopyToClipboard};
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+
ClipboardDestination::Clipboard,
31+
),
32+
Some("--primary") => (
33+
args.next().unwrap_or(default_text),
34+
ClipboardDestination::Primary,
35+
),
36+
Some(text) => (text.to_owned(), ClipboardDestination::Clipboard),
37+
None => (default_text, ClipboardDestination::Clipboard),
38+
};
39+
execute!(stdout, CopyToClipboard(text, dest))
40+
}

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
//! [`SetSize`](terminal/struct.SetSize.html),
6464
//! [`SetTitle`](terminal/struct.SetTitle.html),
6565
//! [`DisableLineWrap`](terminal/struct.DisableLineWrap.html),
66-
//! [`EnableLineWrap`](terminal/struct.EnableLineWrap.html)
66+
//! [`EnableLineWrap`](terminal/struct.EnableLineWrap.html),
67+
//! [`CopyToClipboard`](terminal/struct.CopyToClipboard.html)
6768
//! - Alternate screen - [`EnterAlternateScreen`](terminal/struct.EnterAlternateScreen.html),
6869
//! [`LeaveAlternateScreen`](terminal/struct.LeaveAlternateScreen.html)
6970
//!

src/macros.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
/// Append a the first few characters of an ANSI escape code to the given string.
1+
/// Concatenate string literals while prepending a ANSI control sequence introducer (`"\x1b["`)
22
#[macro_export]
33
#[doc(hidden)]
44
macro_rules! csi {
55
($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) };
66
}
77

8+
/// Concatenate string literals while prepending a xterm Operating System Commands (OSC)
9+
/// introducer (`"\x1b]"`) and appending a BEL (`"\x07"`).
10+
#[macro_export]
11+
#[doc(hidden)]
12+
macro_rules! osc {
13+
($( $l:expr ),*) => { concat!("\x1B]", $( $l ),*, "\x07") };
14+
}
15+
816
/// Queues one or more command(s) for further execution.
917
///
1018
/// Queued commands must be flushed to the underlying device to be executed.

src/terminal.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,17 @@
8585
8686
use std::{fmt, io};
8787

88+
#[cfg(feature = "clipboard")]
89+
use base64::prelude::{Engine, BASE64_STANDARD};
8890
#[cfg(windows)]
8991
use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer};
9092
#[cfg(feature = "serde")]
9193
use serde::{Deserialize, Serialize};
9294
#[cfg(windows)]
9395
use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT;
9496

97+
#[cfg(feature = "clipboard")]
98+
use crate::osc;
9599
#[doc(no_inline)]
96100
use crate::Command;
97101
use crate::{csi, impl_display};
@@ -396,6 +400,63 @@ impl<T: fmt::Display> Command for SetTitle<T> {
396400
}
397401
}
398402

403+
/// Different clipboard classes
404+
#[cfg(feature = "clipboard")]
405+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406+
pub enum ClipboardDestination {
407+
/// Default clipboard when using Ctrl+C or Ctrl+V
408+
Clipboard,
409+
410+
/// Clipboard on Linux/X/Wayland when using selection and middle mouse button
411+
Primary,
412+
}
413+
414+
/// A command that copies to clipboard
415+
///
416+
/// # Notes
417+
///
418+
/// This command uses OSC control sequence `Pr = 5 2` (See
419+
/// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) )
420+
/// to copy data to the terminal host clipboard.
421+
///
422+
/// This only works if it is enabled on the respective terminal emulator. If a terminal multiplexer
423+
/// is used, the multiplexer will likely need to support it, too.
424+
///
425+
/// Commands must be executed/queued for execution otherwise they do nothing.
426+
///
427+
/// # Examples
428+
///
429+
/// See examples/copy.rs for a working example.
430+
#[cfg(feature = "clipboard")]
431+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432+
pub struct CopyToClipboard<T>(pub T, pub ClipboardDestination);
433+
434+
#[cfg(feature = "clipboard")]
435+
impl<T: AsRef<[u8]>> Command for CopyToClipboard<T> {
436+
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
437+
write!(
438+
f,
439+
osc!("52;{destination};{encoded_text}"),
440+
destination = match self.1 {
441+
ClipboardDestination::Clipboard => 'c',
442+
ClipboardDestination::Primary => 'p',
443+
},
444+
encoded_text = BASE64_STANDARD.encode(&self.0)
445+
)
446+
}
447+
448+
#[cfg(windows)]
449+
fn execute_winapi(&self) -> std::io::Result<()> {
450+
use std::io;
451+
452+
Err(io::Error::new(
453+
io::ErrorKind::Unsupported,
454+
"Copying is not implemented for the Windows API.",
455+
))
456+
}
457+
458+
}
459+
399460
/// A command that instructs the terminal emulator to begin a synchronized frame.
400461
///
401462
/// # Notes
@@ -565,4 +626,20 @@ mod tests {
565626
// check we're back to normal mode
566627
assert!(!is_raw_mode_enabled().unwrap());
567628
}
629+
630+
#[test]
631+
#[cfg(feature = "clipboard")]
632+
fn test_copy_string_osc52() {
633+
let mut buffer = String::new();
634+
super::CopyToClipboard("foo", ClipboardDestination::Clipboard)
635+
.write_ansi(&mut buffer)
636+
.unwrap();
637+
assert_eq!(buffer, "\x1b]52;c;Zm9v\x07");
638+
639+
buffer.clear();
640+
super::CopyToClipboard("foo", ClipboardDestination::Primary)
641+
.write_ansi(&mut buffer)
642+
.unwrap();
643+
assert_eq!(buffer, "\x1b]52;p;Zm9v\x07");
644+
}
568645
}

0 commit comments

Comments
 (0)