use base64::{Engine as _, engine::general_purpose::STANDARD};
pub const MAX_OSC52_PAYLOAD: usize = 74_994;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipboardSelection {
Clipboard,
Primary,
Secondary,
CutBuffer(u8),
}
impl ClipboardSelection {
pub fn osc52_code(self) -> Result<char, Osc52Error> {
match self {
Self::Clipboard => Ok('c'),
Self::Primary => Ok('p'),
Self::Secondary => Ok('s'),
Self::CutBuffer(index @ 0..=7) => Ok(char::from(b'0' + index)),
Self::CutBuffer(index) => Err(Osc52Error::InvalidSelection(index)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Osc52Error {
PayloadTooLarge {
len: usize,
max: usize,
},
InvalidSelection(u8),
}
impl std::fmt::Display for Osc52Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PayloadTooLarge { len, max } => {
write!(f, "OSC 52 payload too large ({len} > {max})")
}
Self::InvalidSelection(index) => {
write!(f, "cut buffer index must be 0..=7 (got {index})")
}
}
}
}
impl std::error::Error for Osc52Error {}
pub fn encode_set(selection: ClipboardSelection, bytes: &[u8]) -> Result<Vec<u8>, Osc52Error> {
encode_set_with_limit(selection, bytes, MAX_OSC52_PAYLOAD)
}
pub fn encode_set_with_limit(
selection: ClipboardSelection,
bytes: &[u8],
max: usize,
) -> Result<Vec<u8>, Osc52Error> {
let code = selection.osc52_code()?;
let len = bytes.len().div_ceil(3).saturating_mul(4);
if len > max {
return Err(Osc52Error::PayloadTooLarge { len, max });
}
let encoded = STANDARD.encode(bytes);
Ok(format!("\x1b]52;{code};{encoded}\x07").into_bytes())
}
pub fn encode_query(selection: ClipboardSelection) -> Result<Vec<u8>, Osc52Error> {
let code = selection.osc52_code()?;
Ok(format!("\x1b]52;{code};?\x07").into_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn osc52_wire_format_and_selections() {
assert_eq!(
encode_set(ClipboardSelection::Clipboard, b"hello").unwrap(),
b"\x1b]52;c;aGVsbG8=\x07"
);
for (selection, code) in [
(ClipboardSelection::Clipboard, 'c'),
(ClipboardSelection::Primary, 'p'),
(ClipboardSelection::Secondary, 's'),
(ClipboardSelection::CutBuffer(0), '0'),
(ClipboardSelection::CutBuffer(7), '7'),
] {
assert_eq!(
encode_query(selection).unwrap(),
format!("\x1b]52;{code};?\x07").as_bytes()
);
assert_eq!(
encode_set(selection, b"").unwrap(),
format!("\x1b]52;{code};\x07").as_bytes()
);
}
for index in [8, 255] {
assert_eq!(
encode_query(ClipboardSelection::CutBuffer(index)),
Err(Osc52Error::InvalidSelection(index))
);
assert_eq!(
encode_set(ClipboardSelection::CutBuffer(index), b"x"),
Err(Osc52Error::InvalidSelection(index))
);
}
}
#[test]
fn osc52_payload_cap_and_mux_wire_format() {
let selection = ClipboardSelection::Clipboard;
assert!(encode_set_with_limit(selection, b"abc", 4).is_ok());
assert_eq!(
encode_set_with_limit(selection, b"abcd", 4),
Err(Osc52Error::PayloadTooLarge { len: 8, max: 4 })
);
assert!(encode_set(selection, &vec![0; 56_244]).is_ok());
assert!(matches!(
encode_set(selection, &vec![0; 56_245]),
Err(Osc52Error::PayloadTooLarge { .. })
));
let seq = encode_set(selection, b"hello").unwrap();
let mut tmux = Vec::new();
crate::mux_passthrough::tmux_wrap(&mut tmux, &seq).unwrap();
assert_eq!(tmux, b"\x1bPtmux;\x1b\x1b]52;c;aGVsbG8=\x07\x1b\\");
let mut screen = Vec::new();
crate::mux_passthrough::screen_wrap(&mut screen, &seq).unwrap();
assert_eq!(screen, b"\x1bP\x1b]52;c;aGVsbG8=\x07\x1b\\");
}
}