Skip to main content

tvscreener/tui/
clipboard.rs

1// Copyright 2026 tvscreener-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Clipboard helpers for the TUI (OSC 52; no system clipboard crate).
5
6use std::io::{self, Write};
7
8/// Copies `text` to the terminal clipboard via OSC 52.
9///
10/// Works in many Linux terminal emulators (and often over SSH).
11/// tmux/screen may need clipboard passthrough enabled.
12///
13/// # Errors
14///
15/// Returns an I/O error when writing to stdout fails.
16pub fn copy_via_osc52(text: &str) -> io::Result<()> {
17    let encoded = encode_base64(text.as_bytes());
18    let mut out = io::stdout();
19    // BEL-terminated OSC 52 (widely supported).
20    write!(out, "\x1b]52;c;{encoded}\x07")?;
21    out.flush()
22}
23
24fn encode_base64(data: &[u8]) -> String {
25    const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
26    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
27    let mut i = 0;
28    while i + 3 <= data.len() {
29        let n = (u32::from(data[i]) << 16) | (u32::from(data[i + 1]) << 8) | u32::from(data[i + 2]);
30        out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
31        out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
32        out.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
33        out.push(TABLE[(n & 0x3f) as usize] as char);
34        i += 3;
35    }
36    match data.len() - i {
37        1 => {
38            let n = u32::from(data[i]) << 16;
39            out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
40            out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
41            out.push('=');
42            out.push('=');
43        }
44        2 => {
45            let n = (u32::from(data[i]) << 16) | (u32::from(data[i + 1]) << 8);
46            out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
47            out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
48            out.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
49            out.push('=');
50        }
51        _ => {}
52    }
53    out
54}
55
56#[cfg(test)]
57mod tests {
58    use super::encode_base64;
59
60    #[test]
61    fn base64_known_vectors() {
62        assert_eq!(encode_base64(b""), "");
63        assert_eq!(encode_base64(b"f"), "Zg==");
64        assert_eq!(encode_base64(b"fo"), "Zm8=");
65        assert_eq!(encode_base64(b"foo"), "Zm9v");
66        assert_eq!(encode_base64(br#"{"rows":[]}"#), "eyJyb3dzIjpbXX0=");
67    }
68}