1use std::fs::File;
16use std::io::{self, Write};
17
18fn osc52_sequence(text: &str) -> String {
20 use std::fmt::Write as FmtWrite;
21 let bytes = text.as_bytes();
22 let mut encoded = String::with_capacity((bytes.len() * 4 / 3) + 8);
23 const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
24 let mut i = 0;
25 while i + 3 <= bytes.len() {
26 let b0 = bytes[i] as usize;
27 let b1 = bytes[i + 1] as usize;
28 let b2 = bytes[i + 2] as usize;
29 let _ = FmtWrite::write_char(&mut encoded, TABLE[b0 >> 2] as char);
30 let _ = FmtWrite::write_char(&mut encoded, TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
31 let _ = FmtWrite::write_char(&mut encoded, TABLE[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
32 let _ = FmtWrite::write_char(&mut encoded, TABLE[b2 & 0x3f] as char);
33 i += 3;
34 }
35 let rem = bytes.len() - i;
36 if rem == 1 {
37 let b0 = bytes[i] as usize;
38 let _ = FmtWrite::write_char(&mut encoded, TABLE[b0 >> 2] as char);
39 let _ = FmtWrite::write_char(&mut encoded, TABLE[(b0 & 3) << 4] as char);
40 encoded.push_str("==");
41 } else if rem == 2 {
42 let b0 = bytes[i] as usize;
43 let b1 = bytes[i + 1] as usize;
44 let _ = FmtWrite::write_char(&mut encoded, TABLE[b0 >> 2] as char);
45 let _ = FmtWrite::write_char(&mut encoded, TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char);
46 let _ = FmtWrite::write_char(&mut encoded, TABLE[(b1 & 0xf) << 2] as char);
47 encoded.push('=');
48 }
49 format!("\x1b]52;c;{}\x07", encoded)
50}
51
52pub fn osc52_write_via(
63 text: &str,
64 open_tty: fn() -> io::Result<File>,
65 stdout_fallback: &mut dyn Write,
66) -> bool {
67 let osc = osc52_sequence(text);
68 if let Ok(mut tty) = open_tty()
69 && tty.write_all(osc.as_bytes()).is_ok()
70 && tty.flush().is_ok()
71 {
72 return true;
73 }
74 stdout_fallback.write_all(osc.as_bytes()).is_ok() && stdout_fallback.flush().is_ok()
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn osc52_sequence_encodes_all_remainder_lengths() {
85 assert_eq!(osc52_sequence("foo"), "\x1b]52;c;Zm9v\x07"); assert_eq!(osc52_sequence("f"), "\x1b]52;c;Zg==\x07"); assert_eq!(osc52_sequence("fo"), "\x1b]52;c;Zm8=\x07"); assert!(osc52_sequence("").starts_with("\x1b]52;c;"));
89 }
90
91 fn open_fails() -> io::Result<File> {
92 Err(io::Error::other("no tty"))
93 }
94
95 fn open_temp_writable() -> io::Result<File> {
101 std::fs::OpenOptions::new()
102 .create(true)
103 .write(true)
104 .truncate(true)
105 .open(std::env::temp_dir().join("lev_sys_osc52_fake_tty_w"))
106 }
107
108 fn open_temp_readonly() -> io::Result<File> {
109 let path = std::env::temp_dir().join("lev_sys_osc52_fake_tty_ro");
110 let _ = std::fs::write(&path, b"");
111 std::fs::OpenOptions::new().read(true).open(&path)
112 }
113
114 struct WriteFailWriter;
116 impl Write for WriteFailWriter {
117 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
118 Err(io::Error::other("write fail"))
119 }
120 fn flush(&mut self) -> io::Result<()> {
121 Ok(())
122 }
123 }
124
125 struct FlushFailWriter {
127 buf: Vec<u8>,
128 }
129 impl Write for FlushFailWriter {
130 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
131 self.buf.extend_from_slice(data);
132 Ok(data.len())
133 }
134 fn flush(&mut self) -> io::Result<()> {
135 Err(io::Error::other("flush fail"))
136 }
137 }
138
139 #[test]
140 fn write_via_returns_true_when_tty_write_succeeds() {
141 let mut sink = Vec::new();
144 assert!(osc52_write_via("hi", open_temp_writable, &mut sink));
145 assert!(
146 sink.is_empty(),
147 "fallback must not run when the tty write succeeds"
148 );
149 }
150
151 #[test]
152 fn write_via_falls_back_when_tty_opens_but_write_fails() {
153 let mut sink = Vec::new();
156 assert!(osc52_write_via("hi", open_temp_readonly, &mut sink));
157 assert!(
158 !sink.is_empty(),
159 "fallback should have written the sequence"
160 );
161 }
162
163 #[test]
164 fn write_via_falls_back_when_tty_open_fails() {
165 let mut sink = Vec::new();
166 assert!(osc52_write_via("hi", open_fails, &mut sink));
167 assert!(!sink.is_empty());
168 }
169
170 #[test]
171 fn write_via_returns_false_when_fallback_write_fails() {
172 let mut sink = WriteFailWriter;
173 assert!(!osc52_write_via("hi", open_fails, &mut sink));
174 assert!(sink.flush().is_ok());
178 }
179
180 #[test]
181 fn write_via_returns_false_when_fallback_flush_fails() {
182 let mut sink = FlushFailWriter { buf: Vec::new() };
185 assert!(!osc52_write_via("hi", open_fails, &mut sink));
186 assert!(
187 !sink.buf.is_empty(),
188 "write should have run before flush failed"
189 );
190 }
191}