use std::io::{self, Write};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
pub const MAX_TEXT_BYTES: usize = 74_994;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Osc52Mode {
#[default]
Auto,
Always,
Off,
}
pub fn should_emit(mode: Osc52Mode, remote: bool, local_failed: bool) -> bool {
match mode {
Osc52Mode::Off => false,
Osc52Mode::Always => true,
Osc52Mode::Auto => remote || local_failed,
}
}
pub fn session_looks_remote() -> bool {
std::env::var_os("SSH_TTY").is_some() || std::env::var_os("SSH_CONNECTION").is_some()
}
pub fn in_tmux() -> bool {
std::env::var_os("TMUX").is_some()
}
pub fn sequence(text: &str, tmux: bool) -> Option<String> {
if text.len() > MAX_TEXT_BYTES {
return None;
}
let inner = format!("\x1b]52;c;{}\x07", BASE64.encode(text));
Some(if tmux {
format!("\x1bPtmux;{}\x1b\\", inner.replace('\x1b', "\x1b\x1b"))
} else {
inner
})
}
pub fn write_to<W: Write>(out: &mut W, text: &str, tmux: bool) -> io::Result<bool> {
let Some(seq) = sequence(text, tmux) else {
return Ok(false);
};
out.write_all(seq.as_bytes())?;
out.flush()?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_sequence_is_the_documented_bytes() {
assert_eq!(
sequence("hi", false).unwrap(),
"\x1b]52;c;aGk=\x07",
"ASCII"
);
assert_eq!(
sequence("да", false).unwrap(),
format!("\x1b]52;c;{}\x07", BASE64.encode("да".as_bytes()))
);
assert_eq!(sequence("", false).unwrap(), "\x1b]52;c;\x07");
}
#[test]
fn tmux_wraps_and_doubles_the_escapes() {
let wrapped = sequence("hi", true).unwrap();
assert_eq!(wrapped, "\x1bPtmux;\x1b\x1b]52;c;aGk=\x07\x1b\\");
assert!(wrapped.starts_with("\x1bPtmux;"));
assert!(wrapped.ends_with("\x1b\\"));
assert!(wrapped.contains("aGk="));
assert_ne!(wrapped, sequence("hi", false).unwrap());
}
#[test]
fn the_ceiling_is_exact() {
let fits = "x".repeat(MAX_TEXT_BYTES);
assert!(
sequence(&fits, false).is_some(),
"the largest text that fits"
);
let over = "x".repeat(MAX_TEXT_BYTES + 1);
assert!(sequence(&over, false).is_none(), "one byte more");
let cyrillic = "я".repeat(MAX_TEXT_BYTES / 2 + 1);
assert!(
sequence(&cyrillic, false).is_none(),
"{} characters is {} bytes",
cyrillic.chars().count(),
cyrillic.len()
);
}
#[test]
fn write_to_reports_and_stays_silent_when_it_refuses() {
let mut sink = Vec::new();
assert!(write_to(&mut sink, "hi", false).unwrap());
assert_eq!(sink, b"\x1b]52;c;aGk=\x07");
let mut sink = Vec::new();
let over = "x".repeat(MAX_TEXT_BYTES + 1);
assert!(!write_to(&mut sink, &over, false).unwrap());
assert!(sink.is_empty(), "a refused copy must write no bytes");
}
#[test]
fn the_decision_matrix() {
use Osc52Mode::*;
for (mode, remote, failed, expected) in [
(Auto, false, false, false),
(Auto, true, false, true),
(Auto, false, true, true),
(Auto, true, true, true),
(Always, false, false, true),
(Always, true, true, true),
(Off, true, true, false),
(Off, false, false, false),
] {
assert_eq!(
should_emit(mode, remote, failed),
expected,
"{mode:?} remote={remote} local_failed={failed}"
);
}
}
#[test]
fn the_default_mode_is_auto() {
assert_eq!(Osc52Mode::default(), Osc52Mode::Auto);
assert!(!should_emit(Osc52Mode::default(), false, false));
}
}