1#![forbid(unsafe_code)]
2
3use std::env;
117use std::str::FromStr;
118
119#[derive(Debug, Clone)]
120struct DetectInputs {
121 no_color: bool,
122 term: String,
123 term_program: String,
124 colorterm: String,
125 in_tmux: bool,
126 in_screen: bool,
127 in_zellij: bool,
128 kitty_window_id: bool,
129 wt_session: bool,
130}
131
132impl DetectInputs {
133 fn from_env() -> Self {
134 Self {
135 no_color: env::var("NO_COLOR").is_ok(),
136 term: env::var("TERM").unwrap_or_default(),
137 term_program: env::var("TERM_PROGRAM").unwrap_or_default(),
138 colorterm: env::var("COLORTERM").unwrap_or_default(),
139 in_tmux: env::var("TMUX").is_ok(),
140 in_screen: env::var("STY").is_ok(),
141 in_zellij: env::var("ZELLIJ").is_ok(),
142 kitty_window_id: env::var("KITTY_WINDOW_ID").is_ok(),
143 wt_session: env::var("WT_SESSION").is_ok(),
144 }
145 }
146}
147
148const MODERN_TERMINALS: &[&str] = &[
150 "iTerm.app",
151 "WezTerm",
152 "Alacritty",
153 "Ghostty",
154 "kitty",
155 "Rio",
156 "Hyper",
157 "Contour",
158 "vscode",
159];
160
161const KITTY_KEYBOARD_TERMINALS: &[&str] = &[
163 "iTerm.app",
164 "WezTerm",
165 "Alacritty",
166 "Ghostty",
167 "Rio",
168 "kitty",
169 "foot",
170];
171
172const SYNC_OUTPUT_TERMINALS: &[&str] = &["WezTerm", "Alacritty", "Ghostty", "kitty", "Contour"];
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum TerminalProfile {
180 Modern,
182 Xterm256Color,
184 Xterm,
186 Vt100,
188 Dumb,
190 Screen,
192 Tmux,
194 Zellij,
196 WindowsConsole,
198 Kitty,
200 LinuxConsole,
202 Custom,
204 Detected,
206}
207
208impl TerminalProfile {
209 #[must_use]
211 pub const fn as_str(&self) -> &'static str {
212 match self {
213 Self::Modern => "modern",
214 Self::Xterm256Color => "xterm-256color",
215 Self::Xterm => "xterm",
216 Self::Vt100 => "vt100",
217 Self::Dumb => "dumb",
218 Self::Screen => "screen",
219 Self::Tmux => "tmux",
220 Self::Zellij => "zellij",
221 Self::WindowsConsole => "windows-console",
222 Self::Kitty => "kitty",
223 Self::LinuxConsole => "linux",
224 Self::Custom => "custom",
225 Self::Detected => "detected",
226 }
227 }
228
229 #[must_use]
231 pub const fn all_predefined() -> &'static [Self] {
232 &[
233 Self::Modern,
234 Self::Xterm256Color,
235 Self::Xterm,
236 Self::Vt100,
237 Self::Dumb,
238 Self::Screen,
239 Self::Tmux,
240 Self::Zellij,
241 Self::WindowsConsole,
242 Self::Kitty,
243 Self::LinuxConsole,
244 ]
245 }
246}
247
248impl std::str::FromStr for TerminalProfile {
249 type Err = ();
250
251 fn from_str(s: &str) -> Result<Self, Self::Err> {
252 match s.to_lowercase().as_str() {
253 "modern" => Ok(Self::Modern),
254 "xterm-256color" | "xterm256color" | "xterm-256" => Ok(Self::Xterm256Color),
255 "xterm" => Ok(Self::Xterm),
256 "vt100" => Ok(Self::Vt100),
257 "dumb" => Ok(Self::Dumb),
258 "screen" | "screen-256color" => Ok(Self::Screen),
259 "tmux" | "tmux-256color" => Ok(Self::Tmux),
260 "zellij" => Ok(Self::Zellij),
261 "windows-console" | "windows" | "conhost" => Ok(Self::WindowsConsole),
262 "kitty" | "xterm-kitty" => Ok(Self::Kitty),
263 "linux" | "linux-console" => Ok(Self::LinuxConsole),
264 "custom" => Ok(Self::Custom),
265 "detected" | "auto" => Ok(Self::Detected),
266 _ => Err(()),
267 }
268 }
269}
270
271impl std::fmt::Display for TerminalProfile {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 write!(f, "{}", self.as_str())
274 }
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub struct TerminalCapabilities {
295 profile: TerminalProfile,
297
298 pub true_color: bool,
301 pub colors_256: bool,
303
304 pub unicode_box_drawing: bool,
307 pub unicode_emoji: bool,
309 pub double_width: bool,
311
312 pub sync_output: bool,
315 pub osc8_hyperlinks: bool,
317 pub scroll_region: bool,
319
320 pub in_tmux: bool,
323 pub in_screen: bool,
325 pub in_zellij: bool,
327
328 pub kitty_keyboard: bool,
331 pub focus_events: bool,
333 pub bracketed_paste: bool,
335 pub mouse_sgr: bool,
337
338 pub osc52_clipboard: bool,
341}
342
343impl Default for TerminalCapabilities {
344 fn default() -> Self {
345 Self::basic()
346 }
347}
348
349impl TerminalCapabilities {
354 #[must_use]
358 pub const fn profile(&self) -> TerminalProfile {
359 self.profile
360 }
361
362 #[must_use]
367 pub fn profile_name(&self) -> Option<&'static str> {
368 match self.profile {
369 TerminalProfile::Detected => None,
370 p => Some(p.as_str()),
371 }
372 }
373
374 #[must_use]
376 pub fn from_profile(profile: TerminalProfile) -> Self {
377 match profile {
378 TerminalProfile::Modern => Self::modern(),
379 TerminalProfile::Xterm256Color => Self::xterm_256color(),
380 TerminalProfile::Xterm => Self::xterm(),
381 TerminalProfile::Vt100 => Self::vt100(),
382 TerminalProfile::Dumb => Self::dumb(),
383 TerminalProfile::Screen => Self::screen(),
384 TerminalProfile::Tmux => Self::tmux(),
385 TerminalProfile::Zellij => Self::zellij(),
386 TerminalProfile::WindowsConsole => Self::windows_console(),
387 TerminalProfile::Kitty => Self::kitty(),
388 TerminalProfile::LinuxConsole => Self::linux_console(),
389 TerminalProfile::Custom => Self::basic(),
390 TerminalProfile::Detected => Self::detect(),
391 }
392 }
393
394 #[must_use]
401 pub const fn modern() -> Self {
402 Self {
403 profile: TerminalProfile::Modern,
404 true_color: true,
405 colors_256: true,
406 unicode_box_drawing: true,
407 unicode_emoji: true,
408 double_width: true,
409 sync_output: true,
410 osc8_hyperlinks: true,
411 scroll_region: true,
412 in_tmux: false,
413 in_screen: false,
414 in_zellij: false,
415 kitty_keyboard: true,
416 focus_events: true,
417 bracketed_paste: true,
418 mouse_sgr: true,
419 osc52_clipboard: true,
420 }
421 }
422
423 #[must_use]
428 pub const fn xterm_256color() -> Self {
429 Self {
430 profile: TerminalProfile::Xterm256Color,
431 true_color: false,
432 colors_256: true,
433 unicode_box_drawing: true,
434 unicode_emoji: true,
435 double_width: true,
436 sync_output: false,
437 osc8_hyperlinks: false,
438 scroll_region: true,
439 in_tmux: false,
440 in_screen: false,
441 in_zellij: false,
442 kitty_keyboard: false,
443 focus_events: false,
444 bracketed_paste: true,
445 mouse_sgr: true,
446 osc52_clipboard: false,
447 }
448 }
449
450 #[must_use]
454 pub const fn xterm() -> Self {
455 Self {
456 profile: TerminalProfile::Xterm,
457 true_color: false,
458 colors_256: false,
459 unicode_box_drawing: true,
460 unicode_emoji: false,
461 double_width: true,
462 sync_output: false,
463 osc8_hyperlinks: false,
464 scroll_region: true,
465 in_tmux: false,
466 in_screen: false,
467 in_zellij: false,
468 kitty_keyboard: false,
469 focus_events: false,
470 bracketed_paste: true,
471 mouse_sgr: true,
472 osc52_clipboard: false,
473 }
474 }
475
476 #[must_use]
480 pub const fn vt100() -> Self {
481 Self {
482 profile: TerminalProfile::Vt100,
483 true_color: false,
484 colors_256: false,
485 unicode_box_drawing: false,
486 unicode_emoji: false,
487 double_width: false,
488 sync_output: false,
489 osc8_hyperlinks: false,
490 scroll_region: true,
491 in_tmux: false,
492 in_screen: false,
493 in_zellij: false,
494 kitty_keyboard: false,
495 focus_events: false,
496 bracketed_paste: false,
497 mouse_sgr: false,
498 osc52_clipboard: false,
499 }
500 }
501
502 #[must_use]
506 pub const fn dumb() -> Self {
507 Self {
508 profile: TerminalProfile::Dumb,
509 true_color: false,
510 colors_256: false,
511 unicode_box_drawing: false,
512 unicode_emoji: false,
513 double_width: false,
514 sync_output: false,
515 osc8_hyperlinks: false,
516 scroll_region: false,
517 in_tmux: false,
518 in_screen: false,
519 in_zellij: false,
520 kitty_keyboard: false,
521 focus_events: false,
522 bracketed_paste: false,
523 mouse_sgr: false,
524 osc52_clipboard: false,
525 }
526 }
527
528 #[must_use]
533 pub const fn screen() -> Self {
534 Self {
535 profile: TerminalProfile::Screen,
536 true_color: false,
537 colors_256: true,
538 unicode_box_drawing: true,
539 unicode_emoji: true,
540 double_width: true,
541 sync_output: false,
542 osc8_hyperlinks: false,
543 scroll_region: true,
544 in_tmux: false,
545 in_screen: true,
546 in_zellij: false,
547 kitty_keyboard: false,
548 focus_events: false,
549 bracketed_paste: true,
550 mouse_sgr: true,
551 osc52_clipboard: false,
552 }
553 }
554
555 #[must_use]
560 pub const fn tmux() -> Self {
561 Self {
562 profile: TerminalProfile::Tmux,
563 true_color: false,
564 colors_256: true,
565 unicode_box_drawing: true,
566 unicode_emoji: true,
567 double_width: true,
568 sync_output: false,
569 osc8_hyperlinks: false,
570 scroll_region: true,
571 in_tmux: true,
572 in_screen: false,
573 in_zellij: false,
574 kitty_keyboard: false,
575 focus_events: false,
576 bracketed_paste: true,
577 mouse_sgr: true,
578 osc52_clipboard: false,
579 }
580 }
581
582 #[must_use]
586 pub const fn zellij() -> Self {
587 Self {
588 profile: TerminalProfile::Zellij,
589 true_color: true,
590 colors_256: true,
591 unicode_box_drawing: true,
592 unicode_emoji: true,
593 double_width: true,
594 sync_output: false,
595 osc8_hyperlinks: false,
596 scroll_region: true,
597 in_tmux: false,
598 in_screen: false,
599 in_zellij: true,
600 kitty_keyboard: false,
601 focus_events: true,
602 bracketed_paste: true,
603 mouse_sgr: true,
604 osc52_clipboard: false,
605 }
606 }
607
608 #[must_use]
612 pub const fn windows_console() -> Self {
613 Self {
614 profile: TerminalProfile::WindowsConsole,
615 true_color: true,
616 colors_256: true,
617 unicode_box_drawing: true,
618 unicode_emoji: true,
619 double_width: true,
620 sync_output: false,
621 osc8_hyperlinks: true,
622 scroll_region: true,
623 in_tmux: false,
624 in_screen: false,
625 in_zellij: false,
626 kitty_keyboard: false,
627 focus_events: true,
628 bracketed_paste: true,
629 mouse_sgr: true,
630 osc52_clipboard: true,
631 }
632 }
633
634 #[must_use]
638 pub const fn kitty() -> Self {
639 Self {
640 profile: TerminalProfile::Kitty,
641 true_color: true,
642 colors_256: true,
643 unicode_box_drawing: true,
644 unicode_emoji: true,
645 double_width: true,
646 sync_output: true,
647 osc8_hyperlinks: true,
648 scroll_region: true,
649 in_tmux: false,
650 in_screen: false,
651 in_zellij: false,
652 kitty_keyboard: true,
653 focus_events: true,
654 bracketed_paste: true,
655 mouse_sgr: true,
656 osc52_clipboard: true,
657 }
658 }
659
660 #[must_use]
664 pub const fn linux_console() -> Self {
665 Self {
666 profile: TerminalProfile::LinuxConsole,
667 true_color: false,
668 colors_256: false,
669 unicode_box_drawing: true,
670 unicode_emoji: false,
671 double_width: false,
672 sync_output: false,
673 osc8_hyperlinks: false,
674 scroll_region: true,
675 in_tmux: false,
676 in_screen: false,
677 in_zellij: false,
678 kitty_keyboard: false,
679 focus_events: false,
680 bracketed_paste: true,
681 mouse_sgr: true,
682 osc52_clipboard: false,
683 }
684 }
685
686 pub fn builder() -> CapabilityProfileBuilder {
690 CapabilityProfileBuilder::new()
691 }
692}
693
694#[derive(Debug, Clone)]
719#[must_use]
720pub struct CapabilityProfileBuilder {
721 caps: TerminalCapabilities,
722}
723
724impl Default for CapabilityProfileBuilder {
725 fn default() -> Self {
726 Self::new()
727 }
728}
729
730impl CapabilityProfileBuilder {
731 pub fn new() -> Self {
733 Self {
734 caps: TerminalCapabilities {
735 profile: TerminalProfile::Custom,
736 true_color: false,
737 colors_256: false,
738 unicode_box_drawing: false,
739 unicode_emoji: false,
740 double_width: false,
741 sync_output: false,
742 osc8_hyperlinks: false,
743 scroll_region: false,
744 in_tmux: false,
745 in_screen: false,
746 in_zellij: false,
747 kitty_keyboard: false,
748 focus_events: false,
749 bracketed_paste: false,
750 mouse_sgr: false,
751 osc52_clipboard: false,
752 },
753 }
754 }
755
756 pub fn from_profile(profile: TerminalProfile) -> Self {
758 let mut caps = TerminalCapabilities::from_profile(profile);
759 caps.profile = TerminalProfile::Custom;
760 Self { caps }
761 }
762
763 #[must_use]
765 pub fn build(self) -> TerminalCapabilities {
766 self.caps
767 }
768
769 pub const fn true_color(mut self, enabled: bool) -> Self {
773 self.caps.true_color = enabled;
774 self
775 }
776
777 pub const fn colors_256(mut self, enabled: bool) -> Self {
779 self.caps.colors_256 = enabled;
780 self
781 }
782
783 pub const fn sync_output(mut self, enabled: bool) -> Self {
787 self.caps.sync_output = enabled;
788 self
789 }
790
791 pub const fn osc8_hyperlinks(mut self, enabled: bool) -> Self {
793 self.caps.osc8_hyperlinks = enabled;
794 self
795 }
796
797 pub const fn scroll_region(mut self, enabled: bool) -> Self {
799 self.caps.scroll_region = enabled;
800 self
801 }
802
803 pub const fn in_tmux(mut self, enabled: bool) -> Self {
807 self.caps.in_tmux = enabled;
808 self
809 }
810
811 pub const fn in_screen(mut self, enabled: bool) -> Self {
813 self.caps.in_screen = enabled;
814 self
815 }
816
817 pub const fn in_zellij(mut self, enabled: bool) -> Self {
819 self.caps.in_zellij = enabled;
820 self
821 }
822
823 pub const fn kitty_keyboard(mut self, enabled: bool) -> Self {
827 self.caps.kitty_keyboard = enabled;
828 self
829 }
830
831 pub const fn focus_events(mut self, enabled: bool) -> Self {
833 self.caps.focus_events = enabled;
834 self
835 }
836
837 pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
839 self.caps.bracketed_paste = enabled;
840 self
841 }
842
843 pub const fn mouse_sgr(mut self, enabled: bool) -> Self {
845 self.caps.mouse_sgr = enabled;
846 self
847 }
848
849 pub const fn osc52_clipboard(mut self, enabled: bool) -> Self {
853 self.caps.osc52_clipboard = enabled;
854 self
855 }
856}
857
858impl TerminalCapabilities {
859 #[must_use]
865 pub fn detect() -> Self {
866 let value = env::var("FTUI_TEST_PROFILE").ok();
867 Self::detect_with_test_profile_override(value.as_deref())
868 }
869
870 fn detect_with_test_profile_override(value: Option<&str>) -> Self {
871 if let Some(value) = value
872 && let Ok(profile) = TerminalProfile::from_str(value.trim())
873 && profile != TerminalProfile::Detected
874 {
875 return Self::from_profile(profile);
876 }
877 let env = DetectInputs::from_env();
878 Self::detect_from_inputs(&env)
879 }
880
881 fn detect_from_inputs(env: &DetectInputs) -> Self {
882 let in_tmux = env.in_tmux;
884 let in_screen = env.in_screen;
885 let in_zellij = env.in_zellij;
886 let in_any_mux = in_tmux || in_screen || in_zellij;
887
888 let term = env.term.as_str();
889 let term_program = env.term_program.as_str();
890 let colorterm = env.colorterm.as_str();
891 let term_lower = term.to_ascii_lowercase();
892 let term_program_lower = term_program.to_ascii_lowercase();
893 let colorterm_lower = colorterm.to_ascii_lowercase();
894
895 let is_windows_terminal = env.wt_session;
897
898 let is_dumb = term == "dumb" || (term.is_empty() && !is_windows_terminal);
903
904 let is_kitty = env.kitty_window_id || term_lower.contains("kitty");
906
907 let is_modern_terminal = MODERN_TERMINALS.iter().any(|t| {
909 let t_lower = t.to_ascii_lowercase();
910 term_program_lower.contains(&t_lower) || term_lower.contains(&t_lower)
911 }) || is_windows_terminal;
912
913 let true_color = !env.no_color
915 && !is_dumb
916 && (colorterm_lower.contains("truecolor")
917 || colorterm_lower.contains("24bit")
918 || is_modern_terminal
919 || is_kitty);
920
921 let colors_256 = !env.no_color
923 && !is_dumb
924 && (true_color || term_lower.contains("256color") || term_lower.contains("256"));
925
926 let sync_output = !is_dumb
928 && (is_kitty
929 || SYNC_OUTPUT_TERMINALS.iter().any(|t| {
930 let t_lower = t.to_ascii_lowercase();
931 term_program_lower.contains(&t_lower)
932 }));
933
934 let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
936
937 let scroll_region = !is_dumb;
939
940 let kitty_keyboard = is_kitty
942 || KITTY_KEYBOARD_TERMINALS.iter().any(|t| {
943 let t_lower = t.to_ascii_lowercase();
944 term_program_lower.contains(&t_lower) || term_lower.contains(&t_lower)
945 });
946
947 let focus_events = !is_dumb && (is_modern_terminal || is_kitty);
949
950 let bracketed_paste = !is_dumb;
952
953 let mouse_sgr = !is_dumb;
955
956 let osc52_clipboard = !is_dumb && !in_any_mux && (is_modern_terminal || is_kitty);
958
959 let unicode_box_drawing = !is_dumb;
961 let unicode_emoji = !is_dumb && (is_modern_terminal || is_kitty);
962 let double_width = !is_dumb;
963
964 Self {
965 profile: TerminalProfile::Detected,
966 true_color,
967 colors_256,
968 unicode_box_drawing,
969 unicode_emoji,
970 double_width,
971 sync_output,
972 osc8_hyperlinks,
973 scroll_region,
974 in_tmux,
975 in_screen,
976 in_zellij,
977 kitty_keyboard,
978 focus_events,
979 bracketed_paste,
980 mouse_sgr,
981 osc52_clipboard,
982 }
983 }
984
985 #[must_use]
990 pub const fn basic() -> Self {
991 Self {
992 profile: TerminalProfile::Dumb,
993 true_color: false,
994 colors_256: false,
995 unicode_box_drawing: false,
996 unicode_emoji: false,
997 double_width: false,
998 sync_output: false,
999 osc8_hyperlinks: false,
1000 scroll_region: false,
1001 in_tmux: false,
1002 in_screen: false,
1003 in_zellij: false,
1004 kitty_keyboard: false,
1005 focus_events: false,
1006 bracketed_paste: false,
1007 mouse_sgr: false,
1008 osc52_clipboard: false,
1009 }
1010 }
1011
1012 #[must_use]
1016 #[inline]
1017 pub const fn in_any_mux(&self) -> bool {
1018 self.in_tmux || self.in_screen || self.in_zellij
1019 }
1020
1021 #[must_use]
1023 #[inline]
1024 pub const fn has_color(&self) -> bool {
1025 self.true_color || self.colors_256
1026 }
1027
1028 #[must_use]
1030 pub const fn color_depth(&self) -> &'static str {
1031 if self.true_color {
1032 "truecolor"
1033 } else if self.colors_256 {
1034 "256"
1035 } else {
1036 "mono"
1037 }
1038 }
1039
1040 #[must_use]
1050 #[inline]
1051 pub const fn use_sync_output(&self) -> bool {
1052 if self.in_tmux || self.in_screen || self.in_zellij {
1053 return false;
1054 }
1055 self.sync_output
1056 }
1057
1058 #[must_use]
1063 #[inline]
1064 pub const fn use_scroll_region(&self) -> bool {
1065 if self.in_tmux || self.in_screen || self.in_zellij {
1066 return false;
1067 }
1068 self.scroll_region
1069 }
1070
1071 #[must_use]
1077 #[inline]
1078 pub const fn use_hyperlinks(&self) -> bool {
1079 if self.in_tmux || self.in_screen || self.in_zellij {
1080 return false;
1081 }
1082 self.osc8_hyperlinks
1083 }
1084
1085 #[must_use]
1090 #[inline]
1091 pub const fn use_clipboard(&self) -> bool {
1092 if self.in_tmux || self.in_screen || self.in_zellij {
1093 return false;
1094 }
1095 self.osc52_clipboard
1096 }
1097
1098 #[must_use]
1104 #[inline]
1105 pub const fn needs_passthrough_wrap(&self) -> bool {
1106 self.in_tmux || self.in_screen
1107 }
1108}
1109
1110pub struct SharedCapabilities {
1133 inner: crate::read_optimized::ArcSwapStore<TerminalCapabilities>,
1134}
1135
1136impl SharedCapabilities {
1137 pub fn new(caps: TerminalCapabilities) -> Self {
1139 Self {
1140 inner: crate::read_optimized::ArcSwapStore::new(caps),
1141 }
1142 }
1143
1144 pub fn detect() -> Self {
1146 Self::new(TerminalCapabilities::detect())
1147 }
1148
1149 #[inline]
1151 pub fn load(&self) -> TerminalCapabilities {
1152 crate::read_optimized::ReadOptimized::load(&self.inner)
1153 }
1154
1155 #[inline]
1157 pub fn store(&self, caps: TerminalCapabilities) {
1158 crate::read_optimized::ReadOptimized::store(&self.inner, caps);
1159 }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::*;
1165
1166 fn detect_with_override(value: Option<&str>) -> TerminalCapabilities {
1167 TerminalCapabilities::detect_with_test_profile_override(value)
1168 }
1169
1170 #[test]
1171 fn basic_is_minimal() {
1172 let caps = TerminalCapabilities::basic();
1173 assert!(!caps.true_color);
1174 assert!(!caps.colors_256);
1175 assert!(!caps.sync_output);
1176 assert!(!caps.osc8_hyperlinks);
1177 assert!(!caps.scroll_region);
1178 assert!(!caps.in_tmux);
1179 assert!(!caps.in_screen);
1180 assert!(!caps.in_zellij);
1181 assert!(!caps.kitty_keyboard);
1182 assert!(!caps.focus_events);
1183 assert!(!caps.bracketed_paste);
1184 assert!(!caps.mouse_sgr);
1185 assert!(!caps.osc52_clipboard);
1186 }
1187
1188 #[test]
1189 fn basic_is_default() {
1190 let basic = TerminalCapabilities::basic();
1191 let default = TerminalCapabilities::default();
1192 assert_eq!(basic, default);
1193 }
1194
1195 #[test]
1196 fn in_any_mux_logic() {
1197 let mut caps = TerminalCapabilities::basic();
1198 assert!(!caps.in_any_mux());
1199
1200 caps.in_tmux = true;
1201 assert!(caps.in_any_mux());
1202
1203 caps.in_tmux = false;
1204 caps.in_screen = true;
1205 assert!(caps.in_any_mux());
1206
1207 caps.in_screen = false;
1208 caps.in_zellij = true;
1209 assert!(caps.in_any_mux());
1210 }
1211
1212 #[test]
1213 fn has_color_logic() {
1214 let mut caps = TerminalCapabilities::basic();
1215 assert!(!caps.has_color());
1216
1217 caps.colors_256 = true;
1218 assert!(caps.has_color());
1219
1220 caps.colors_256 = false;
1221 caps.true_color = true;
1222 assert!(caps.has_color());
1223 }
1224
1225 #[test]
1226 fn color_depth_strings() {
1227 let mut caps = TerminalCapabilities::basic();
1228 assert_eq!(caps.color_depth(), "mono");
1229
1230 caps.colors_256 = true;
1231 assert_eq!(caps.color_depth(), "256");
1232
1233 caps.true_color = true;
1234 assert_eq!(caps.color_depth(), "truecolor");
1235 }
1236
1237 #[test]
1238 fn detect_does_not_panic() {
1239 let _caps = TerminalCapabilities::detect();
1241 }
1242
1243 #[test]
1244 fn windows_terminal_not_dumb_when_term_missing() {
1245 let env = DetectInputs {
1246 no_color: false,
1247 term: String::new(),
1248 term_program: String::new(),
1249 colorterm: String::new(),
1250 in_tmux: false,
1251 in_screen: false,
1252 in_zellij: false,
1253 kitty_window_id: false,
1254 wt_session: true,
1255 };
1256
1257 let caps = TerminalCapabilities::detect_from_inputs(&env);
1258 assert!(caps.true_color, "WT_SESSION implies true color by default");
1259 assert!(caps.colors_256, "truecolor implies 256-color");
1260 assert!(
1261 caps.osc8_hyperlinks,
1262 "WT_SESSION implies OSC 8 hyperlink support by default"
1263 );
1264 assert!(
1265 caps.bracketed_paste,
1266 "WT_SESSION should not be treated as dumb"
1267 );
1268 assert!(caps.mouse_sgr, "WT_SESSION should not be treated as dumb");
1269 }
1270
1271 #[test]
1272 #[cfg(target_os = "windows")]
1273 fn detect_windows_terminal_from_wt_session() {
1274 let mut env = make_env("", "", "");
1275 env.wt_session = true;
1276 let caps = TerminalCapabilities::detect_from_inputs(&env);
1277 assert!(caps.true_color, "WT_SESSION implies true color");
1278 assert!(caps.colors_256, "WT_SESSION implies 256-color");
1279 assert!(caps.osc8_hyperlinks, "WT_SESSION implies OSC 8 support");
1280 }
1281
1282 #[test]
1283 fn no_color_disables_color_and_links() {
1284 let env = DetectInputs {
1285 no_color: true,
1286 term: "xterm-256color".to_string(),
1287 term_program: "WezTerm".to_string(),
1288 colorterm: "truecolor".to_string(),
1289 in_tmux: false,
1290 in_screen: false,
1291 in_zellij: false,
1292 kitty_window_id: false,
1293 wt_session: false,
1294 };
1295
1296 let caps = TerminalCapabilities::detect_from_inputs(&env);
1297 assert!(!caps.true_color, "NO_COLOR must disable true color");
1298 assert!(!caps.colors_256, "NO_COLOR must disable 256-color");
1299 assert!(
1300 !caps.osc8_hyperlinks,
1301 "NO_COLOR must disable OSC 8 hyperlinks"
1302 );
1303 }
1304
1305 #[test]
1308 fn use_sync_output_disabled_in_tmux() {
1309 let mut caps = TerminalCapabilities::basic();
1310 caps.sync_output = true;
1311 assert!(caps.use_sync_output());
1312
1313 caps.in_tmux = true;
1314 assert!(!caps.use_sync_output());
1315 }
1316
1317 #[test]
1318 fn use_sync_output_disabled_in_screen() {
1319 let mut caps = TerminalCapabilities::basic();
1320 caps.sync_output = true;
1321 caps.in_screen = true;
1322 assert!(!caps.use_sync_output());
1323 }
1324
1325 #[test]
1326 fn use_sync_output_disabled_in_zellij() {
1327 let mut caps = TerminalCapabilities::basic();
1328 caps.sync_output = true;
1329 caps.in_zellij = true;
1330 assert!(!caps.use_sync_output());
1331 }
1332
1333 #[test]
1334 fn use_scroll_region_disabled_in_mux() {
1335 let mut caps = TerminalCapabilities::basic();
1336 caps.scroll_region = true;
1337 assert!(caps.use_scroll_region());
1338
1339 caps.in_tmux = true;
1340 assert!(!caps.use_scroll_region());
1341
1342 caps.in_tmux = false;
1343 caps.in_screen = true;
1344 assert!(!caps.use_scroll_region());
1345
1346 caps.in_screen = false;
1347 caps.in_zellij = true;
1348 assert!(!caps.use_scroll_region());
1349 }
1350
1351 #[test]
1352 fn use_hyperlinks_disabled_in_mux() {
1353 let mut caps = TerminalCapabilities::basic();
1354 caps.osc8_hyperlinks = true;
1355 assert!(caps.use_hyperlinks());
1356
1357 caps.in_tmux = true;
1358 assert!(!caps.use_hyperlinks());
1359 }
1360
1361 #[test]
1362 fn use_clipboard_disabled_in_mux() {
1363 let mut caps = TerminalCapabilities::basic();
1364 caps.osc52_clipboard = true;
1365 assert!(caps.use_clipboard());
1366
1367 caps.in_screen = true;
1368 assert!(!caps.use_clipboard());
1369 }
1370
1371 #[test]
1372 fn needs_passthrough_wrap_only_for_tmux_screen() {
1373 let mut caps = TerminalCapabilities::basic();
1374 assert!(!caps.needs_passthrough_wrap());
1375
1376 caps.in_tmux = true;
1377 assert!(caps.needs_passthrough_wrap());
1378
1379 caps.in_tmux = false;
1380 caps.in_screen = true;
1381 assert!(caps.needs_passthrough_wrap());
1382
1383 caps.in_screen = false;
1385 caps.in_zellij = true;
1386 assert!(!caps.needs_passthrough_wrap());
1387 }
1388
1389 #[test]
1390 fn policies_return_false_when_capability_absent() {
1391 let caps = TerminalCapabilities::basic();
1393 assert!(!caps.use_sync_output());
1394 assert!(!caps.use_scroll_region());
1395 assert!(!caps.use_hyperlinks());
1396 assert!(!caps.use_clipboard());
1397 }
1398
1399 fn make_env(term: &str, term_program: &str, colorterm: &str) -> DetectInputs {
1402 DetectInputs {
1403 no_color: false,
1404 term: term.to_string(),
1405 term_program: term_program.to_string(),
1406 colorterm: colorterm.to_string(),
1407 in_tmux: false,
1408 in_screen: false,
1409 in_zellij: false,
1410 kitty_window_id: false,
1411 wt_session: false,
1412 }
1413 }
1414
1415 #[test]
1416 fn detect_dumb_terminal() {
1417 let env = make_env("dumb", "", "");
1418 let caps = TerminalCapabilities::detect_from_inputs(&env);
1419 assert!(!caps.true_color);
1420 assert!(!caps.colors_256);
1421 assert!(!caps.sync_output);
1422 assert!(!caps.osc8_hyperlinks);
1423 assert!(!caps.scroll_region);
1424 assert!(!caps.focus_events);
1425 assert!(!caps.bracketed_paste);
1426 assert!(!caps.mouse_sgr);
1427 }
1428
1429 #[test]
1430 fn detect_dumb_overrides_truecolor_env() {
1431 let env = make_env("dumb", "WezTerm", "truecolor");
1432 let caps = TerminalCapabilities::detect_from_inputs(&env);
1433 assert!(!caps.true_color, "dumb should override COLORTERM");
1434 assert!(!caps.colors_256);
1435 assert!(!caps.bracketed_paste);
1436 assert!(!caps.mouse_sgr);
1437 assert!(!caps.osc8_hyperlinks);
1438 }
1439
1440 #[test]
1441 fn detect_empty_term_is_dumb() {
1442 let env = make_env("", "", "");
1443 let caps = TerminalCapabilities::detect_from_inputs(&env);
1444 assert!(!caps.true_color);
1445 assert!(!caps.bracketed_paste);
1446 }
1447
1448 #[test]
1449 fn detect_xterm_256color() {
1450 let env = make_env("xterm-256color", "", "");
1451 let caps = TerminalCapabilities::detect_from_inputs(&env);
1452 assert!(caps.colors_256, "xterm-256color implies 256 color");
1453 assert!(!caps.true_color, "256color alone does not imply truecolor");
1454 assert!(caps.bracketed_paste);
1455 assert!(caps.mouse_sgr);
1456 assert!(caps.scroll_region);
1457 }
1458
1459 #[test]
1460 fn detect_colorterm_truecolor() {
1461 let env = make_env("xterm-256color", "", "truecolor");
1462 let caps = TerminalCapabilities::detect_from_inputs(&env);
1463 assert!(caps.true_color, "COLORTERM=truecolor enables truecolor");
1464 assert!(caps.colors_256, "truecolor implies 256-color");
1465 }
1466
1467 #[test]
1468 fn detect_colorterm_24bit() {
1469 let env = make_env("xterm-256color", "", "24bit");
1470 let caps = TerminalCapabilities::detect_from_inputs(&env);
1471 assert!(caps.true_color, "COLORTERM=24bit enables truecolor");
1472 }
1473
1474 #[test]
1475 fn detect_kitty_by_window_id() {
1476 let mut env = make_env("xterm-kitty", "", "");
1477 env.kitty_window_id = true;
1478 let caps = TerminalCapabilities::detect_from_inputs(&env);
1479 assert!(caps.true_color, "Kitty supports truecolor");
1480 assert!(
1481 caps.kitty_keyboard,
1482 "Kitty supports kitty keyboard protocol"
1483 );
1484 assert!(caps.sync_output, "Kitty supports sync output");
1485 }
1486
1487 #[test]
1488 fn detect_kitty_by_term() {
1489 let env = make_env("xterm-kitty", "", "");
1490 let caps = TerminalCapabilities::detect_from_inputs(&env);
1491 assert!(caps.true_color, "kitty TERM implies truecolor");
1492 assert!(caps.kitty_keyboard);
1493 }
1494
1495 #[test]
1496 fn detect_wezterm() {
1497 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1498 let caps = TerminalCapabilities::detect_from_inputs(&env);
1499 assert!(caps.true_color);
1500 assert!(caps.sync_output, "WezTerm supports sync output");
1501 assert!(caps.osc8_hyperlinks, "WezTerm supports hyperlinks");
1502 assert!(caps.kitty_keyboard, "WezTerm supports kitty keyboard");
1503 assert!(caps.focus_events);
1504 assert!(caps.osc52_clipboard);
1505 }
1506
1507 #[test]
1508 #[cfg(target_os = "macos")]
1509 fn detect_iterm2_from_term_program() {
1510 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1511 let caps = TerminalCapabilities::detect_from_inputs(&env);
1512 assert!(caps.true_color, "iTerm2 implies truecolor");
1513 assert!(caps.osc8_hyperlinks, "iTerm2 supports OSC 8 hyperlinks");
1514 }
1515
1516 #[test]
1517 fn detect_alacritty() {
1518 let env = make_env("alacritty", "Alacritty", "truecolor");
1519 let caps = TerminalCapabilities::detect_from_inputs(&env);
1520 assert!(caps.true_color);
1521 assert!(caps.sync_output);
1522 assert!(caps.osc8_hyperlinks);
1523 assert!(caps.kitty_keyboard);
1524 assert!(caps.focus_events);
1525 }
1526
1527 #[test]
1528 fn detect_ghostty() {
1529 let env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1530 let caps = TerminalCapabilities::detect_from_inputs(&env);
1531 assert!(caps.true_color);
1532 assert!(caps.sync_output);
1533 assert!(caps.osc8_hyperlinks);
1534 assert!(caps.kitty_keyboard);
1535 assert!(caps.focus_events);
1536 }
1537
1538 #[test]
1539 fn detect_iterm() {
1540 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1541 let caps = TerminalCapabilities::detect_from_inputs(&env);
1542 assert!(caps.true_color);
1543 assert!(caps.osc8_hyperlinks);
1544 assert!(caps.kitty_keyboard);
1545 assert!(caps.focus_events);
1546 }
1547
1548 #[test]
1549 fn detect_vscode_terminal() {
1550 let env = make_env("xterm-256color", "vscode", "truecolor");
1551 let caps = TerminalCapabilities::detect_from_inputs(&env);
1552 assert!(caps.true_color);
1553 assert!(caps.osc8_hyperlinks);
1554 assert!(caps.focus_events);
1555 }
1556
1557 #[test]
1560 fn detect_in_tmux() {
1561 let mut env = make_env("screen-256color", "", "");
1562 env.in_tmux = true;
1563 let caps = TerminalCapabilities::detect_from_inputs(&env);
1564 assert!(caps.in_tmux);
1565 assert!(caps.in_any_mux());
1566 assert!(caps.colors_256);
1567 assert!(!caps.osc52_clipboard, "clipboard disabled in tmux");
1568 }
1569
1570 #[test]
1571 fn detect_in_screen() {
1572 let mut env = make_env("screen", "", "");
1573 env.in_screen = true;
1574 let caps = TerminalCapabilities::detect_from_inputs(&env);
1575 assert!(caps.in_screen);
1576 assert!(caps.in_any_mux());
1577 assert!(caps.needs_passthrough_wrap());
1578 }
1579
1580 #[test]
1581 fn detect_in_zellij() {
1582 let mut env = make_env("xterm-256color", "", "truecolor");
1583 env.in_zellij = true;
1584 let caps = TerminalCapabilities::detect_from_inputs(&env);
1585 assert!(caps.in_zellij);
1586 assert!(caps.in_any_mux());
1587 assert!(
1588 !caps.needs_passthrough_wrap(),
1589 "Zellij handles passthrough natively"
1590 );
1591 assert!(!caps.osc52_clipboard, "clipboard disabled in mux");
1592 }
1593
1594 #[test]
1595 fn detect_modern_terminal_in_tmux() {
1596 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
1597 env.in_tmux = true;
1598 let caps = TerminalCapabilities::detect_from_inputs(&env);
1599 assert!(caps.true_color);
1601 assert!(caps.sync_output);
1602 assert!(!caps.use_sync_output());
1604 assert!(!caps.use_hyperlinks());
1605 assert!(!caps.use_scroll_region());
1606 }
1607
1608 #[test]
1611 fn no_color_overrides_everything() {
1612 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1613 env.no_color = true;
1614 let caps = TerminalCapabilities::detect_from_inputs(&env);
1615 assert!(!caps.true_color);
1616 assert!(!caps.colors_256);
1617 assert!(!caps.osc8_hyperlinks);
1618 assert!(caps.sync_output);
1620 assert!(caps.bracketed_paste);
1621 assert!(caps.mouse_sgr);
1622 }
1623
1624 #[test]
1627 fn unknown_term_program() {
1628 let env = make_env("xterm", "SomeUnknownTerminal", "");
1629 let caps = TerminalCapabilities::detect_from_inputs(&env);
1630 assert!(
1631 !caps.true_color,
1632 "unknown terminal should not assume truecolor"
1633 );
1634 assert!(!caps.osc8_hyperlinks);
1635 assert!(caps.bracketed_paste);
1637 assert!(caps.mouse_sgr);
1638 assert!(caps.scroll_region);
1639 }
1640
1641 #[test]
1642 fn all_mux_flags_simultaneous() {
1643 let mut env = make_env("screen", "", "");
1644 env.in_tmux = true;
1645 env.in_screen = true;
1646 env.in_zellij = true;
1647 let caps = TerminalCapabilities::detect_from_inputs(&env);
1648 assert!(caps.in_any_mux());
1649 assert!(caps.needs_passthrough_wrap());
1650 assert!(!caps.use_sync_output());
1651 assert!(!caps.use_hyperlinks());
1652 assert!(!caps.use_clipboard());
1653 }
1654
1655 #[test]
1658 fn detect_rio() {
1659 let env = make_env("xterm-256color", "Rio", "truecolor");
1660 let caps = TerminalCapabilities::detect_from_inputs(&env);
1661 assert!(caps.true_color);
1662 assert!(caps.osc8_hyperlinks);
1663 assert!(caps.kitty_keyboard);
1664 assert!(caps.focus_events);
1665 }
1666
1667 #[test]
1668 fn detect_contour() {
1669 let env = make_env("xterm-256color", "Contour", "truecolor");
1670 let caps = TerminalCapabilities::detect_from_inputs(&env);
1671 assert!(caps.true_color);
1672 assert!(caps.sync_output);
1673 assert!(caps.osc8_hyperlinks);
1674 assert!(caps.focus_events);
1675 }
1676
1677 #[test]
1678 fn detect_foot() {
1679 let env = make_env("foot", "foot", "truecolor");
1680 let caps = TerminalCapabilities::detect_from_inputs(&env);
1681 assert!(caps.kitty_keyboard, "foot supports kitty keyboard");
1682 }
1683
1684 #[test]
1685 fn detect_hyper() {
1686 let env = make_env("xterm-256color", "Hyper", "truecolor");
1687 let caps = TerminalCapabilities::detect_from_inputs(&env);
1688 assert!(caps.true_color);
1689 assert!(caps.osc8_hyperlinks);
1690 assert!(caps.focus_events);
1691 }
1692
1693 #[test]
1694 fn detect_linux_console() {
1695 let env = make_env("linux", "", "");
1696 let caps = TerminalCapabilities::detect_from_inputs(&env);
1697 assert!(!caps.true_color, "linux console doesn't support truecolor");
1698 assert!(!caps.colors_256, "linux console doesn't support 256 colors");
1699 assert!(caps.bracketed_paste);
1701 assert!(caps.mouse_sgr);
1702 assert!(caps.scroll_region);
1703 }
1704
1705 #[test]
1706 fn detect_xterm_direct() {
1707 let env = make_env("xterm", "", "");
1708 let caps = TerminalCapabilities::detect_from_inputs(&env);
1709 assert!(!caps.true_color, "plain xterm has no truecolor");
1710 assert!(!caps.colors_256, "plain xterm has no 256color");
1711 assert!(caps.bracketed_paste);
1712 assert!(caps.mouse_sgr);
1713 }
1714
1715 #[test]
1716 fn detect_screen_256color() {
1717 let env = make_env("screen-256color", "", "");
1718 let caps = TerminalCapabilities::detect_from_inputs(&env);
1719 assert!(caps.colors_256, "screen-256color has 256 colors");
1720 assert!(!caps.true_color);
1721 }
1722
1723 #[test]
1726 fn wezterm_without_colorterm() {
1727 let env = make_env("xterm-256color", "WezTerm", "");
1728 let caps = TerminalCapabilities::detect_from_inputs(&env);
1729 assert!(caps.true_color, "WezTerm is modern, implies truecolor");
1731 assert!(caps.sync_output);
1732 assert!(caps.osc8_hyperlinks);
1733 }
1734
1735 #[test]
1736 fn alacritty_via_term_only() {
1737 let env = make_env("alacritty", "", "");
1739 let caps = TerminalCapabilities::detect_from_inputs(&env);
1740 assert!(caps.true_color);
1742 assert!(caps.osc8_hyperlinks);
1743 }
1744
1745 #[test]
1748 fn kitty_via_term_without_window_id() {
1749 let env = make_env("xterm-kitty", "", "");
1750 let caps = TerminalCapabilities::detect_from_inputs(&env);
1751 assert!(caps.kitty_keyboard);
1752 assert!(caps.true_color);
1753 assert!(caps.sync_output);
1754 }
1755
1756 #[test]
1757 fn kitty_window_id_with_generic_term() {
1758 let mut env = make_env("xterm-256color", "", "");
1759 env.kitty_window_id = true;
1760 let caps = TerminalCapabilities::detect_from_inputs(&env);
1761 assert!(caps.kitty_keyboard);
1762 assert!(caps.true_color);
1763 }
1764
1765 #[test]
1768 fn use_clipboard_enabled_when_no_mux_and_modern() {
1769 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1770 let caps = TerminalCapabilities::detect_from_inputs(&env);
1771 assert!(caps.osc52_clipboard);
1772 assert!(caps.use_clipboard());
1773 }
1774
1775 #[test]
1776 fn use_clipboard_disabled_in_tmux_even_if_detected() {
1777 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1778 env.in_tmux = true;
1779 let caps = TerminalCapabilities::detect_from_inputs(&env);
1780 assert!(!caps.osc52_clipboard);
1782 assert!(!caps.use_clipboard());
1783 }
1784
1785 #[test]
1786 fn scroll_region_enabled_for_basic_xterm() {
1787 let env = make_env("xterm", "", "");
1788 let caps = TerminalCapabilities::detect_from_inputs(&env);
1789 assert!(caps.scroll_region);
1790 assert!(caps.use_scroll_region());
1791 }
1792
1793 #[test]
1794 fn no_color_preserves_non_visual_features() {
1795 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1796 env.no_color = true;
1797 let caps = TerminalCapabilities::detect_from_inputs(&env);
1798 assert!(!caps.true_color);
1800 assert!(!caps.colors_256);
1801 assert!(!caps.osc8_hyperlinks);
1802 assert!(caps.sync_output);
1804 assert!(caps.kitty_keyboard);
1805 assert!(caps.focus_events);
1806 assert!(caps.bracketed_paste);
1807 assert!(caps.mouse_sgr);
1808 }
1809
1810 #[test]
1813 fn colorterm_yes_not_truecolor() {
1814 let env = make_env("xterm-256color", "", "yes");
1815 let caps = TerminalCapabilities::detect_from_inputs(&env);
1816 assert!(!caps.true_color, "COLORTERM=yes is not truecolor");
1817 assert!(caps.colors_256, "TERM=xterm-256color implies 256");
1818 }
1819
1820 #[test]
1823 fn profile_enum_as_str() {
1824 assert_eq!(TerminalProfile::Modern.as_str(), "modern");
1825 assert_eq!(TerminalProfile::Xterm256Color.as_str(), "xterm-256color");
1826 assert_eq!(TerminalProfile::Vt100.as_str(), "vt100");
1827 assert_eq!(TerminalProfile::Dumb.as_str(), "dumb");
1828 assert_eq!(TerminalProfile::Tmux.as_str(), "tmux");
1829 assert_eq!(TerminalProfile::Screen.as_str(), "screen");
1830 assert_eq!(TerminalProfile::Kitty.as_str(), "kitty");
1831 }
1832
1833 #[test]
1834 fn profile_enum_from_str() {
1835 use std::str::FromStr;
1836 assert_eq!(
1837 TerminalProfile::from_str("modern"),
1838 Ok(TerminalProfile::Modern)
1839 );
1840 assert_eq!(
1841 TerminalProfile::from_str("xterm-256color"),
1842 Ok(TerminalProfile::Xterm256Color)
1843 );
1844 assert_eq!(
1845 TerminalProfile::from_str("xterm256color"),
1846 Ok(TerminalProfile::Xterm256Color)
1847 );
1848 assert_eq!(TerminalProfile::from_str("DUMB"), Ok(TerminalProfile::Dumb));
1849 assert!(TerminalProfile::from_str("unknown").is_err());
1850 }
1851
1852 #[test]
1853 fn profile_all_predefined() {
1854 let all = TerminalProfile::all_predefined();
1855 assert!(all.len() >= 10);
1856 assert!(all.contains(&TerminalProfile::Modern));
1857 assert!(all.contains(&TerminalProfile::Dumb));
1858 assert!(!all.contains(&TerminalProfile::Custom));
1859 assert!(!all.contains(&TerminalProfile::Detected));
1860 }
1861
1862 #[test]
1863 fn profile_modern_has_all_features() {
1864 let caps = TerminalCapabilities::modern();
1865 assert_eq!(caps.profile(), TerminalProfile::Modern);
1866 assert_eq!(caps.profile_name(), Some("modern"));
1867 assert!(caps.true_color);
1868 assert!(caps.colors_256);
1869 assert!(caps.sync_output);
1870 assert!(caps.osc8_hyperlinks);
1871 assert!(caps.scroll_region);
1872 assert!(caps.kitty_keyboard);
1873 assert!(caps.focus_events);
1874 assert!(caps.bracketed_paste);
1875 assert!(caps.mouse_sgr);
1876 assert!(caps.osc52_clipboard);
1877 assert!(!caps.in_any_mux());
1878 }
1879
1880 #[test]
1881 fn profile_xterm_256color() {
1882 let caps = TerminalCapabilities::xterm_256color();
1883 assert_eq!(caps.profile(), TerminalProfile::Xterm256Color);
1884 assert!(!caps.true_color);
1885 assert!(caps.colors_256);
1886 assert!(!caps.sync_output);
1887 assert!(!caps.osc8_hyperlinks);
1888 assert!(caps.scroll_region);
1889 assert!(caps.bracketed_paste);
1890 assert!(caps.mouse_sgr);
1891 }
1892
1893 #[test]
1894 fn profile_xterm_basic() {
1895 let caps = TerminalCapabilities::xterm();
1896 assert_eq!(caps.profile(), TerminalProfile::Xterm);
1897 assert!(!caps.true_color);
1898 assert!(!caps.colors_256);
1899 assert!(caps.scroll_region);
1900 }
1901
1902 #[test]
1903 fn profile_vt100_minimal() {
1904 let caps = TerminalCapabilities::vt100();
1905 assert_eq!(caps.profile(), TerminalProfile::Vt100);
1906 assert!(!caps.true_color);
1907 assert!(!caps.colors_256);
1908 assert!(caps.scroll_region);
1909 assert!(!caps.bracketed_paste);
1910 assert!(!caps.mouse_sgr);
1911 }
1912
1913 #[test]
1914 fn profile_dumb_no_features() {
1915 let caps = TerminalCapabilities::dumb();
1916 assert_eq!(caps.profile(), TerminalProfile::Dumb);
1917 assert!(!caps.true_color);
1918 assert!(!caps.colors_256);
1919 assert!(!caps.scroll_region);
1920 assert!(!caps.bracketed_paste);
1921 assert!(!caps.mouse_sgr);
1922 assert!(!caps.use_sync_output());
1923 assert!(!caps.use_scroll_region());
1924 }
1925
1926 #[test]
1927 fn profile_tmux_mux_flags() {
1928 let caps = TerminalCapabilities::tmux();
1929 assert_eq!(caps.profile(), TerminalProfile::Tmux);
1930 assert!(caps.in_tmux);
1931 assert!(!caps.in_screen);
1932 assert!(!caps.in_zellij);
1933 assert!(caps.in_any_mux());
1934 assert!(!caps.use_sync_output());
1936 assert!(!caps.use_scroll_region());
1937 assert!(!caps.use_hyperlinks());
1938 }
1939
1940 #[test]
1941 fn profile_screen_mux_flags() {
1942 let caps = TerminalCapabilities::screen();
1943 assert_eq!(caps.profile(), TerminalProfile::Screen);
1944 assert!(!caps.in_tmux);
1945 assert!(caps.in_screen);
1946 assert!(caps.in_any_mux());
1947 assert!(caps.needs_passthrough_wrap());
1948 }
1949
1950 #[test]
1951 fn profile_zellij_mux_flags() {
1952 let caps = TerminalCapabilities::zellij();
1953 assert_eq!(caps.profile(), TerminalProfile::Zellij);
1954 assert!(caps.in_zellij);
1955 assert!(caps.in_any_mux());
1956 assert!(caps.true_color);
1958 assert!(caps.focus_events);
1959 assert!(!caps.needs_passthrough_wrap());
1961 }
1962
1963 #[test]
1964 fn profile_kitty_full_features() {
1965 let caps = TerminalCapabilities::kitty();
1966 assert_eq!(caps.profile(), TerminalProfile::Kitty);
1967 assert!(caps.true_color);
1968 assert!(caps.sync_output);
1969 assert!(caps.kitty_keyboard);
1970 assert!(caps.osc8_hyperlinks);
1971 }
1972
1973 #[test]
1974 fn profile_windows_console() {
1975 let caps = TerminalCapabilities::windows_console();
1976 assert_eq!(caps.profile(), TerminalProfile::WindowsConsole);
1977 assert!(caps.true_color);
1978 assert!(caps.osc8_hyperlinks);
1979 assert!(caps.focus_events);
1980 }
1981
1982 #[test]
1983 fn profile_linux_console() {
1984 let caps = TerminalCapabilities::linux_console();
1985 assert_eq!(caps.profile(), TerminalProfile::LinuxConsole);
1986 assert!(!caps.true_color);
1987 assert!(!caps.colors_256);
1988 assert!(caps.scroll_region);
1989 }
1990
1991 #[test]
1992 fn from_profile_roundtrip() {
1993 for profile in TerminalProfile::all_predefined() {
1994 let caps = TerminalCapabilities::from_profile(*profile);
1995 assert_eq!(caps.profile(), *profile);
1996 }
1997 }
1998
1999 #[test]
2000 fn detected_profile_has_none_name() {
2001 let caps = detect_with_override(None);
2002 assert_eq!(caps.profile(), TerminalProfile::Detected);
2003 assert_eq!(caps.profile_name(), None);
2004 }
2005
2006 #[test]
2007 fn detect_respects_test_profile_env() {
2008 let caps = detect_with_override(Some("dumb"));
2009 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2010 }
2011
2012 #[test]
2013 fn detect_ignores_invalid_test_profile() {
2014 let caps = detect_with_override(Some("not-a-real-profile"));
2015 assert_eq!(caps.profile(), TerminalProfile::Detected);
2016 }
2017
2018 #[test]
2019 fn basic_has_dumb_profile() {
2020 let caps = TerminalCapabilities::basic();
2021 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2022 }
2023
2024 #[test]
2027 fn builder_starts_empty() {
2028 let caps = CapabilityProfileBuilder::new().build();
2029 assert_eq!(caps.profile(), TerminalProfile::Custom);
2030 assert!(!caps.true_color);
2031 assert!(!caps.colors_256);
2032 assert!(!caps.sync_output);
2033 assert!(!caps.scroll_region);
2034 assert!(!caps.mouse_sgr);
2035 }
2036
2037 #[test]
2038 fn builder_set_colors() {
2039 let caps = CapabilityProfileBuilder::new()
2040 .true_color(true)
2041 .colors_256(true)
2042 .build();
2043 assert!(caps.true_color);
2044 assert!(caps.colors_256);
2045 }
2046
2047 #[test]
2048 fn builder_set_advanced() {
2049 let caps = CapabilityProfileBuilder::new()
2050 .sync_output(true)
2051 .osc8_hyperlinks(true)
2052 .scroll_region(true)
2053 .build();
2054 assert!(caps.sync_output);
2055 assert!(caps.osc8_hyperlinks);
2056 assert!(caps.scroll_region);
2057 }
2058
2059 #[test]
2060 fn builder_set_mux() {
2061 let caps = CapabilityProfileBuilder::new()
2062 .in_tmux(true)
2063 .in_screen(false)
2064 .in_zellij(false)
2065 .build();
2066 assert!(caps.in_tmux);
2067 assert!(!caps.in_screen);
2068 assert!(caps.in_any_mux());
2069 }
2070
2071 #[test]
2072 fn builder_set_input() {
2073 let caps = CapabilityProfileBuilder::new()
2074 .kitty_keyboard(true)
2075 .focus_events(true)
2076 .bracketed_paste(true)
2077 .mouse_sgr(true)
2078 .build();
2079 assert!(caps.kitty_keyboard);
2080 assert!(caps.focus_events);
2081 assert!(caps.bracketed_paste);
2082 assert!(caps.mouse_sgr);
2083 }
2084
2085 #[test]
2086 fn builder_set_clipboard() {
2087 let caps = CapabilityProfileBuilder::new()
2088 .osc52_clipboard(true)
2089 .build();
2090 assert!(caps.osc52_clipboard);
2091 }
2092
2093 #[test]
2094 fn builder_from_profile() {
2095 let caps = CapabilityProfileBuilder::from_profile(TerminalProfile::Modern)
2096 .sync_output(false) .build();
2098 assert!(caps.true_color);
2100 assert!(caps.colors_256);
2101 assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
2103 assert_eq!(caps.profile(), TerminalProfile::Custom);
2105 }
2106
2107 #[test]
2108 fn builder_chain_multiple() {
2109 let caps = TerminalCapabilities::builder()
2110 .colors_256(true)
2111 .bracketed_paste(true)
2112 .mouse_sgr(true)
2113 .scroll_region(true)
2114 .build();
2115 assert!(caps.colors_256);
2116 assert!(caps.bracketed_paste);
2117 assert!(caps.mouse_sgr);
2118 assert!(caps.scroll_region);
2119 assert!(!caps.true_color);
2120 assert!(!caps.sync_output);
2121 }
2122
2123 #[test]
2124 fn builder_default() {
2125 let builder = CapabilityProfileBuilder::default();
2126 let caps = builder.build();
2127 assert_eq!(caps.profile(), TerminalProfile::Custom);
2128 }
2129
2130 #[test]
2139 fn mux_compatibility_matrix() {
2140 {
2145 let caps = TerminalCapabilities::modern();
2146 assert!(
2147 caps.use_sync_output(),
2148 "baseline: sync_output should be enabled"
2149 );
2150 assert!(
2151 caps.use_scroll_region(),
2152 "baseline: scroll_region should be enabled"
2153 );
2154 assert!(
2155 caps.use_hyperlinks(),
2156 "baseline: hyperlinks should be enabled"
2157 );
2158 assert!(!caps.needs_passthrough_wrap(), "baseline: no wrap needed");
2159 }
2160
2161 {
2163 let caps = TerminalCapabilities::tmux();
2164 assert!(!caps.use_sync_output(), "tmux: sync_output disabled");
2165 assert!(!caps.use_scroll_region(), "tmux: scroll_region disabled");
2166 assert!(!caps.use_hyperlinks(), "tmux: hyperlinks disabled");
2167 assert!(caps.needs_passthrough_wrap(), "tmux: needs wrap");
2168 }
2169
2170 {
2172 let caps = TerminalCapabilities::screen();
2173 assert!(!caps.use_sync_output(), "screen: sync_output disabled");
2174 assert!(!caps.use_scroll_region(), "screen: scroll_region disabled");
2175 assert!(!caps.use_hyperlinks(), "screen: hyperlinks disabled");
2176 assert!(caps.needs_passthrough_wrap(), "screen: needs wrap");
2177 }
2178
2179 {
2181 let caps = TerminalCapabilities::zellij();
2182 assert!(!caps.use_sync_output(), "zellij: sync_output disabled");
2183 assert!(!caps.use_scroll_region(), "zellij: scroll_region disabled");
2184 assert!(!caps.use_hyperlinks(), "zellij: hyperlinks disabled");
2185 assert!(
2186 !caps.needs_passthrough_wrap(),
2187 "zellij: no wrap needed (native passthrough)"
2188 );
2189 }
2190 }
2191
2192 #[test]
2194 fn modern_terminal_in_mux_matrix() {
2195 for (mux_name, in_tmux, in_screen, in_zellij) in [
2199 ("tmux", true, false, false),
2200 ("screen", false, true, false),
2201 ("zellij", false, false, true),
2202 ] {
2203 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2204 env.in_tmux = in_tmux;
2205 env.in_screen = in_screen;
2206 env.in_zellij = in_zellij;
2207 let caps = TerminalCapabilities::detect_from_inputs(&env);
2208
2209 assert!(
2211 caps.true_color,
2212 "{mux_name}: true_color detection should work"
2213 );
2214 assert!(
2215 caps.sync_output,
2216 "{mux_name}: sync_output detection should work"
2217 );
2218
2219 assert!(
2221 !caps.use_sync_output(),
2222 "{mux_name}: use_sync_output() should be false"
2223 );
2224 assert!(
2225 !caps.use_scroll_region(),
2226 "{mux_name}: use_scroll_region() should be false"
2227 );
2228 assert!(
2229 !caps.use_hyperlinks(),
2230 "{mux_name}: use_hyperlinks() should be false"
2231 );
2232 }
2233 }
2234
2235 #[test]
2237 fn profile_mux_invariant_matrix() {
2238 for profile in TerminalProfile::all_predefined() {
2240 let caps = TerminalCapabilities::from_profile(*profile);
2241 let name = profile.as_str();
2242
2243 let expected_mux = caps.in_tmux || caps.in_screen || caps.in_zellij;
2245 assert_eq!(
2246 caps.in_any_mux(),
2247 expected_mux,
2248 "{name}: in_any_mux() should match individual flags"
2249 );
2250
2251 if caps.in_any_mux() {
2253 assert!(
2254 !caps.use_sync_output(),
2255 "{name}: mux should disable use_sync_output()"
2256 );
2257 assert!(
2258 !caps.use_scroll_region(),
2259 "{name}: mux should disable use_scroll_region()"
2260 );
2261 assert!(
2262 !caps.use_hyperlinks(),
2263 "{name}: mux should disable use_hyperlinks()"
2264 );
2265 }
2266
2267 if caps.in_tmux || caps.in_screen {
2269 assert!(
2270 caps.needs_passthrough_wrap(),
2271 "{name}: tmux/screen should need passthrough wrap"
2272 );
2273 } else if caps.in_zellij {
2274 assert!(
2275 !caps.needs_passthrough_wrap(),
2276 "{name}: zellij should NOT need passthrough wrap"
2277 );
2278 }
2279 }
2280 }
2281
2282 #[test]
2284 fn fallback_ordering_matrix() {
2285 use crate::inline_mode::InlineStrategy;
2286
2287 let caps_full = TerminalCapabilities::builder()
2289 .sync_output(true)
2290 .scroll_region(true)
2291 .build();
2292 assert_eq!(
2293 InlineStrategy::select(&caps_full),
2294 InlineStrategy::ScrollRegion,
2295 "full capabilities should use ScrollRegion"
2296 );
2297
2298 let caps_hybrid = TerminalCapabilities::builder()
2300 .sync_output(false)
2301 .scroll_region(true)
2302 .build();
2303 assert_eq!(
2304 InlineStrategy::select(&caps_hybrid),
2305 InlineStrategy::Hybrid,
2306 "scroll without sync should use Hybrid"
2307 );
2308
2309 let caps_none = TerminalCapabilities::builder()
2311 .sync_output(false)
2312 .scroll_region(false)
2313 .build();
2314 assert_eq!(
2315 InlineStrategy::select(&caps_none),
2316 InlineStrategy::OverlayRedraw,
2317 "no capabilities should use OverlayRedraw"
2318 );
2319
2320 let caps_tmux = TerminalCapabilities::tmux();
2322 assert_eq!(
2323 InlineStrategy::select(&caps_tmux),
2324 InlineStrategy::OverlayRedraw,
2325 "tmux should force OverlayRedraw"
2326 );
2327 }
2328
2329 #[test]
2331 fn terminal_mux_strategy_matrix() {
2332 use crate::inline_mode::InlineStrategy;
2333
2334 struct TestCase {
2335 name: &'static str,
2336 profile: TerminalProfile,
2337 expected: InlineStrategy,
2338 }
2339
2340 let cases = [
2341 TestCase {
2342 name: "modern (no mux)",
2343 profile: TerminalProfile::Modern,
2344 expected: InlineStrategy::ScrollRegion,
2345 },
2346 TestCase {
2347 name: "kitty (no mux)",
2348 profile: TerminalProfile::Kitty,
2349 expected: InlineStrategy::ScrollRegion,
2350 },
2351 TestCase {
2352 name: "xterm-256color (no mux)",
2353 profile: TerminalProfile::Xterm256Color,
2354 expected: InlineStrategy::Hybrid, },
2356 TestCase {
2357 name: "xterm (no mux)",
2358 profile: TerminalProfile::Xterm,
2359 expected: InlineStrategy::Hybrid,
2360 },
2361 TestCase {
2362 name: "vt100 (no mux)",
2363 profile: TerminalProfile::Vt100,
2364 expected: InlineStrategy::Hybrid,
2365 },
2366 TestCase {
2367 name: "dumb",
2368 profile: TerminalProfile::Dumb,
2369 expected: InlineStrategy::OverlayRedraw, },
2371 TestCase {
2372 name: "tmux",
2373 profile: TerminalProfile::Tmux,
2374 expected: InlineStrategy::OverlayRedraw,
2375 },
2376 TestCase {
2377 name: "screen",
2378 profile: TerminalProfile::Screen,
2379 expected: InlineStrategy::OverlayRedraw,
2380 },
2381 TestCase {
2382 name: "zellij",
2383 profile: TerminalProfile::Zellij,
2384 expected: InlineStrategy::OverlayRedraw,
2385 },
2386 ];
2387
2388 for case in cases {
2389 let caps = TerminalCapabilities::from_profile(case.profile);
2390 let actual = InlineStrategy::select(&caps);
2391 assert_eq!(
2392 actual, case.expected,
2393 "{}: expected {:?}, got {:?}",
2394 case.name, case.expected, actual
2395 );
2396 }
2397 }
2398
2399 #[test]
2402 fn shared_caps_load_returns_initial() {
2403 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
2404 assert!(shared.load().true_color);
2405 assert!(shared.load().sync_output);
2406 }
2407
2408 #[test]
2409 fn shared_caps_store_replaces_value() {
2410 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
2411 shared.store(TerminalCapabilities::dumb());
2412 let loaded = shared.load();
2413 assert!(!loaded.true_color);
2414 assert!(!loaded.sync_output);
2415 }
2416
2417 #[test]
2418 fn shared_caps_concurrent_read_write() {
2419 use std::sync::{Arc, Barrier};
2420 use std::thread;
2421
2422 let shared = Arc::new(SharedCapabilities::new(TerminalCapabilities::basic()));
2423 let barrier = Arc::new(Barrier::new(5)); let readers: Vec<_> = (0..4)
2426 .map(|_| {
2427 let s = Arc::clone(&shared);
2428 let b = Arc::clone(&barrier);
2429 thread::spawn(move || {
2430 b.wait();
2431 for _ in 0..10_000 {
2432 let caps = s.load();
2433 let _ = caps.use_sync_output();
2435 let _ = caps.true_color;
2436 }
2437 })
2438 })
2439 .collect();
2440
2441 let writer = {
2442 let s = Arc::clone(&shared);
2443 let b = Arc::clone(&barrier);
2444 thread::spawn(move || {
2445 b.wait();
2446 for i in 0..1_000 {
2447 if i % 2 == 0 {
2448 s.store(TerminalCapabilities::modern());
2449 } else {
2450 s.store(TerminalCapabilities::dumb());
2451 }
2452 }
2453 })
2454 };
2455
2456 writer.join().unwrap();
2457 for h in readers {
2458 h.join().unwrap();
2459 }
2460 }
2461}
2462
2463#[cfg(test)]
2468mod proptests {
2469 use super::*;
2470 use proptest::prelude::*;
2471
2472 proptest! {
2473 #[test]
2475 fn prop_in_any_mux_consistent(
2476 in_tmux in any::<bool>(),
2477 in_screen in any::<bool>(),
2478 in_zellij in any::<bool>(),
2479 ) {
2480 let caps = TerminalCapabilities::builder()
2481 .in_tmux(in_tmux)
2482 .in_screen(in_screen)
2483 .in_zellij(in_zellij)
2484 .build();
2485
2486 let expected = in_tmux || in_screen || in_zellij;
2487 prop_assert_eq!(caps.in_any_mux(), expected);
2488 }
2489
2490 #[test]
2492 fn prop_mux_disables_sync_output(
2493 in_tmux in any::<bool>(),
2494 in_screen in any::<bool>(),
2495 in_zellij in any::<bool>(),
2496 sync_output in any::<bool>(),
2497 ) {
2498 let caps = TerminalCapabilities::builder()
2499 .in_tmux(in_tmux)
2500 .in_screen(in_screen)
2501 .in_zellij(in_zellij)
2502 .sync_output(sync_output)
2503 .build();
2504
2505 if caps.in_any_mux() {
2506 prop_assert!(!caps.use_sync_output(), "mux should disable sync_output policy");
2507 }
2508 }
2509
2510 #[test]
2512 fn prop_mux_disables_scroll_region(
2513 in_tmux in any::<bool>(),
2514 in_screen in any::<bool>(),
2515 in_zellij in any::<bool>(),
2516 scroll_region in any::<bool>(),
2517 ) {
2518 let caps = TerminalCapabilities::builder()
2519 .in_tmux(in_tmux)
2520 .in_screen(in_screen)
2521 .in_zellij(in_zellij)
2522 .scroll_region(scroll_region)
2523 .build();
2524
2525 if caps.in_any_mux() {
2526 prop_assert!(!caps.use_scroll_region(), "mux should disable scroll_region policy");
2527 }
2528 }
2529
2530 #[test]
2532 fn prop_mux_disables_hyperlinks(
2533 in_tmux in any::<bool>(),
2534 in_screen in any::<bool>(),
2535 in_zellij in any::<bool>(),
2536 osc8_hyperlinks in any::<bool>(),
2537 ) {
2538 let caps = TerminalCapabilities::builder()
2539 .in_tmux(in_tmux)
2540 .in_screen(in_screen)
2541 .in_zellij(in_zellij)
2542 .osc8_hyperlinks(osc8_hyperlinks)
2543 .build();
2544
2545 if caps.in_any_mux() {
2546 prop_assert!(!caps.use_hyperlinks(), "mux should disable hyperlinks policy");
2547 }
2548 }
2549
2550 #[test]
2552 fn prop_passthrough_wrap_logic(
2553 in_tmux in any::<bool>(),
2554 in_screen in any::<bool>(),
2555 in_zellij in any::<bool>(),
2556 ) {
2557 let caps = TerminalCapabilities::builder()
2558 .in_tmux(in_tmux)
2559 .in_screen(in_screen)
2560 .in_zellij(in_zellij)
2561 .build();
2562
2563 let expected = in_tmux || in_screen; prop_assert_eq!(caps.needs_passthrough_wrap(), expected);
2565 }
2566
2567 #[test]
2569 fn prop_policy_false_when_capability_off(
2570 in_tmux in any::<bool>(),
2571 in_screen in any::<bool>(),
2572 in_zellij in any::<bool>(),
2573 ) {
2574 let caps = TerminalCapabilities::builder()
2575 .in_tmux(in_tmux)
2576 .in_screen(in_screen)
2577 .in_zellij(in_zellij)
2578 .sync_output(false)
2579 .scroll_region(false)
2580 .osc8_hyperlinks(false)
2581 .osc52_clipboard(false)
2582 .build();
2583
2584 prop_assert!(!caps.use_sync_output(), "sync_output=false implies use_sync_output()=false");
2585 prop_assert!(!caps.use_scroll_region(), "scroll_region=false implies use_scroll_region()=false");
2586 prop_assert!(!caps.use_hyperlinks(), "osc8_hyperlinks=false implies use_hyperlinks()=false");
2587 prop_assert!(!caps.use_clipboard(), "osc52_clipboard=false implies use_clipboard()=false");
2588 }
2589
2590 #[test]
2592 fn prop_no_color_preserves_non_visual(no_color in any::<bool>()) {
2593 let env = DetectInputs {
2594 no_color,
2595 term: "xterm-256color".to_string(),
2596 term_program: "WezTerm".to_string(),
2597 colorterm: "truecolor".to_string(),
2598 in_tmux: false,
2599 in_screen: false,
2600 in_zellij: false,
2601 kitty_window_id: false,
2602 wt_session: false,
2603 };
2604 let caps = TerminalCapabilities::detect_from_inputs(&env);
2605
2606 if no_color {
2607 prop_assert!(!caps.true_color, "NO_COLOR disables true_color");
2608 prop_assert!(!caps.colors_256, "NO_COLOR disables colors_256");
2609 prop_assert!(!caps.osc8_hyperlinks, "NO_COLOR disables hyperlinks");
2610 }
2611
2612 prop_assert!(caps.sync_output, "sync_output preserved despite NO_COLOR");
2614 prop_assert!(caps.bracketed_paste, "bracketed_paste preserved despite NO_COLOR");
2615 }
2616 }
2617}