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 sync_output: bool,
307 pub osc8_hyperlinks: bool,
309 pub scroll_region: bool,
311
312 pub in_tmux: bool,
315 pub in_screen: bool,
317 pub in_zellij: bool,
319
320 pub kitty_keyboard: bool,
323 pub focus_events: bool,
325 pub bracketed_paste: bool,
327 pub mouse_sgr: bool,
329
330 pub osc52_clipboard: bool,
333}
334
335impl Default for TerminalCapabilities {
336 fn default() -> Self {
337 Self::basic()
338 }
339}
340
341impl TerminalCapabilities {
346 #[must_use]
350 pub const fn profile(&self) -> TerminalProfile {
351 self.profile
352 }
353
354 #[must_use]
359 pub fn profile_name(&self) -> Option<&'static str> {
360 match self.profile {
361 TerminalProfile::Detected => None,
362 p => Some(p.as_str()),
363 }
364 }
365
366 #[must_use]
368 pub fn from_profile(profile: TerminalProfile) -> Self {
369 match profile {
370 TerminalProfile::Modern => Self::modern(),
371 TerminalProfile::Xterm256Color => Self::xterm_256color(),
372 TerminalProfile::Xterm => Self::xterm(),
373 TerminalProfile::Vt100 => Self::vt100(),
374 TerminalProfile::Dumb => Self::dumb(),
375 TerminalProfile::Screen => Self::screen(),
376 TerminalProfile::Tmux => Self::tmux(),
377 TerminalProfile::Zellij => Self::zellij(),
378 TerminalProfile::WindowsConsole => Self::windows_console(),
379 TerminalProfile::Kitty => Self::kitty(),
380 TerminalProfile::LinuxConsole => Self::linux_console(),
381 TerminalProfile::Custom => Self::basic(),
382 TerminalProfile::Detected => Self::detect(),
383 }
384 }
385
386 #[must_use]
393 pub const fn modern() -> Self {
394 Self {
395 profile: TerminalProfile::Modern,
396 true_color: true,
397 colors_256: true,
398 sync_output: true,
399 osc8_hyperlinks: true,
400 scroll_region: true,
401 in_tmux: false,
402 in_screen: false,
403 in_zellij: false,
404 kitty_keyboard: true,
405 focus_events: true,
406 bracketed_paste: true,
407 mouse_sgr: true,
408 osc52_clipboard: true,
409 }
410 }
411
412 #[must_use]
417 pub const fn xterm_256color() -> Self {
418 Self {
419 profile: TerminalProfile::Xterm256Color,
420 true_color: false,
421 colors_256: true,
422 sync_output: false,
423 osc8_hyperlinks: false,
424 scroll_region: true,
425 in_tmux: false,
426 in_screen: false,
427 in_zellij: false,
428 kitty_keyboard: false,
429 focus_events: false,
430 bracketed_paste: true,
431 mouse_sgr: true,
432 osc52_clipboard: false,
433 }
434 }
435
436 #[must_use]
440 pub const fn xterm() -> Self {
441 Self {
442 profile: TerminalProfile::Xterm,
443 true_color: false,
444 colors_256: false,
445 sync_output: false,
446 osc8_hyperlinks: false,
447 scroll_region: true,
448 in_tmux: false,
449 in_screen: false,
450 in_zellij: false,
451 kitty_keyboard: false,
452 focus_events: false,
453 bracketed_paste: true,
454 mouse_sgr: true,
455 osc52_clipboard: false,
456 }
457 }
458
459 #[must_use]
463 pub const fn vt100() -> Self {
464 Self {
465 profile: TerminalProfile::Vt100,
466 true_color: false,
467 colors_256: false,
468 sync_output: false,
469 osc8_hyperlinks: false,
470 scroll_region: true,
471 in_tmux: false,
472 in_screen: false,
473 in_zellij: false,
474 kitty_keyboard: false,
475 focus_events: false,
476 bracketed_paste: false,
477 mouse_sgr: false,
478 osc52_clipboard: false,
479 }
480 }
481
482 #[must_use]
486 pub const fn dumb() -> Self {
487 Self {
488 profile: TerminalProfile::Dumb,
489 true_color: false,
490 colors_256: false,
491 sync_output: false,
492 osc8_hyperlinks: false,
493 scroll_region: false,
494 in_tmux: false,
495 in_screen: false,
496 in_zellij: false,
497 kitty_keyboard: false,
498 focus_events: false,
499 bracketed_paste: false,
500 mouse_sgr: false,
501 osc52_clipboard: false,
502 }
503 }
504
505 #[must_use]
510 pub const fn screen() -> Self {
511 Self {
512 profile: TerminalProfile::Screen,
513 true_color: false,
514 colors_256: true,
515 sync_output: false,
516 osc8_hyperlinks: false,
517 scroll_region: true,
518 in_tmux: false,
519 in_screen: true,
520 in_zellij: false,
521 kitty_keyboard: false,
522 focus_events: false,
523 bracketed_paste: true,
524 mouse_sgr: true,
525 osc52_clipboard: false,
526 }
527 }
528
529 #[must_use]
534 pub const fn tmux() -> Self {
535 Self {
536 profile: TerminalProfile::Tmux,
537 true_color: false,
538 colors_256: true,
539 sync_output: false,
540 osc8_hyperlinks: false,
541 scroll_region: true,
542 in_tmux: true,
543 in_screen: false,
544 in_zellij: false,
545 kitty_keyboard: false,
546 focus_events: false,
547 bracketed_paste: true,
548 mouse_sgr: true,
549 osc52_clipboard: false,
550 }
551 }
552
553 #[must_use]
557 pub const fn zellij() -> Self {
558 Self {
559 profile: TerminalProfile::Zellij,
560 true_color: true,
561 colors_256: true,
562 sync_output: false,
563 osc8_hyperlinks: false,
564 scroll_region: true,
565 in_tmux: false,
566 in_screen: false,
567 in_zellij: true,
568 kitty_keyboard: false,
569 focus_events: true,
570 bracketed_paste: true,
571 mouse_sgr: true,
572 osc52_clipboard: false,
573 }
574 }
575
576 #[must_use]
580 pub const fn windows_console() -> Self {
581 Self {
582 profile: TerminalProfile::WindowsConsole,
583 true_color: true,
584 colors_256: true,
585 sync_output: false,
586 osc8_hyperlinks: true,
587 scroll_region: true,
588 in_tmux: false,
589 in_screen: false,
590 in_zellij: false,
591 kitty_keyboard: false,
592 focus_events: true,
593 bracketed_paste: true,
594 mouse_sgr: true,
595 osc52_clipboard: true,
596 }
597 }
598
599 #[must_use]
603 pub const fn kitty() -> Self {
604 Self {
605 profile: TerminalProfile::Kitty,
606 true_color: true,
607 colors_256: true,
608 sync_output: true,
609 osc8_hyperlinks: true,
610 scroll_region: true,
611 in_tmux: false,
612 in_screen: false,
613 in_zellij: false,
614 kitty_keyboard: true,
615 focus_events: true,
616 bracketed_paste: true,
617 mouse_sgr: true,
618 osc52_clipboard: true,
619 }
620 }
621
622 #[must_use]
626 pub const fn linux_console() -> Self {
627 Self {
628 profile: TerminalProfile::LinuxConsole,
629 true_color: false,
630 colors_256: false,
631 sync_output: false,
632 osc8_hyperlinks: false,
633 scroll_region: true,
634 in_tmux: false,
635 in_screen: false,
636 in_zellij: false,
637 kitty_keyboard: false,
638 focus_events: false,
639 bracketed_paste: true,
640 mouse_sgr: true,
641 osc52_clipboard: false,
642 }
643 }
644
645 #[must_use]
649 pub fn builder() -> CapabilityProfileBuilder {
650 CapabilityProfileBuilder::new()
651 }
652}
653
654#[derive(Debug, Clone)]
679pub struct CapabilityProfileBuilder {
680 caps: TerminalCapabilities,
681}
682
683impl Default for CapabilityProfileBuilder {
684 fn default() -> Self {
685 Self::new()
686 }
687}
688
689impl CapabilityProfileBuilder {
690 #[must_use]
692 pub fn new() -> Self {
693 Self {
694 caps: TerminalCapabilities {
695 profile: TerminalProfile::Custom,
696 true_color: false,
697 colors_256: false,
698 sync_output: false,
699 osc8_hyperlinks: false,
700 scroll_region: false,
701 in_tmux: false,
702 in_screen: false,
703 in_zellij: false,
704 kitty_keyboard: false,
705 focus_events: false,
706 bracketed_paste: false,
707 mouse_sgr: false,
708 osc52_clipboard: false,
709 },
710 }
711 }
712
713 #[must_use]
715 pub fn from_profile(profile: TerminalProfile) -> Self {
716 let mut caps = TerminalCapabilities::from_profile(profile);
717 caps.profile = TerminalProfile::Custom;
718 Self { caps }
719 }
720
721 #[must_use]
723 pub fn build(self) -> TerminalCapabilities {
724 self.caps
725 }
726
727 #[must_use]
731 pub const fn true_color(mut self, enabled: bool) -> Self {
732 self.caps.true_color = enabled;
733 self
734 }
735
736 #[must_use]
738 pub const fn colors_256(mut self, enabled: bool) -> Self {
739 self.caps.colors_256 = enabled;
740 self
741 }
742
743 #[must_use]
747 pub const fn sync_output(mut self, enabled: bool) -> Self {
748 self.caps.sync_output = enabled;
749 self
750 }
751
752 #[must_use]
754 pub const fn osc8_hyperlinks(mut self, enabled: bool) -> Self {
755 self.caps.osc8_hyperlinks = enabled;
756 self
757 }
758
759 #[must_use]
761 pub const fn scroll_region(mut self, enabled: bool) -> Self {
762 self.caps.scroll_region = enabled;
763 self
764 }
765
766 #[must_use]
770 pub const fn in_tmux(mut self, enabled: bool) -> Self {
771 self.caps.in_tmux = enabled;
772 self
773 }
774
775 #[must_use]
777 pub const fn in_screen(mut self, enabled: bool) -> Self {
778 self.caps.in_screen = enabled;
779 self
780 }
781
782 #[must_use]
784 pub const fn in_zellij(mut self, enabled: bool) -> Self {
785 self.caps.in_zellij = enabled;
786 self
787 }
788
789 #[must_use]
793 pub const fn kitty_keyboard(mut self, enabled: bool) -> Self {
794 self.caps.kitty_keyboard = enabled;
795 self
796 }
797
798 #[must_use]
800 pub const fn focus_events(mut self, enabled: bool) -> Self {
801 self.caps.focus_events = enabled;
802 self
803 }
804
805 #[must_use]
807 pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
808 self.caps.bracketed_paste = enabled;
809 self
810 }
811
812 #[must_use]
814 pub const fn mouse_sgr(mut self, enabled: bool) -> Self {
815 self.caps.mouse_sgr = enabled;
816 self
817 }
818
819 #[must_use]
823 pub const fn osc52_clipboard(mut self, enabled: bool) -> Self {
824 self.caps.osc52_clipboard = enabled;
825 self
826 }
827}
828
829impl TerminalCapabilities {
830 #[must_use]
836 pub fn detect() -> Self {
837 let value = env::var("FTUI_TEST_PROFILE").ok();
838 Self::detect_with_test_profile_override(value.as_deref())
839 }
840
841 fn detect_with_test_profile_override(value: Option<&str>) -> Self {
842 if let Some(value) = value
843 && let Ok(profile) = TerminalProfile::from_str(value.trim())
844 && profile != TerminalProfile::Detected
845 {
846 return Self::from_profile(profile);
847 }
848 let env = DetectInputs::from_env();
849 Self::detect_from_inputs(&env)
850 }
851
852 fn detect_from_inputs(env: &DetectInputs) -> Self {
853 let in_tmux = env.in_tmux;
855 let in_screen = env.in_screen;
856 let in_zellij = env.in_zellij;
857 let in_any_mux = in_tmux || in_screen || in_zellij;
858
859 let term = env.term.as_str();
860 let term_program = env.term_program.as_str();
861 let colorterm = env.colorterm.as_str();
862
863 let is_windows_terminal = env.wt_session;
865
866 let is_dumb = term == "dumb" || (term.is_empty() && !is_windows_terminal);
871
872 let is_kitty = env.kitty_window_id || term.contains("kitty");
874
875 let is_modern_terminal = MODERN_TERMINALS
877 .iter()
878 .any(|t| term_program.contains(t) || term.contains(&t.to_lowercase()))
879 || is_windows_terminal;
880
881 let true_color = !env.no_color
883 && !is_dumb
884 && (colorterm.contains("truecolor")
885 || colorterm.contains("24bit")
886 || is_modern_terminal
887 || is_kitty);
888
889 let colors_256 = !env.no_color
891 && !is_dumb
892 && (true_color || term.contains("256color") || term.contains("256"));
893
894 let sync_output = !is_dumb
896 && (is_kitty
897 || SYNC_OUTPUT_TERMINALS
898 .iter()
899 .any(|t| term_program.contains(t)));
900
901 let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
903
904 let scroll_region = !is_dumb;
906
907 let kitty_keyboard = is_kitty
909 || KITTY_KEYBOARD_TERMINALS
910 .iter()
911 .any(|t| term_program.contains(t) || term.contains(&t.to_lowercase()));
912
913 let focus_events = !is_dumb && (is_modern_terminal || is_kitty);
915
916 let bracketed_paste = !is_dumb;
918
919 let mouse_sgr = !is_dumb;
921
922 let osc52_clipboard = !is_dumb && !in_any_mux && (is_modern_terminal || is_kitty);
924
925 Self {
926 profile: TerminalProfile::Detected,
927 true_color,
928 colors_256,
929 sync_output,
930 osc8_hyperlinks,
931 scroll_region,
932 in_tmux,
933 in_screen,
934 in_zellij,
935 kitty_keyboard,
936 focus_events,
937 bracketed_paste,
938 mouse_sgr,
939 osc52_clipboard,
940 }
941 }
942
943 #[must_use]
948 pub const fn basic() -> Self {
949 Self {
950 profile: TerminalProfile::Dumb,
951 true_color: false,
952 colors_256: false,
953 sync_output: false,
954 osc8_hyperlinks: false,
955 scroll_region: false,
956 in_tmux: false,
957 in_screen: false,
958 in_zellij: false,
959 kitty_keyboard: false,
960 focus_events: false,
961 bracketed_paste: false,
962 mouse_sgr: false,
963 osc52_clipboard: false,
964 }
965 }
966
967 #[must_use]
971 #[inline]
972 pub const fn in_any_mux(&self) -> bool {
973 self.in_tmux || self.in_screen || self.in_zellij
974 }
975
976 #[must_use]
978 #[inline]
979 pub const fn has_color(&self) -> bool {
980 self.true_color || self.colors_256
981 }
982
983 #[must_use]
985 pub const fn color_depth(&self) -> &'static str {
986 if self.true_color {
987 "truecolor"
988 } else if self.colors_256 {
989 "256"
990 } else {
991 "mono"
992 }
993 }
994
995 #[must_use]
1005 #[inline]
1006 pub const fn use_sync_output(&self) -> bool {
1007 if self.in_tmux || self.in_screen || self.in_zellij {
1008 return false;
1009 }
1010 self.sync_output
1011 }
1012
1013 #[must_use]
1018 #[inline]
1019 pub const fn use_scroll_region(&self) -> bool {
1020 if self.in_tmux || self.in_screen || self.in_zellij {
1021 return false;
1022 }
1023 self.scroll_region
1024 }
1025
1026 #[must_use]
1032 #[inline]
1033 pub const fn use_hyperlinks(&self) -> bool {
1034 if self.in_tmux || self.in_screen || self.in_zellij {
1035 return false;
1036 }
1037 self.osc8_hyperlinks
1038 }
1039
1040 #[must_use]
1045 #[inline]
1046 pub const fn use_clipboard(&self) -> bool {
1047 if self.in_tmux || self.in_screen || self.in_zellij {
1048 return false;
1049 }
1050 self.osc52_clipboard
1051 }
1052
1053 #[must_use]
1059 #[inline]
1060 pub const fn needs_passthrough_wrap(&self) -> bool {
1061 self.in_tmux || self.in_screen
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 fn detect_with_override(value: Option<&str>) -> TerminalCapabilities {
1070 TerminalCapabilities::detect_with_test_profile_override(value)
1071 }
1072
1073 #[test]
1074 fn basic_is_minimal() {
1075 let caps = TerminalCapabilities::basic();
1076 assert!(!caps.true_color);
1077 assert!(!caps.colors_256);
1078 assert!(!caps.sync_output);
1079 assert!(!caps.osc8_hyperlinks);
1080 assert!(!caps.scroll_region);
1081 assert!(!caps.in_tmux);
1082 assert!(!caps.in_screen);
1083 assert!(!caps.in_zellij);
1084 assert!(!caps.kitty_keyboard);
1085 assert!(!caps.focus_events);
1086 assert!(!caps.bracketed_paste);
1087 assert!(!caps.mouse_sgr);
1088 assert!(!caps.osc52_clipboard);
1089 }
1090
1091 #[test]
1092 fn basic_is_default() {
1093 let basic = TerminalCapabilities::basic();
1094 let default = TerminalCapabilities::default();
1095 assert_eq!(basic, default);
1096 }
1097
1098 #[test]
1099 fn in_any_mux_logic() {
1100 let mut caps = TerminalCapabilities::basic();
1101 assert!(!caps.in_any_mux());
1102
1103 caps.in_tmux = true;
1104 assert!(caps.in_any_mux());
1105
1106 caps.in_tmux = false;
1107 caps.in_screen = true;
1108 assert!(caps.in_any_mux());
1109
1110 caps.in_screen = false;
1111 caps.in_zellij = true;
1112 assert!(caps.in_any_mux());
1113 }
1114
1115 #[test]
1116 fn has_color_logic() {
1117 let mut caps = TerminalCapabilities::basic();
1118 assert!(!caps.has_color());
1119
1120 caps.colors_256 = true;
1121 assert!(caps.has_color());
1122
1123 caps.colors_256 = false;
1124 caps.true_color = true;
1125 assert!(caps.has_color());
1126 }
1127
1128 #[test]
1129 fn color_depth_strings() {
1130 let mut caps = TerminalCapabilities::basic();
1131 assert_eq!(caps.color_depth(), "mono");
1132
1133 caps.colors_256 = true;
1134 assert_eq!(caps.color_depth(), "256");
1135
1136 caps.true_color = true;
1137 assert_eq!(caps.color_depth(), "truecolor");
1138 }
1139
1140 #[test]
1141 fn detect_does_not_panic() {
1142 let _caps = TerminalCapabilities::detect();
1144 }
1145
1146 #[test]
1147 fn windows_terminal_not_dumb_when_term_missing() {
1148 let env = DetectInputs {
1149 no_color: false,
1150 term: String::new(),
1151 term_program: String::new(),
1152 colorterm: String::new(),
1153 in_tmux: false,
1154 in_screen: false,
1155 in_zellij: false,
1156 kitty_window_id: false,
1157 wt_session: true,
1158 };
1159
1160 let caps = TerminalCapabilities::detect_from_inputs(&env);
1161 assert!(caps.true_color, "WT_SESSION implies true color by default");
1162 assert!(caps.colors_256, "truecolor implies 256-color");
1163 assert!(
1164 caps.osc8_hyperlinks,
1165 "WT_SESSION implies OSC 8 hyperlink support by default"
1166 );
1167 assert!(
1168 caps.bracketed_paste,
1169 "WT_SESSION should not be treated as dumb"
1170 );
1171 assert!(caps.mouse_sgr, "WT_SESSION should not be treated as dumb");
1172 }
1173
1174 #[test]
1175 #[cfg(target_os = "windows")]
1176 fn detect_windows_terminal_from_wt_session() {
1177 let mut env = make_env("", "", "");
1178 env.wt_session = true;
1179 let caps = TerminalCapabilities::detect_from_inputs(&env);
1180 assert!(caps.true_color, "WT_SESSION implies true color");
1181 assert!(caps.colors_256, "WT_SESSION implies 256-color");
1182 assert!(caps.osc8_hyperlinks, "WT_SESSION implies OSC 8 support");
1183 }
1184
1185 #[test]
1186 fn no_color_disables_color_and_links() {
1187 let env = DetectInputs {
1188 no_color: true,
1189 term: "xterm-256color".to_string(),
1190 term_program: "WezTerm".to_string(),
1191 colorterm: "truecolor".to_string(),
1192 in_tmux: false,
1193 in_screen: false,
1194 in_zellij: false,
1195 kitty_window_id: false,
1196 wt_session: false,
1197 };
1198
1199 let caps = TerminalCapabilities::detect_from_inputs(&env);
1200 assert!(!caps.true_color, "NO_COLOR must disable true color");
1201 assert!(!caps.colors_256, "NO_COLOR must disable 256-color");
1202 assert!(
1203 !caps.osc8_hyperlinks,
1204 "NO_COLOR must disable OSC 8 hyperlinks"
1205 );
1206 }
1207
1208 #[test]
1211 fn use_sync_output_disabled_in_tmux() {
1212 let mut caps = TerminalCapabilities::basic();
1213 caps.sync_output = true;
1214 assert!(caps.use_sync_output());
1215
1216 caps.in_tmux = true;
1217 assert!(!caps.use_sync_output());
1218 }
1219
1220 #[test]
1221 fn use_sync_output_disabled_in_screen() {
1222 let mut caps = TerminalCapabilities::basic();
1223 caps.sync_output = true;
1224 caps.in_screen = true;
1225 assert!(!caps.use_sync_output());
1226 }
1227
1228 #[test]
1229 fn use_sync_output_disabled_in_zellij() {
1230 let mut caps = TerminalCapabilities::basic();
1231 caps.sync_output = true;
1232 caps.in_zellij = true;
1233 assert!(!caps.use_sync_output());
1234 }
1235
1236 #[test]
1237 fn use_scroll_region_disabled_in_mux() {
1238 let mut caps = TerminalCapabilities::basic();
1239 caps.scroll_region = true;
1240 assert!(caps.use_scroll_region());
1241
1242 caps.in_tmux = true;
1243 assert!(!caps.use_scroll_region());
1244
1245 caps.in_tmux = false;
1246 caps.in_screen = true;
1247 assert!(!caps.use_scroll_region());
1248
1249 caps.in_screen = false;
1250 caps.in_zellij = true;
1251 assert!(!caps.use_scroll_region());
1252 }
1253
1254 #[test]
1255 fn use_hyperlinks_disabled_in_mux() {
1256 let mut caps = TerminalCapabilities::basic();
1257 caps.osc8_hyperlinks = true;
1258 assert!(caps.use_hyperlinks());
1259
1260 caps.in_tmux = true;
1261 assert!(!caps.use_hyperlinks());
1262 }
1263
1264 #[test]
1265 fn use_clipboard_disabled_in_mux() {
1266 let mut caps = TerminalCapabilities::basic();
1267 caps.osc52_clipboard = true;
1268 assert!(caps.use_clipboard());
1269
1270 caps.in_screen = true;
1271 assert!(!caps.use_clipboard());
1272 }
1273
1274 #[test]
1275 fn needs_passthrough_wrap_only_for_tmux_screen() {
1276 let mut caps = TerminalCapabilities::basic();
1277 assert!(!caps.needs_passthrough_wrap());
1278
1279 caps.in_tmux = true;
1280 assert!(caps.needs_passthrough_wrap());
1281
1282 caps.in_tmux = false;
1283 caps.in_screen = true;
1284 assert!(caps.needs_passthrough_wrap());
1285
1286 caps.in_screen = false;
1288 caps.in_zellij = true;
1289 assert!(!caps.needs_passthrough_wrap());
1290 }
1291
1292 #[test]
1293 fn policies_return_false_when_capability_absent() {
1294 let caps = TerminalCapabilities::basic();
1296 assert!(!caps.use_sync_output());
1297 assert!(!caps.use_scroll_region());
1298 assert!(!caps.use_hyperlinks());
1299 assert!(!caps.use_clipboard());
1300 }
1301
1302 fn make_env(term: &str, term_program: &str, colorterm: &str) -> DetectInputs {
1305 DetectInputs {
1306 no_color: false,
1307 term: term.to_string(),
1308 term_program: term_program.to_string(),
1309 colorterm: colorterm.to_string(),
1310 in_tmux: false,
1311 in_screen: false,
1312 in_zellij: false,
1313 kitty_window_id: false,
1314 wt_session: false,
1315 }
1316 }
1317
1318 #[test]
1319 fn detect_dumb_terminal() {
1320 let env = make_env("dumb", "", "");
1321 let caps = TerminalCapabilities::detect_from_inputs(&env);
1322 assert!(!caps.true_color);
1323 assert!(!caps.colors_256);
1324 assert!(!caps.sync_output);
1325 assert!(!caps.osc8_hyperlinks);
1326 assert!(!caps.scroll_region);
1327 assert!(!caps.focus_events);
1328 assert!(!caps.bracketed_paste);
1329 assert!(!caps.mouse_sgr);
1330 }
1331
1332 #[test]
1333 fn detect_empty_term_is_dumb() {
1334 let env = make_env("", "", "");
1335 let caps = TerminalCapabilities::detect_from_inputs(&env);
1336 assert!(!caps.true_color);
1337 assert!(!caps.bracketed_paste);
1338 }
1339
1340 #[test]
1341 fn detect_xterm_256color() {
1342 let env = make_env("xterm-256color", "", "");
1343 let caps = TerminalCapabilities::detect_from_inputs(&env);
1344 assert!(caps.colors_256, "xterm-256color implies 256 color");
1345 assert!(!caps.true_color, "256color alone does not imply truecolor");
1346 assert!(caps.bracketed_paste);
1347 assert!(caps.mouse_sgr);
1348 assert!(caps.scroll_region);
1349 }
1350
1351 #[test]
1352 fn detect_colorterm_truecolor() {
1353 let env = make_env("xterm-256color", "", "truecolor");
1354 let caps = TerminalCapabilities::detect_from_inputs(&env);
1355 assert!(caps.true_color, "COLORTERM=truecolor enables truecolor");
1356 assert!(caps.colors_256, "truecolor implies 256-color");
1357 }
1358
1359 #[test]
1360 fn detect_colorterm_24bit() {
1361 let env = make_env("xterm-256color", "", "24bit");
1362 let caps = TerminalCapabilities::detect_from_inputs(&env);
1363 assert!(caps.true_color, "COLORTERM=24bit enables truecolor");
1364 }
1365
1366 #[test]
1367 fn detect_kitty_by_window_id() {
1368 let mut env = make_env("xterm-kitty", "", "");
1369 env.kitty_window_id = true;
1370 let caps = TerminalCapabilities::detect_from_inputs(&env);
1371 assert!(caps.true_color, "Kitty supports truecolor");
1372 assert!(
1373 caps.kitty_keyboard,
1374 "Kitty supports kitty keyboard protocol"
1375 );
1376 assert!(caps.sync_output, "Kitty supports sync output");
1377 }
1378
1379 #[test]
1380 fn detect_kitty_by_term() {
1381 let env = make_env("xterm-kitty", "", "");
1382 let caps = TerminalCapabilities::detect_from_inputs(&env);
1383 assert!(caps.true_color, "kitty TERM implies truecolor");
1384 assert!(caps.kitty_keyboard);
1385 }
1386
1387 #[test]
1388 fn detect_wezterm() {
1389 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1390 let caps = TerminalCapabilities::detect_from_inputs(&env);
1391 assert!(caps.true_color);
1392 assert!(caps.sync_output, "WezTerm supports sync output");
1393 assert!(caps.osc8_hyperlinks, "WezTerm supports hyperlinks");
1394 assert!(caps.kitty_keyboard, "WezTerm supports kitty keyboard");
1395 assert!(caps.focus_events);
1396 assert!(caps.osc52_clipboard);
1397 }
1398
1399 #[test]
1400 #[cfg(target_os = "macos")]
1401 fn detect_iterm2_from_term_program() {
1402 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1403 let caps = TerminalCapabilities::detect_from_inputs(&env);
1404 assert!(caps.true_color, "iTerm2 implies truecolor");
1405 assert!(caps.osc8_hyperlinks, "iTerm2 supports OSC 8 hyperlinks");
1406 }
1407
1408 #[test]
1409 fn detect_alacritty() {
1410 let env = make_env("alacritty", "Alacritty", "truecolor");
1411 let caps = TerminalCapabilities::detect_from_inputs(&env);
1412 assert!(caps.true_color);
1413 assert!(caps.sync_output);
1414 assert!(caps.osc8_hyperlinks);
1415 assert!(caps.kitty_keyboard);
1416 assert!(caps.focus_events);
1417 }
1418
1419 #[test]
1420 fn detect_ghostty() {
1421 let env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1422 let caps = TerminalCapabilities::detect_from_inputs(&env);
1423 assert!(caps.true_color);
1424 assert!(caps.sync_output);
1425 assert!(caps.osc8_hyperlinks);
1426 assert!(caps.kitty_keyboard);
1427 assert!(caps.focus_events);
1428 }
1429
1430 #[test]
1431 fn detect_iterm() {
1432 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1433 let caps = TerminalCapabilities::detect_from_inputs(&env);
1434 assert!(caps.true_color);
1435 assert!(caps.osc8_hyperlinks);
1436 assert!(caps.kitty_keyboard);
1437 assert!(caps.focus_events);
1438 }
1439
1440 #[test]
1441 fn detect_vscode_terminal() {
1442 let env = make_env("xterm-256color", "vscode", "truecolor");
1443 let caps = TerminalCapabilities::detect_from_inputs(&env);
1444 assert!(caps.true_color);
1445 assert!(caps.osc8_hyperlinks);
1446 assert!(caps.focus_events);
1447 }
1448
1449 #[test]
1452 fn detect_in_tmux() {
1453 let mut env = make_env("screen-256color", "", "");
1454 env.in_tmux = true;
1455 let caps = TerminalCapabilities::detect_from_inputs(&env);
1456 assert!(caps.in_tmux);
1457 assert!(caps.in_any_mux());
1458 assert!(caps.colors_256);
1459 assert!(!caps.osc52_clipboard, "clipboard disabled in tmux");
1460 }
1461
1462 #[test]
1463 fn detect_in_screen() {
1464 let mut env = make_env("screen", "", "");
1465 env.in_screen = true;
1466 let caps = TerminalCapabilities::detect_from_inputs(&env);
1467 assert!(caps.in_screen);
1468 assert!(caps.in_any_mux());
1469 assert!(caps.needs_passthrough_wrap());
1470 }
1471
1472 #[test]
1473 fn detect_in_zellij() {
1474 let mut env = make_env("xterm-256color", "", "truecolor");
1475 env.in_zellij = true;
1476 let caps = TerminalCapabilities::detect_from_inputs(&env);
1477 assert!(caps.in_zellij);
1478 assert!(caps.in_any_mux());
1479 assert!(
1480 !caps.needs_passthrough_wrap(),
1481 "Zellij handles passthrough natively"
1482 );
1483 assert!(!caps.osc52_clipboard, "clipboard disabled in mux");
1484 }
1485
1486 #[test]
1487 fn detect_modern_terminal_in_tmux() {
1488 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
1489 env.in_tmux = true;
1490 let caps = TerminalCapabilities::detect_from_inputs(&env);
1491 assert!(caps.true_color);
1493 assert!(caps.sync_output);
1494 assert!(!caps.use_sync_output());
1496 assert!(!caps.use_hyperlinks());
1497 assert!(!caps.use_scroll_region());
1498 }
1499
1500 #[test]
1503 fn no_color_overrides_everything() {
1504 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1505 env.no_color = true;
1506 let caps = TerminalCapabilities::detect_from_inputs(&env);
1507 assert!(!caps.true_color);
1508 assert!(!caps.colors_256);
1509 assert!(!caps.osc8_hyperlinks);
1510 assert!(caps.sync_output);
1512 assert!(caps.bracketed_paste);
1513 assert!(caps.mouse_sgr);
1514 }
1515
1516 #[test]
1519 fn unknown_term_program() {
1520 let env = make_env("xterm", "SomeUnknownTerminal", "");
1521 let caps = TerminalCapabilities::detect_from_inputs(&env);
1522 assert!(
1523 !caps.true_color,
1524 "unknown terminal should not assume truecolor"
1525 );
1526 assert!(!caps.osc8_hyperlinks);
1527 assert!(caps.bracketed_paste);
1529 assert!(caps.mouse_sgr);
1530 assert!(caps.scroll_region);
1531 }
1532
1533 #[test]
1534 fn all_mux_flags_simultaneous() {
1535 let mut env = make_env("screen", "", "");
1536 env.in_tmux = true;
1537 env.in_screen = true;
1538 env.in_zellij = true;
1539 let caps = TerminalCapabilities::detect_from_inputs(&env);
1540 assert!(caps.in_any_mux());
1541 assert!(caps.needs_passthrough_wrap());
1542 assert!(!caps.use_sync_output());
1543 assert!(!caps.use_hyperlinks());
1544 assert!(!caps.use_clipboard());
1545 }
1546
1547 #[test]
1550 fn detect_rio() {
1551 let env = make_env("xterm-256color", "Rio", "truecolor");
1552 let caps = TerminalCapabilities::detect_from_inputs(&env);
1553 assert!(caps.true_color);
1554 assert!(caps.osc8_hyperlinks);
1555 assert!(caps.kitty_keyboard);
1556 assert!(caps.focus_events);
1557 }
1558
1559 #[test]
1560 fn detect_contour() {
1561 let env = make_env("xterm-256color", "Contour", "truecolor");
1562 let caps = TerminalCapabilities::detect_from_inputs(&env);
1563 assert!(caps.true_color);
1564 assert!(caps.sync_output);
1565 assert!(caps.osc8_hyperlinks);
1566 assert!(caps.focus_events);
1567 }
1568
1569 #[test]
1570 fn detect_foot() {
1571 let env = make_env("foot", "foot", "truecolor");
1572 let caps = TerminalCapabilities::detect_from_inputs(&env);
1573 assert!(caps.kitty_keyboard, "foot supports kitty keyboard");
1574 }
1575
1576 #[test]
1577 fn detect_hyper() {
1578 let env = make_env("xterm-256color", "Hyper", "truecolor");
1579 let caps = TerminalCapabilities::detect_from_inputs(&env);
1580 assert!(caps.true_color);
1581 assert!(caps.osc8_hyperlinks);
1582 assert!(caps.focus_events);
1583 }
1584
1585 #[test]
1586 fn detect_linux_console() {
1587 let env = make_env("linux", "", "");
1588 let caps = TerminalCapabilities::detect_from_inputs(&env);
1589 assert!(!caps.true_color, "linux console doesn't support truecolor");
1590 assert!(!caps.colors_256, "linux console doesn't support 256 colors");
1591 assert!(caps.bracketed_paste);
1593 assert!(caps.mouse_sgr);
1594 assert!(caps.scroll_region);
1595 }
1596
1597 #[test]
1598 fn detect_xterm_direct() {
1599 let env = make_env("xterm", "", "");
1600 let caps = TerminalCapabilities::detect_from_inputs(&env);
1601 assert!(!caps.true_color, "plain xterm has no truecolor");
1602 assert!(!caps.colors_256, "plain xterm has no 256color");
1603 assert!(caps.bracketed_paste);
1604 assert!(caps.mouse_sgr);
1605 }
1606
1607 #[test]
1608 fn detect_screen_256color() {
1609 let env = make_env("screen-256color", "", "");
1610 let caps = TerminalCapabilities::detect_from_inputs(&env);
1611 assert!(caps.colors_256, "screen-256color has 256 colors");
1612 assert!(!caps.true_color);
1613 }
1614
1615 #[test]
1618 fn wezterm_without_colorterm() {
1619 let env = make_env("xterm-256color", "WezTerm", "");
1620 let caps = TerminalCapabilities::detect_from_inputs(&env);
1621 assert!(caps.true_color, "WezTerm is modern, implies truecolor");
1623 assert!(caps.sync_output);
1624 assert!(caps.osc8_hyperlinks);
1625 }
1626
1627 #[test]
1628 fn alacritty_via_term_only() {
1629 let env = make_env("alacritty", "", "");
1631 let caps = TerminalCapabilities::detect_from_inputs(&env);
1632 assert!(caps.true_color);
1634 assert!(caps.osc8_hyperlinks);
1635 }
1636
1637 #[test]
1640 fn kitty_via_term_without_window_id() {
1641 let env = make_env("xterm-kitty", "", "");
1642 let caps = TerminalCapabilities::detect_from_inputs(&env);
1643 assert!(caps.kitty_keyboard);
1644 assert!(caps.true_color);
1645 assert!(caps.sync_output);
1646 }
1647
1648 #[test]
1649 fn kitty_window_id_with_generic_term() {
1650 let mut env = make_env("xterm-256color", "", "");
1651 env.kitty_window_id = true;
1652 let caps = TerminalCapabilities::detect_from_inputs(&env);
1653 assert!(caps.kitty_keyboard);
1654 assert!(caps.true_color);
1655 }
1656
1657 #[test]
1660 fn use_clipboard_enabled_when_no_mux_and_modern() {
1661 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1662 let caps = TerminalCapabilities::detect_from_inputs(&env);
1663 assert!(caps.osc52_clipboard);
1664 assert!(caps.use_clipboard());
1665 }
1666
1667 #[test]
1668 fn use_clipboard_disabled_in_tmux_even_if_detected() {
1669 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1670 env.in_tmux = true;
1671 let caps = TerminalCapabilities::detect_from_inputs(&env);
1672 assert!(!caps.osc52_clipboard);
1674 assert!(!caps.use_clipboard());
1675 }
1676
1677 #[test]
1678 fn scroll_region_enabled_for_basic_xterm() {
1679 let env = make_env("xterm", "", "");
1680 let caps = TerminalCapabilities::detect_from_inputs(&env);
1681 assert!(caps.scroll_region);
1682 assert!(caps.use_scroll_region());
1683 }
1684
1685 #[test]
1686 fn no_color_preserves_non_visual_features() {
1687 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1688 env.no_color = true;
1689 let caps = TerminalCapabilities::detect_from_inputs(&env);
1690 assert!(!caps.true_color);
1692 assert!(!caps.colors_256);
1693 assert!(!caps.osc8_hyperlinks);
1694 assert!(caps.sync_output);
1696 assert!(caps.kitty_keyboard);
1697 assert!(caps.focus_events);
1698 assert!(caps.bracketed_paste);
1699 assert!(caps.mouse_sgr);
1700 }
1701
1702 #[test]
1705 fn colorterm_yes_not_truecolor() {
1706 let env = make_env("xterm-256color", "", "yes");
1707 let caps = TerminalCapabilities::detect_from_inputs(&env);
1708 assert!(!caps.true_color, "COLORTERM=yes is not truecolor");
1709 assert!(caps.colors_256, "TERM=xterm-256color implies 256");
1710 }
1711
1712 #[test]
1715 fn profile_enum_as_str() {
1716 assert_eq!(TerminalProfile::Modern.as_str(), "modern");
1717 assert_eq!(TerminalProfile::Xterm256Color.as_str(), "xterm-256color");
1718 assert_eq!(TerminalProfile::Vt100.as_str(), "vt100");
1719 assert_eq!(TerminalProfile::Dumb.as_str(), "dumb");
1720 assert_eq!(TerminalProfile::Tmux.as_str(), "tmux");
1721 assert_eq!(TerminalProfile::Screen.as_str(), "screen");
1722 assert_eq!(TerminalProfile::Kitty.as_str(), "kitty");
1723 }
1724
1725 #[test]
1726 fn profile_enum_from_str() {
1727 use std::str::FromStr;
1728 assert_eq!(
1729 TerminalProfile::from_str("modern"),
1730 Ok(TerminalProfile::Modern)
1731 );
1732 assert_eq!(
1733 TerminalProfile::from_str("xterm-256color"),
1734 Ok(TerminalProfile::Xterm256Color)
1735 );
1736 assert_eq!(
1737 TerminalProfile::from_str("xterm256color"),
1738 Ok(TerminalProfile::Xterm256Color)
1739 );
1740 assert_eq!(TerminalProfile::from_str("DUMB"), Ok(TerminalProfile::Dumb));
1741 assert!(TerminalProfile::from_str("unknown").is_err());
1742 }
1743
1744 #[test]
1745 fn profile_all_predefined() {
1746 let all = TerminalProfile::all_predefined();
1747 assert!(all.len() >= 10);
1748 assert!(all.contains(&TerminalProfile::Modern));
1749 assert!(all.contains(&TerminalProfile::Dumb));
1750 assert!(!all.contains(&TerminalProfile::Custom));
1751 assert!(!all.contains(&TerminalProfile::Detected));
1752 }
1753
1754 #[test]
1755 fn profile_modern_has_all_features() {
1756 let caps = TerminalCapabilities::modern();
1757 assert_eq!(caps.profile(), TerminalProfile::Modern);
1758 assert_eq!(caps.profile_name(), Some("modern"));
1759 assert!(caps.true_color);
1760 assert!(caps.colors_256);
1761 assert!(caps.sync_output);
1762 assert!(caps.osc8_hyperlinks);
1763 assert!(caps.scroll_region);
1764 assert!(caps.kitty_keyboard);
1765 assert!(caps.focus_events);
1766 assert!(caps.bracketed_paste);
1767 assert!(caps.mouse_sgr);
1768 assert!(caps.osc52_clipboard);
1769 assert!(!caps.in_any_mux());
1770 }
1771
1772 #[test]
1773 fn profile_xterm_256color() {
1774 let caps = TerminalCapabilities::xterm_256color();
1775 assert_eq!(caps.profile(), TerminalProfile::Xterm256Color);
1776 assert!(!caps.true_color);
1777 assert!(caps.colors_256);
1778 assert!(!caps.sync_output);
1779 assert!(!caps.osc8_hyperlinks);
1780 assert!(caps.scroll_region);
1781 assert!(caps.bracketed_paste);
1782 assert!(caps.mouse_sgr);
1783 }
1784
1785 #[test]
1786 fn profile_xterm_basic() {
1787 let caps = TerminalCapabilities::xterm();
1788 assert_eq!(caps.profile(), TerminalProfile::Xterm);
1789 assert!(!caps.true_color);
1790 assert!(!caps.colors_256);
1791 assert!(caps.scroll_region);
1792 }
1793
1794 #[test]
1795 fn profile_vt100_minimal() {
1796 let caps = TerminalCapabilities::vt100();
1797 assert_eq!(caps.profile(), TerminalProfile::Vt100);
1798 assert!(!caps.true_color);
1799 assert!(!caps.colors_256);
1800 assert!(caps.scroll_region);
1801 assert!(!caps.bracketed_paste);
1802 assert!(!caps.mouse_sgr);
1803 }
1804
1805 #[test]
1806 fn profile_dumb_no_features() {
1807 let caps = TerminalCapabilities::dumb();
1808 assert_eq!(caps.profile(), TerminalProfile::Dumb);
1809 assert!(!caps.true_color);
1810 assert!(!caps.colors_256);
1811 assert!(!caps.scroll_region);
1812 assert!(!caps.bracketed_paste);
1813 assert!(!caps.mouse_sgr);
1814 assert!(!caps.use_sync_output());
1815 assert!(!caps.use_scroll_region());
1816 }
1817
1818 #[test]
1819 fn profile_tmux_mux_flags() {
1820 let caps = TerminalCapabilities::tmux();
1821 assert_eq!(caps.profile(), TerminalProfile::Tmux);
1822 assert!(caps.in_tmux);
1823 assert!(!caps.in_screen);
1824 assert!(!caps.in_zellij);
1825 assert!(caps.in_any_mux());
1826 assert!(!caps.use_sync_output());
1828 assert!(!caps.use_scroll_region());
1829 assert!(!caps.use_hyperlinks());
1830 }
1831
1832 #[test]
1833 fn profile_screen_mux_flags() {
1834 let caps = TerminalCapabilities::screen();
1835 assert_eq!(caps.profile(), TerminalProfile::Screen);
1836 assert!(!caps.in_tmux);
1837 assert!(caps.in_screen);
1838 assert!(caps.in_any_mux());
1839 assert!(caps.needs_passthrough_wrap());
1840 }
1841
1842 #[test]
1843 fn profile_zellij_mux_flags() {
1844 let caps = TerminalCapabilities::zellij();
1845 assert_eq!(caps.profile(), TerminalProfile::Zellij);
1846 assert!(caps.in_zellij);
1847 assert!(caps.in_any_mux());
1848 assert!(caps.true_color);
1850 assert!(caps.focus_events);
1851 assert!(!caps.needs_passthrough_wrap());
1853 }
1854
1855 #[test]
1856 fn profile_kitty_full_features() {
1857 let caps = TerminalCapabilities::kitty();
1858 assert_eq!(caps.profile(), TerminalProfile::Kitty);
1859 assert!(caps.true_color);
1860 assert!(caps.sync_output);
1861 assert!(caps.kitty_keyboard);
1862 assert!(caps.osc8_hyperlinks);
1863 }
1864
1865 #[test]
1866 fn profile_windows_console() {
1867 let caps = TerminalCapabilities::windows_console();
1868 assert_eq!(caps.profile(), TerminalProfile::WindowsConsole);
1869 assert!(caps.true_color);
1870 assert!(caps.osc8_hyperlinks);
1871 assert!(caps.focus_events);
1872 }
1873
1874 #[test]
1875 fn profile_linux_console() {
1876 let caps = TerminalCapabilities::linux_console();
1877 assert_eq!(caps.profile(), TerminalProfile::LinuxConsole);
1878 assert!(!caps.true_color);
1879 assert!(!caps.colors_256);
1880 assert!(caps.scroll_region);
1881 }
1882
1883 #[test]
1884 fn from_profile_roundtrip() {
1885 for profile in TerminalProfile::all_predefined() {
1886 let caps = TerminalCapabilities::from_profile(*profile);
1887 assert_eq!(caps.profile(), *profile);
1888 }
1889 }
1890
1891 #[test]
1892 fn detected_profile_has_none_name() {
1893 let caps = detect_with_override(None);
1894 assert_eq!(caps.profile(), TerminalProfile::Detected);
1895 assert_eq!(caps.profile_name(), None);
1896 }
1897
1898 #[test]
1899 fn detect_respects_test_profile_env() {
1900 let caps = detect_with_override(Some("dumb"));
1901 assert_eq!(caps.profile(), TerminalProfile::Dumb);
1902 }
1903
1904 #[test]
1905 fn detect_ignores_invalid_test_profile() {
1906 let caps = detect_with_override(Some("not-a-real-profile"));
1907 assert_eq!(caps.profile(), TerminalProfile::Detected);
1908 }
1909
1910 #[test]
1911 fn basic_has_dumb_profile() {
1912 let caps = TerminalCapabilities::basic();
1913 assert_eq!(caps.profile(), TerminalProfile::Dumb);
1914 }
1915
1916 #[test]
1919 fn builder_starts_empty() {
1920 let caps = CapabilityProfileBuilder::new().build();
1921 assert_eq!(caps.profile(), TerminalProfile::Custom);
1922 assert!(!caps.true_color);
1923 assert!(!caps.colors_256);
1924 assert!(!caps.sync_output);
1925 assert!(!caps.scroll_region);
1926 assert!(!caps.mouse_sgr);
1927 }
1928
1929 #[test]
1930 fn builder_set_colors() {
1931 let caps = CapabilityProfileBuilder::new()
1932 .true_color(true)
1933 .colors_256(true)
1934 .build();
1935 assert!(caps.true_color);
1936 assert!(caps.colors_256);
1937 }
1938
1939 #[test]
1940 fn builder_set_advanced() {
1941 let caps = CapabilityProfileBuilder::new()
1942 .sync_output(true)
1943 .osc8_hyperlinks(true)
1944 .scroll_region(true)
1945 .build();
1946 assert!(caps.sync_output);
1947 assert!(caps.osc8_hyperlinks);
1948 assert!(caps.scroll_region);
1949 }
1950
1951 #[test]
1952 fn builder_set_mux() {
1953 let caps = CapabilityProfileBuilder::new()
1954 .in_tmux(true)
1955 .in_screen(false)
1956 .in_zellij(false)
1957 .build();
1958 assert!(caps.in_tmux);
1959 assert!(!caps.in_screen);
1960 assert!(caps.in_any_mux());
1961 }
1962
1963 #[test]
1964 fn builder_set_input() {
1965 let caps = CapabilityProfileBuilder::new()
1966 .kitty_keyboard(true)
1967 .focus_events(true)
1968 .bracketed_paste(true)
1969 .mouse_sgr(true)
1970 .build();
1971 assert!(caps.kitty_keyboard);
1972 assert!(caps.focus_events);
1973 assert!(caps.bracketed_paste);
1974 assert!(caps.mouse_sgr);
1975 }
1976
1977 #[test]
1978 fn builder_set_clipboard() {
1979 let caps = CapabilityProfileBuilder::new()
1980 .osc52_clipboard(true)
1981 .build();
1982 assert!(caps.osc52_clipboard);
1983 }
1984
1985 #[test]
1986 fn builder_from_profile() {
1987 let caps = CapabilityProfileBuilder::from_profile(TerminalProfile::Modern)
1988 .sync_output(false) .build();
1990 assert!(caps.true_color);
1992 assert!(caps.colors_256);
1993 assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
1995 assert_eq!(caps.profile(), TerminalProfile::Custom);
1997 }
1998
1999 #[test]
2000 fn builder_chain_multiple() {
2001 let caps = TerminalCapabilities::builder()
2002 .colors_256(true)
2003 .bracketed_paste(true)
2004 .mouse_sgr(true)
2005 .scroll_region(true)
2006 .build();
2007 assert!(caps.colors_256);
2008 assert!(caps.bracketed_paste);
2009 assert!(caps.mouse_sgr);
2010 assert!(caps.scroll_region);
2011 assert!(!caps.true_color);
2012 assert!(!caps.sync_output);
2013 }
2014
2015 #[test]
2016 fn builder_default() {
2017 let builder = CapabilityProfileBuilder::default();
2018 let caps = builder.build();
2019 assert_eq!(caps.profile(), TerminalProfile::Custom);
2020 }
2021
2022 #[test]
2031 fn mux_compatibility_matrix() {
2032 {
2037 let caps = TerminalCapabilities::modern();
2038 assert!(
2039 caps.use_sync_output(),
2040 "baseline: sync_output should be enabled"
2041 );
2042 assert!(
2043 caps.use_scroll_region(),
2044 "baseline: scroll_region should be enabled"
2045 );
2046 assert!(
2047 caps.use_hyperlinks(),
2048 "baseline: hyperlinks should be enabled"
2049 );
2050 assert!(!caps.needs_passthrough_wrap(), "baseline: no wrap needed");
2051 }
2052
2053 {
2055 let caps = TerminalCapabilities::tmux();
2056 assert!(!caps.use_sync_output(), "tmux: sync_output disabled");
2057 assert!(!caps.use_scroll_region(), "tmux: scroll_region disabled");
2058 assert!(!caps.use_hyperlinks(), "tmux: hyperlinks disabled");
2059 assert!(caps.needs_passthrough_wrap(), "tmux: needs wrap");
2060 }
2061
2062 {
2064 let caps = TerminalCapabilities::screen();
2065 assert!(!caps.use_sync_output(), "screen: sync_output disabled");
2066 assert!(!caps.use_scroll_region(), "screen: scroll_region disabled");
2067 assert!(!caps.use_hyperlinks(), "screen: hyperlinks disabled");
2068 assert!(caps.needs_passthrough_wrap(), "screen: needs wrap");
2069 }
2070
2071 {
2073 let caps = TerminalCapabilities::zellij();
2074 assert!(!caps.use_sync_output(), "zellij: sync_output disabled");
2075 assert!(!caps.use_scroll_region(), "zellij: scroll_region disabled");
2076 assert!(!caps.use_hyperlinks(), "zellij: hyperlinks disabled");
2077 assert!(
2078 !caps.needs_passthrough_wrap(),
2079 "zellij: no wrap needed (native passthrough)"
2080 );
2081 }
2082 }
2083
2084 #[test]
2086 fn modern_terminal_in_mux_matrix() {
2087 for (mux_name, in_tmux, in_screen, in_zellij) in [
2091 ("tmux", true, false, false),
2092 ("screen", false, true, false),
2093 ("zellij", false, false, true),
2094 ] {
2095 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2096 env.in_tmux = in_tmux;
2097 env.in_screen = in_screen;
2098 env.in_zellij = in_zellij;
2099 let caps = TerminalCapabilities::detect_from_inputs(&env);
2100
2101 assert!(
2103 caps.true_color,
2104 "{mux_name}: true_color detection should work"
2105 );
2106 assert!(
2107 caps.sync_output,
2108 "{mux_name}: sync_output detection should work"
2109 );
2110
2111 assert!(
2113 !caps.use_sync_output(),
2114 "{mux_name}: use_sync_output() should be false"
2115 );
2116 assert!(
2117 !caps.use_scroll_region(),
2118 "{mux_name}: use_scroll_region() should be false"
2119 );
2120 assert!(
2121 !caps.use_hyperlinks(),
2122 "{mux_name}: use_hyperlinks() should be false"
2123 );
2124 }
2125 }
2126
2127 #[test]
2129 fn profile_mux_invariant_matrix() {
2130 for profile in TerminalProfile::all_predefined() {
2132 let caps = TerminalCapabilities::from_profile(*profile);
2133 let name = profile.as_str();
2134
2135 let expected_mux = caps.in_tmux || caps.in_screen || caps.in_zellij;
2137 assert_eq!(
2138 caps.in_any_mux(),
2139 expected_mux,
2140 "{name}: in_any_mux() should match individual flags"
2141 );
2142
2143 if caps.in_any_mux() {
2145 assert!(
2146 !caps.use_sync_output(),
2147 "{name}: mux should disable use_sync_output()"
2148 );
2149 assert!(
2150 !caps.use_scroll_region(),
2151 "{name}: mux should disable use_scroll_region()"
2152 );
2153 assert!(
2154 !caps.use_hyperlinks(),
2155 "{name}: mux should disable use_hyperlinks()"
2156 );
2157 }
2158
2159 if caps.in_tmux || caps.in_screen {
2161 assert!(
2162 caps.needs_passthrough_wrap(),
2163 "{name}: tmux/screen should need passthrough wrap"
2164 );
2165 } else if caps.in_zellij {
2166 assert!(
2167 !caps.needs_passthrough_wrap(),
2168 "{name}: zellij should NOT need passthrough wrap"
2169 );
2170 }
2171 }
2172 }
2173
2174 #[test]
2176 fn fallback_ordering_matrix() {
2177 use crate::inline_mode::InlineStrategy;
2178
2179 let caps_full = TerminalCapabilities::builder()
2181 .sync_output(true)
2182 .scroll_region(true)
2183 .build();
2184 assert_eq!(
2185 InlineStrategy::select(&caps_full),
2186 InlineStrategy::ScrollRegion,
2187 "full capabilities should use ScrollRegion"
2188 );
2189
2190 let caps_hybrid = TerminalCapabilities::builder()
2192 .sync_output(false)
2193 .scroll_region(true)
2194 .build();
2195 assert_eq!(
2196 InlineStrategy::select(&caps_hybrid),
2197 InlineStrategy::Hybrid,
2198 "scroll without sync should use Hybrid"
2199 );
2200
2201 let caps_none = TerminalCapabilities::builder()
2203 .sync_output(false)
2204 .scroll_region(false)
2205 .build();
2206 assert_eq!(
2207 InlineStrategy::select(&caps_none),
2208 InlineStrategy::OverlayRedraw,
2209 "no capabilities should use OverlayRedraw"
2210 );
2211
2212 let caps_tmux = TerminalCapabilities::tmux();
2214 assert_eq!(
2215 InlineStrategy::select(&caps_tmux),
2216 InlineStrategy::OverlayRedraw,
2217 "tmux should force OverlayRedraw"
2218 );
2219 }
2220
2221 #[test]
2223 fn terminal_mux_strategy_matrix() {
2224 use crate::inline_mode::InlineStrategy;
2225
2226 struct TestCase {
2227 name: &'static str,
2228 profile: TerminalProfile,
2229 expected: InlineStrategy,
2230 }
2231
2232 let cases = [
2233 TestCase {
2234 name: "modern (no mux)",
2235 profile: TerminalProfile::Modern,
2236 expected: InlineStrategy::ScrollRegion,
2237 },
2238 TestCase {
2239 name: "kitty (no mux)",
2240 profile: TerminalProfile::Kitty,
2241 expected: InlineStrategy::ScrollRegion,
2242 },
2243 TestCase {
2244 name: "xterm-256color (no mux)",
2245 profile: TerminalProfile::Xterm256Color,
2246 expected: InlineStrategy::Hybrid, },
2248 TestCase {
2249 name: "xterm (no mux)",
2250 profile: TerminalProfile::Xterm,
2251 expected: InlineStrategy::Hybrid,
2252 },
2253 TestCase {
2254 name: "vt100 (no mux)",
2255 profile: TerminalProfile::Vt100,
2256 expected: InlineStrategy::Hybrid,
2257 },
2258 TestCase {
2259 name: "dumb",
2260 profile: TerminalProfile::Dumb,
2261 expected: InlineStrategy::OverlayRedraw, },
2263 TestCase {
2264 name: "tmux",
2265 profile: TerminalProfile::Tmux,
2266 expected: InlineStrategy::OverlayRedraw,
2267 },
2268 TestCase {
2269 name: "screen",
2270 profile: TerminalProfile::Screen,
2271 expected: InlineStrategy::OverlayRedraw,
2272 },
2273 TestCase {
2274 name: "zellij",
2275 profile: TerminalProfile::Zellij,
2276 expected: InlineStrategy::OverlayRedraw,
2277 },
2278 ];
2279
2280 for case in cases {
2281 let caps = TerminalCapabilities::from_profile(case.profile);
2282 let actual = InlineStrategy::select(&caps);
2283 assert_eq!(
2284 actual, case.expected,
2285 "{}: expected {:?}, got {:?}",
2286 case.name, case.expected, actual
2287 );
2288 }
2289 }
2290}
2291
2292#[cfg(test)]
2297mod proptests {
2298 use super::*;
2299 use proptest::prelude::*;
2300
2301 proptest! {
2302 #[test]
2304 fn prop_in_any_mux_consistent(
2305 in_tmux in any::<bool>(),
2306 in_screen in any::<bool>(),
2307 in_zellij in any::<bool>(),
2308 ) {
2309 let caps = TerminalCapabilities::builder()
2310 .in_tmux(in_tmux)
2311 .in_screen(in_screen)
2312 .in_zellij(in_zellij)
2313 .build();
2314
2315 let expected = in_tmux || in_screen || in_zellij;
2316 prop_assert_eq!(caps.in_any_mux(), expected);
2317 }
2318
2319 #[test]
2321 fn prop_mux_disables_sync_output(
2322 in_tmux in any::<bool>(),
2323 in_screen in any::<bool>(),
2324 in_zellij in any::<bool>(),
2325 sync_output in any::<bool>(),
2326 ) {
2327 let caps = TerminalCapabilities::builder()
2328 .in_tmux(in_tmux)
2329 .in_screen(in_screen)
2330 .in_zellij(in_zellij)
2331 .sync_output(sync_output)
2332 .build();
2333
2334 if caps.in_any_mux() {
2335 prop_assert!(!caps.use_sync_output(), "mux should disable sync_output policy");
2336 }
2337 }
2338
2339 #[test]
2341 fn prop_mux_disables_scroll_region(
2342 in_tmux in any::<bool>(),
2343 in_screen in any::<bool>(),
2344 in_zellij in any::<bool>(),
2345 scroll_region in any::<bool>(),
2346 ) {
2347 let caps = TerminalCapabilities::builder()
2348 .in_tmux(in_tmux)
2349 .in_screen(in_screen)
2350 .in_zellij(in_zellij)
2351 .scroll_region(scroll_region)
2352 .build();
2353
2354 if caps.in_any_mux() {
2355 prop_assert!(!caps.use_scroll_region(), "mux should disable scroll_region policy");
2356 }
2357 }
2358
2359 #[test]
2361 fn prop_mux_disables_hyperlinks(
2362 in_tmux in any::<bool>(),
2363 in_screen in any::<bool>(),
2364 in_zellij in any::<bool>(),
2365 osc8_hyperlinks in any::<bool>(),
2366 ) {
2367 let caps = TerminalCapabilities::builder()
2368 .in_tmux(in_tmux)
2369 .in_screen(in_screen)
2370 .in_zellij(in_zellij)
2371 .osc8_hyperlinks(osc8_hyperlinks)
2372 .build();
2373
2374 if caps.in_any_mux() {
2375 prop_assert!(!caps.use_hyperlinks(), "mux should disable hyperlinks policy");
2376 }
2377 }
2378
2379 #[test]
2381 fn prop_passthrough_wrap_logic(
2382 in_tmux in any::<bool>(),
2383 in_screen in any::<bool>(),
2384 in_zellij in any::<bool>(),
2385 ) {
2386 let caps = TerminalCapabilities::builder()
2387 .in_tmux(in_tmux)
2388 .in_screen(in_screen)
2389 .in_zellij(in_zellij)
2390 .build();
2391
2392 let expected = in_tmux || in_screen; prop_assert_eq!(caps.needs_passthrough_wrap(), expected);
2394 }
2395
2396 #[test]
2398 fn prop_policy_false_when_capability_off(
2399 in_tmux in any::<bool>(),
2400 in_screen in any::<bool>(),
2401 in_zellij in any::<bool>(),
2402 ) {
2403 let caps = TerminalCapabilities::builder()
2404 .in_tmux(in_tmux)
2405 .in_screen(in_screen)
2406 .in_zellij(in_zellij)
2407 .sync_output(false)
2408 .scroll_region(false)
2409 .osc8_hyperlinks(false)
2410 .osc52_clipboard(false)
2411 .build();
2412
2413 prop_assert!(!caps.use_sync_output(), "sync_output=false implies use_sync_output()=false");
2414 prop_assert!(!caps.use_scroll_region(), "scroll_region=false implies use_scroll_region()=false");
2415 prop_assert!(!caps.use_hyperlinks(), "osc8_hyperlinks=false implies use_hyperlinks()=false");
2416 prop_assert!(!caps.use_clipboard(), "osc52_clipboard=false implies use_clipboard()=false");
2417 }
2418
2419 #[test]
2421 fn prop_no_color_preserves_non_visual(no_color in any::<bool>()) {
2422 let env = DetectInputs {
2423 no_color,
2424 term: "xterm-256color".to_string(),
2425 term_program: "WezTerm".to_string(),
2426 colorterm: "truecolor".to_string(),
2427 in_tmux: false,
2428 in_screen: false,
2429 in_zellij: false,
2430 kitty_window_id: false,
2431 wt_session: false,
2432 };
2433 let caps = TerminalCapabilities::detect_from_inputs(&env);
2434
2435 if no_color {
2436 prop_assert!(!caps.true_color, "NO_COLOR disables true_color");
2437 prop_assert!(!caps.colors_256, "NO_COLOR disables colors_256");
2438 prop_assert!(!caps.osc8_hyperlinks, "NO_COLOR disables hyperlinks");
2439 }
2440
2441 prop_assert!(caps.sync_output, "sync_output preserved despite NO_COLOR");
2443 prop_assert!(caps.bracketed_paste, "bracketed_paste preserved despite NO_COLOR");
2444 }
2445 }
2446}