1use base64::{Engine as _, engine::general_purpose::STANDARD};
4
5pub const MAX_OSC52_PAYLOAD: usize = 74_994;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ClipboardSelection {
11 Clipboard,
13 Primary,
15 Secondary,
17 CutBuffer(u8),
19}
20
21impl ClipboardSelection {
22 pub fn osc52_code(self) -> Result<char, Osc52Error> {
24 match self {
25 Self::Clipboard => Ok('c'),
26 Self::Primary => Ok('p'),
27 Self::Secondary => Ok('s'),
28 Self::CutBuffer(index @ 0..=7) => Ok(char::from(b'0' + index)),
29 Self::CutBuffer(index) => Err(Osc52Error::InvalidSelection(index)),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Osc52Error {
37 PayloadTooLarge {
39 len: usize,
41 max: usize,
43 },
44 InvalidSelection(u8),
46}
47
48impl std::fmt::Display for Osc52Error {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::PayloadTooLarge { len, max } => {
52 write!(f, "OSC 52 payload too large ({len} > {max})")
53 }
54 Self::InvalidSelection(index) => {
55 write!(f, "cut buffer index must be 0..=7 (got {index})")
56 }
57 }
58 }
59}
60
61impl std::error::Error for Osc52Error {}
62
63pub fn encode_set(selection: ClipboardSelection, bytes: &[u8]) -> Result<Vec<u8>, Osc52Error> {
65 encode_set_with_limit(selection, bytes, MAX_OSC52_PAYLOAD)
66}
67
68pub fn encode_set_with_limit(
72 selection: ClipboardSelection,
73 bytes: &[u8],
74 max: usize,
75) -> Result<Vec<u8>, Osc52Error> {
76 let code = selection.osc52_code()?;
77 let len = bytes.len().div_ceil(3).saturating_mul(4);
78 if len > max {
79 return Err(Osc52Error::PayloadTooLarge { len, max });
80 }
81 let encoded = STANDARD.encode(bytes);
82 Ok(format!("\x1b]52;{code};{encoded}\x07").into_bytes())
83}
84
85pub fn encode_query(selection: ClipboardSelection) -> Result<Vec<u8>, Osc52Error> {
87 let code = selection.osc52_code()?;
88 Ok(format!("\x1b]52;{code};?\x07").into_bytes())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn osc52_wire_format_and_selections() {
97 assert_eq!(
98 encode_set(ClipboardSelection::Clipboard, b"hello").unwrap(),
99 b"\x1b]52;c;aGVsbG8=\x07"
100 );
101 for (selection, code) in [
102 (ClipboardSelection::Clipboard, 'c'),
103 (ClipboardSelection::Primary, 'p'),
104 (ClipboardSelection::Secondary, 's'),
105 (ClipboardSelection::CutBuffer(0), '0'),
106 (ClipboardSelection::CutBuffer(7), '7'),
107 ] {
108 assert_eq!(
109 encode_query(selection).unwrap(),
110 format!("\x1b]52;{code};?\x07").as_bytes()
111 );
112 assert_eq!(
113 encode_set(selection, b"").unwrap(),
114 format!("\x1b]52;{code};\x07").as_bytes()
115 );
116 }
117 for index in [8, 255] {
118 assert_eq!(
119 encode_query(ClipboardSelection::CutBuffer(index)),
120 Err(Osc52Error::InvalidSelection(index))
121 );
122 assert_eq!(
123 encode_set(ClipboardSelection::CutBuffer(index), b"x"),
124 Err(Osc52Error::InvalidSelection(index))
125 );
126 }
127 }
128
129 #[test]
130 fn osc52_payload_cap_and_mux_wire_format() {
131 let selection = ClipboardSelection::Clipboard;
132 assert!(encode_set_with_limit(selection, b"abc", 4).is_ok());
133 assert_eq!(
134 encode_set_with_limit(selection, b"abcd", 4),
135 Err(Osc52Error::PayloadTooLarge { len: 8, max: 4 })
136 );
137 assert!(encode_set(selection, &vec![0; 56_244]).is_ok());
138 assert!(matches!(
139 encode_set(selection, &vec![0; 56_245]),
140 Err(Osc52Error::PayloadTooLarge { .. })
141 ));
142 let seq = encode_set(selection, b"hello").unwrap();
143 let mut tmux = Vec::new();
144 crate::mux_passthrough::tmux_wrap(&mut tmux, &seq).unwrap();
145 assert_eq!(tmux, b"\x1bPtmux;\x1b\x1b]52;c;aGVsbG8=\x07\x1b\\");
146 let mut screen = Vec::new();
147 crate::mux_passthrough::screen_wrap(&mut screen, &seq).unwrap();
148 assert_eq!(screen, b"\x1bP\x1b]52;c;aGVsbG8=\x07\x1b\\");
149 }
150}