Skip to main content

uncurses/ansi/
clipboard.rs

1//! Clipboard and selection access through OSC 52.
2//!
3//! ## Category
4//!
5//! OSC 52 sets, requests, or clears named clipboards/selections. This module
6//! provides the common system clipboard (`c`) and primary selection (`p`) selector
7//! bytes plus writers for each operation.
8//!
9//! ## OSC framing
10//!
11//! The emitted sequences use 7-bit OSC with BEL termination:
12//!
13//! ```text
14//! ESC ] 52 ; c ; aGk= BEL
15//! ──┬── ┬─   ┬   ─┬─  ─┬─
16//!  OSC code  Pc  base64 terminator
17//! ```
18//!
19//! Clipboard data is base64-encoded by [`write_set_clipboard`]. Request and
20//! clear operations use `?` or an empty payload in the same field.
21//!
22//! ## Mode interaction
23//!
24//! OSC 52 is not gated by an ANSI/DEC mode. Terminals may still reject clipboard
25//! access by policy; this module only encodes the request bytes.
26
27use std::io::{self, Write};
28
29/// OSC 52 selector byte `c` for the system clipboard.
30pub const SYSTEM_CLIPBOARD: u8 = b'c';
31
32/// OSC 52 selector byte `p` for the primary selection.
33pub const PRIMARY_CLIPBOARD: u8 = b'p';
34
35/// Set a clipboard or selection with `ESC ] 52 ; <pc> ; <base64-data> BEL`.
36///
37/// `pc` is a selector such as [`SYSTEM_CLIPBOARD`] or [`PRIMARY_CLIPBOARD`]. `data` is base64-encoded by this function before it is emitted.
38pub fn write_set_clipboard<W: Write>(w: &mut W, pc: u8, data: &[u8]) -> io::Result<()> {
39    let encoded = base64_encode(data);
40    write!(w, "\x1b]52;{};{}\x07", pc as char, encoded)
41}
42
43/// Request clipboard contents with `ESC ] 52 ; <pc> ; ? BEL`.
44///
45/// `pc` is written as a single selector character. Replies, when allowed by terminal policy, arrive asynchronously as OSC 52 data.
46pub fn write_request_clipboard<W: Write>(w: &mut W, pc: u8) -> io::Result<()> {
47    write!(w, "\x1b]52;{};?\x07", pc as char)
48}
49
50/// Clear a clipboard or selection with `ESC ] 52 ; <pc> ; BEL`.
51///
52/// The payload field is intentionally empty; `pc` selects which clipboard or selection to clear.
53pub fn write_clear_clipboard<W: Write>(w: &mut W, pc: u8) -> io::Result<()> {
54    write!(w, "\x1b]52;{};\x07", pc as char)
55}
56
57fn base64_encode(input: &[u8]) -> String {
58    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
59    let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
60    let mut i = 0;
61    while i + 3 <= input.len() {
62        let n = ((input[i] as u32) << 16) | ((input[i + 1] as u32) << 8) | input[i + 2] as u32;
63        out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
64        out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
65        out.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
66        out.push(ALPHABET[(n & 0x3f) as usize] as char);
67        i += 3;
68    }
69    let rem = input.len() - i;
70    if rem == 1 {
71        let n = (input[i] as u32) << 16;
72        out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
73        out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
74        out.push('=');
75        out.push('=');
76    } else if rem == 2 {
77        let n = ((input[i] as u32) << 16) | ((input[i + 1] as u32) << 8);
78        out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
79        out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
80        out.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
81        out.push('=');
82    }
83    out
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_base64() {
92        assert_eq!(base64_encode(b"hi"), "aGk=");
93        assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
94        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
95    }
96
97    #[test]
98    fn test_set_clipboard() {
99        let mut buf = Vec::new();
100        write_set_clipboard(&mut buf, SYSTEM_CLIPBOARD, b"hi").unwrap();
101        assert_eq!(buf, b"\x1b]52;c;aGk=\x07");
102    }
103
104    #[test]
105    fn test_request_clipboard() {
106        let mut buf = Vec::new();
107        write_request_clipboard(&mut buf, PRIMARY_CLIPBOARD).unwrap();
108        assert_eq!(buf, b"\x1b]52;p;?\x07");
109    }
110}