1use std::io::{self, Read, Write};
10use std::time::Duration;
11
12#[derive(Debug, Clone, Default)]
14pub struct TermCapabilities {
15 pub truecolor: bool,
16 pub bracketed_paste: bool,
17 pub clipboard_osc52: bool,
18 pub cursor_shape: bool,
19 pub osc7_cwd: bool,
20 pub osc133_prompt: bool,
21 pub sixel_graphics: bool,
22 pub kitty_keyboard: bool,
23 pub synchronized_output: bool,
24 pub unicode_version: Option<String>,
25}
26
27const PROBE_TIMEOUT_MS: u64 = 500;
29
30pub fn query_terminal() -> TermCapabilities {
32 let mut caps = TermCapabilities::default();
33
34 #[cfg(unix)]
36 {
37 if unsafe { libc::isatty(1) } != 1 {
38 return caps;
39 }
40 }
41
42 if let Ok(response) = send_query("\x1b[c", PROBE_TIMEOUT_MS) {
44 parse_device_attributes(&response, &mut caps);
45 }
46
47 if let Ok(ct) = std::env::var("COLORTERM") {
49 if ct == "truecolor" || ct == "24bit" {
50 caps.truecolor = true;
51 }
52 }
53
54 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
56 match term_program.as_str() {
57 "iTerm.app" | "WezTerm" | "Alacritty" | "kitty" => {
58 caps.truecolor = true;
59 caps.bracketed_paste = true;
60 caps.osc7_cwd = true;
61 }
62 _ => {}
63 }
64 }
65
66 if std::env::var("KITTY_WINDOW_ID").is_ok() {
67 caps.kitty_keyboard = true;
68 caps.truecolor = true;
69 }
70
71 caps
72}
73
74fn send_query(query: &str, timeout_ms: u64) -> io::Result<String> {
76 #[cfg(unix)]
77 {
78 let mut old_termios: libc::termios = unsafe { std::mem::zeroed() };
80 let has_old = unsafe { libc::tcgetattr(0, &mut old_termios) } == 0;
81
82 if has_old {
83 let mut raw = old_termios;
84 raw.c_lflag &= !(libc::ICANON | libc::ECHO);
85 raw.c_cc[libc::VMIN] = 0;
86 raw.c_cc[libc::VTIME] = (timeout_ms / 100).min(255) as u8;
87 unsafe { libc::tcsetattr(0, libc::TCSANOW, &raw) };
88 }
89
90 let _ = io::stdout().write_all(query.as_bytes());
92 let _ = io::stdout().flush();
93
94 let mut response = Vec::new();
96 let mut buf = [0u8; 1];
97 let deadline = std::time::Instant::now() + Duration::from_millis(timeout_ms);
98
99 while std::time::Instant::now() < deadline {
100 match io::stdin().read(&mut buf) {
101 Ok(1) => {
102 response.push(buf[0]);
103 if buf[0] == b'c'
105 || buf[0] == b'n'
106 || buf[0] == b't'
107 || buf[0] == b'\\'
108 || buf[0] == 0x07
109 {
110 break;
111 }
112 }
113 Ok(0) => break,
114 _ => break,
115 }
116 }
117
118 if has_old {
120 unsafe { libc::tcsetattr(0, libc::TCSANOW, &old_termios) };
121 }
122
123 Ok(String::from_utf8_lossy(&response).to_string())
124 }
125
126 #[cfg(not(unix))]
127 {
128 let _ = (query, timeout_ms);
129 Ok(String::new())
130 }
131}
132
133fn parse_device_attributes(response: &str, caps: &mut TermCapabilities) {
135 if response.contains("?") {
141 let params: Vec<&str> = response
142 .trim_start_matches("\x1b[?")
143 .trim_end_matches('c')
144 .split(';')
145 .collect();
146
147 for param in params {
148 match param.trim() {
149 "4" => caps.sixel_graphics = true,
150 _ => {}
151 }
152 }
153 }
154}
155
156pub fn probe_bracketed_paste() -> bool {
158 if let Ok(term) = std::env::var("TERM") {
160 !term.starts_with("dumb") && !term.starts_with("cons")
161 } else {
162 false
163 }
164}
165
166pub fn enable_bracketed_paste() -> String {
168 "\x1b[?2004h".to_string()
169}
170
171pub fn disable_bracketed_paste() -> String {
172 "\x1b[?2004l".to_string()
173}
174
175pub fn url_encode(s: &str) -> String {
177 let mut result = String::with_capacity(s.len() * 3);
178 for b in s.bytes() {
179 match b {
180 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
181 result.push(b as char);
182 }
183 _ => {
184 result.push_str(&format!("%{:02X}", b));
185 }
186 }
187 }
188 result
189}
190
191pub fn system_clipget() -> Option<String> {
193 None }
197
198pub fn system_clipput(data: &str) -> String {
200 use std::io::Write;
201 let mut buf = Vec::new();
202 {
203 let encoder = base64_encode(data.as_bytes());
204 buf.extend_from_slice(b"\x1b]52;c;");
205 buf.extend_from_slice(encoder.as_bytes());
206 buf.extend_from_slice(b"\x1b\\");
207 }
208 String::from_utf8_lossy(&buf).to_string()
209}
210
211fn base64_encode(data: &[u8]) -> String {
212 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
213 let mut result = String::with_capacity((data.len() + 2) / 3 * 4);
214 for chunk in data.chunks(3) {
215 let b0 = chunk[0] as u32;
216 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
217 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
218 let n = (b0 << 16) | (b1 << 8) | b2;
219 result.push(CHARS[((n >> 18) & 63) as usize] as char);
220 result.push(CHARS[((n >> 12) & 63) as usize] as char);
221 if chunk.len() > 1 {
222 result.push(CHARS[((n >> 6) & 63) as usize] as char);
223 } else {
224 result.push('=');
225 }
226 if chunk.len() > 2 {
227 result.push(CHARS[(n & 63) as usize] as char);
228 } else {
229 result.push('=');
230 }
231 }
232 result
233}
234
235pub fn extension_enabled(name: &str) -> bool {
237 match name {
238 "bracketed-paste" => probe_bracketed_paste(),
239 "truecolor" => std::env::var("COLORTERM")
240 .map(|v| v == "truecolor" || v == "24bit")
241 .unwrap_or(false),
242 "osc7" | "osc133" => std::env::var("TERM_PROGRAM")
243 .map(|v| matches!(v.as_str(), "iTerm.app" | "WezTerm" | "kitty"))
244 .unwrap_or(false),
245 _ => false,
246 }
247}
248
249pub fn set_cursor_shape(shape: CursorShape) -> String {
251 match shape {
252 CursorShape::Block => "\x1b[2 q".to_string(),
253 CursorShape::Underline => "\x1b[4 q".to_string(),
254 CursorShape::Bar => "\x1b[6 q".to_string(),
255 CursorShape::BlinkingBlock => "\x1b[1 q".to_string(),
256 CursorShape::BlinkingUnderline => "\x1b[3 q".to_string(),
257 CursorShape::BlinkingBar => "\x1b[5 q".to_string(),
258 CursorShape::Default => "\x1b[0 q".to_string(),
259 }
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum CursorShape {
264 Default,
265 BlinkingBlock,
266 Block,
267 BlinkingUnderline,
268 Underline,
269 BlinkingBar,
270 Bar,
271}
272
273pub fn notify_pwd(path: &str) -> String {
275 let hostname = crate::utils::gethostname();
277 format!("\x1b]7;file://{}{}\x1b\\", hostname, url_encode(path))
278}
279
280pub fn prompt_marker_start() -> &'static str {
282 "\x1b]133;A\x1b\\" }
284
285pub fn prompt_marker_end() -> &'static str {
286 "\x1b]133;B\x1b\\" }
288
289pub fn output_marker_start() -> &'static str {
290 "\x1b]133;C\x1b\\" }
292
293pub fn output_marker_end(exit_code: i32) -> String {
294 format!("\x1b]133;D;{}\x1b\\", exit_code) }
296
297pub fn sync_output_start() -> &'static str {
299 "\x1b[?2026h"
300}
301
302pub fn sync_output_end() -> &'static str {
303 "\x1b[?2026l"
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_url_encode() {
312 assert_eq!(url_encode("/home/user"), "/home/user");
313 assert_eq!(url_encode("/path with spaces"), "/path%20with%20spaces");
314 assert_eq!(url_encode("hello&world"), "hello%26world");
315 }
316
317 #[test]
318 fn test_cursor_shape() {
319 assert_eq!(set_cursor_shape(CursorShape::Bar), "\x1b[6 q");
320 assert_eq!(set_cursor_shape(CursorShape::Block), "\x1b[2 q");
321 }
322
323 #[test]
324 fn test_bracketed_paste() {
325 assert_eq!(enable_bracketed_paste(), "\x1b[?2004h");
326 assert_eq!(disable_bracketed_paste(), "\x1b[?2004l");
327 }
328
329 #[test]
330 fn test_base64_encode() {
331 assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
332 assert_eq!(base64_encode(b""), "");
333 assert_eq!(base64_encode(b"a"), "YQ==");
334 }
335
336 #[test]
337 fn test_prompt_markers() {
338 assert!(prompt_marker_start().contains("133;A"));
339 assert!(prompt_marker_end().contains("133;B"));
340 }
341}