1use crossterm::cursor::{Hide, Show};
4use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
5use crossterm::execute;
6use crossterm::terminal::{
7 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
8};
9use std::io;
10use std::panic;
11
12pub fn init_panic_hook() {
15 let original_hook = panic::take_hook();
16 panic::set_hook(Box::new(move |panic_info| {
17 let _ = disable_raw_mode();
18 let _ = execute!(
19 io::stdout(),
20 LeaveAlternateScreen,
21 DisableMouseCapture,
22 Show
23 );
24 original_hook(panic_info);
25 }));
26}
27
28pub struct TerminalGuard;
30
31impl TerminalGuard {
32 pub fn new() -> io::Result<Self> {
34 enable_raw_mode()?;
35 execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, Hide)?;
36 Ok(TerminalGuard)
37 }
38}
39
40impl Drop for TerminalGuard {
41 fn drop(&mut self) {
42 let _ = disable_raw_mode();
43 let _ = execute!(
44 io::stdout(),
45 LeaveAlternateScreen,
46 DisableMouseCapture,
47 Show
48 );
49 }
50}
51
52#[derive(Debug, Clone)]
61pub struct MouseCaptureStack {
62 stack: Vec<bool>,
63}
64
65impl MouseCaptureStack {
66 pub fn new() -> Self {
68 Self { stack: Vec::new() }
69 }
70
71 pub fn is_enabled(&self) -> bool {
74 self.stack.last().copied().unwrap_or(true)
75 }
76
77 pub fn push(&mut self, enabled: bool) -> bool {
80 let prev = self.is_enabled();
81 self.stack.push(enabled);
82 prev
83 }
84
85 pub fn pop(&mut self) -> bool {
88 self.stack.pop();
89 self.is_enabled()
90 }
91
92 pub fn reset(&mut self) {
94 self.stack.clear();
95 }
96}
97
98impl Default for MouseCaptureStack {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum ColorDepth {
111 NoColor,
113 Standard,
115 EightBit,
117 TrueColor,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
126pub enum RenderingQuality {
127 Ascii,
129 Basic,
131 Standard,
133 High,
135}
136
137#[derive(Debug, Clone)]
143pub struct TerminalCaps {
144 pub color_depth: ColorDepth,
146 pub unicode: bool,
148 pub mouse: bool,
150 pub title: bool,
152 pub kitty_graphics: bool,
154 pub sixel: bool,
156 pub iterm_images: bool,
158 pub rendering_quality: RenderingQuality,
160}
161
162impl TerminalCaps {
163 pub fn detect() -> Self {
182 let color_depth = detect_color_depth();
183 let unicode = detect_unicode();
184 let title = detect_title_support();
185 let kitty_graphics = detect_kitty_graphics();
186 let sixel = detect_sixel();
187 let iterm_images = detect_iterm_images();
188 let rendering_quality = derive_rendering_quality(color_depth, unicode);
189
190 Self {
191 color_depth,
192 unicode,
193 mouse: true, title,
195 kitty_graphics,
196 sixel,
197 iterm_images,
198 rendering_quality,
199 }
200 }
201}
202
203pub fn detect_capabilities() -> TerminalCaps {
205 TerminalCaps::detect()
206}
207
208fn detect_color_depth() -> ColorDepth {
209 if let Ok(ct) = std::env::var("COLORTERM") {
211 let ct_lower = ct.to_lowercase();
212 if ct_lower.contains("truecolor") || ct_lower.contains("24bit") {
213 return ColorDepth::TrueColor;
214 }
215 }
216
217 if let Ok(term) = std::env::var("TERM") {
219 if term.contains("256color") {
220 return ColorDepth::EightBit;
221 }
222 if term == "dumb" {
223 return ColorDepth::NoColor;
224 }
225 }
226
227 #[cfg(target_os = "windows")]
229 {
230 if std::env::var("WT_SESSION").is_ok() {
232 return ColorDepth::TrueColor;
233 }
234 ColorDepth::EightBit
236 }
237
238 #[cfg(not(target_os = "windows"))]
240 ColorDepth::Standard
241}
242
243fn detect_unicode() -> bool {
244 for var_name in &["LC_ALL", "LANG", "LC_CTYPE"] {
246 if let Ok(val) = std::env::var(var_name) {
247 let val_upper = val.to_uppercase();
248 if val_upper.contains("UTF-8") || val_upper.contains("UTF8") {
249 return true;
250 }
251 }
252 }
253
254 #[cfg(target_os = "windows")]
256 {
257 true
258 }
259
260 #[cfg(not(target_os = "windows"))]
262 {
263 if let Ok(term) = std::env::var("TERM") {
264 if term.starts_with("xterm") || term.starts_with("rxvt") || term.contains("256color") {
265 return true;
266 }
267 }
268 false
269 }
270}
271
272fn detect_title_support() -> bool {
273 if let Ok(term) = std::env::var("TERM") {
274 if term == "dumb" || term == "linux" {
276 return false;
277 }
278 }
279 true
281}
282
283fn detect_kitty_graphics() -> bool {
286 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
287 if prog.eq_ignore_ascii_case("kitty") {
288 return true;
289 }
290 }
291 std::env::var("KITTY_WINDOW_ID").is_ok()
292}
293
294fn detect_sixel() -> bool {
297 if std::env::var("SIXEL_SUPPORT").is_ok() {
299 return true;
300 }
301 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
303 let prog_lower = prog.to_lowercase();
304 if prog_lower == "mlterm"
306 || prog_lower == "contour"
307 || prog_lower == "foot"
308 || prog_lower == "wezterm"
309 {
310 return true;
311 }
312 }
313 false
314}
315
316fn detect_iterm_images() -> bool {
319 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
320 if prog == "iTerm.app" {
321 return true;
322 }
323 }
324 if let Ok(lc) = std::env::var("LC_TERMINAL") {
325 if lc == "iTerm2" {
326 return true;
327 }
328 }
329 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
331 if prog.to_lowercase() == "wezterm" {
332 return true;
333 }
334 }
335 false
336}
337
338fn derive_rendering_quality(color_depth: ColorDepth, unicode: bool) -> RenderingQuality {
340 if !unicode {
341 return RenderingQuality::Ascii;
342 }
343 match color_depth {
344 ColorDepth::NoColor | ColorDepth::Standard => RenderingQuality::Basic,
345 ColorDepth::EightBit => RenderingQuality::Standard,
346 ColorDepth::TrueColor => RenderingQuality::High,
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn terminal_caps_detect_returns_valid_struct() {
356 let caps = TerminalCaps::detect();
357 assert!(caps.mouse, "mouse should always be true");
359 match caps.color_depth {
361 ColorDepth::NoColor
362 | ColorDepth::Standard
363 | ColorDepth::EightBit
364 | ColorDepth::TrueColor => {}
365 }
366 }
367
368 #[test]
369 fn terminal_caps_color_depth_equality() {
370 assert_eq!(ColorDepth::TrueColor, ColorDepth::TrueColor);
371 assert_ne!(ColorDepth::TrueColor, ColorDepth::EightBit);
372 assert_ne!(ColorDepth::Standard, ColorDepth::NoColor);
373 }
374
375 #[test]
376 fn terminal_detect_capabilities_convenience() {
377 let caps = detect_capabilities();
378 assert!(caps.mouse);
379 }
381
382 #[test]
383 fn terminal_caps_clone_and_debug() {
384 let caps = TerminalCaps::detect();
385 let cloned = caps.clone();
386 assert_eq!(caps.color_depth, cloned.color_depth);
387 assert_eq!(caps.unicode, cloned.unicode);
388 assert_eq!(caps.mouse, cloned.mouse);
389 assert_eq!(caps.title, cloned.title);
390 assert_eq!(caps.kitty_graphics, cloned.kitty_graphics);
391 assert_eq!(caps.sixel, cloned.sixel);
392 assert_eq!(caps.iterm_images, cloned.iterm_images);
393 assert_eq!(caps.rendering_quality, cloned.rendering_quality);
394 let _debug = format!("{:?}", caps);
396 }
397
398 #[test]
399 fn rendering_quality_ordering() {
400 assert!(RenderingQuality::Ascii < RenderingQuality::Basic);
401 assert!(RenderingQuality::Basic < RenderingQuality::Standard);
402 assert!(RenderingQuality::Standard < RenderingQuality::High);
403 }
404
405 #[test]
406 fn derive_quality_from_caps() {
407 assert_eq!(
408 derive_rendering_quality(ColorDepth::NoColor, false),
409 RenderingQuality::Ascii
410 );
411 assert_eq!(
412 derive_rendering_quality(ColorDepth::TrueColor, false),
413 RenderingQuality::Ascii
414 );
415 assert_eq!(
416 derive_rendering_quality(ColorDepth::NoColor, true),
417 RenderingQuality::Basic
418 );
419 assert_eq!(
420 derive_rendering_quality(ColorDepth::Standard, true),
421 RenderingQuality::Basic
422 );
423 assert_eq!(
424 derive_rendering_quality(ColorDepth::EightBit, true),
425 RenderingQuality::Standard
426 );
427 assert_eq!(
428 derive_rendering_quality(ColorDepth::TrueColor, true),
429 RenderingQuality::High
430 );
431 }
432
433 #[test]
434 fn terminal_color_depth_detection_windows() {
435 #[cfg(target_os = "windows")]
437 {
438 let caps = TerminalCaps::detect();
439 assert!(
440 caps.color_depth == ColorDepth::EightBit
441 || caps.color_depth == ColorDepth::TrueColor,
442 "Windows should detect at least 256 colors, got {:?}",
443 caps.color_depth
444 );
445 }
446 }
447
448 #[test]
449 fn terminal_unicode_detection_windows() {
450 #[cfg(target_os = "windows")]
451 {
452 let caps = TerminalCaps::detect();
453 assert!(caps.unicode, "Windows should detect Unicode support");
454 }
455 }
456}