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 #[must_use]
690 pub fn builder() -> CapabilityProfileBuilder {
691 CapabilityProfileBuilder::new()
692 }
693}
694
695#[derive(Debug, Clone)]
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 #[must_use]
733 pub fn new() -> Self {
734 Self {
735 caps: TerminalCapabilities {
736 profile: TerminalProfile::Custom,
737 true_color: false,
738 colors_256: false,
739 unicode_box_drawing: false,
740 unicode_emoji: false,
741 double_width: false,
742 sync_output: false,
743 osc8_hyperlinks: false,
744 scroll_region: false,
745 in_tmux: false,
746 in_screen: false,
747 in_zellij: false,
748 kitty_keyboard: false,
749 focus_events: false,
750 bracketed_paste: false,
751 mouse_sgr: false,
752 osc52_clipboard: false,
753 },
754 }
755 }
756
757 #[must_use]
759 pub fn from_profile(profile: TerminalProfile) -> Self {
760 let mut caps = TerminalCapabilities::from_profile(profile);
761 caps.profile = TerminalProfile::Custom;
762 Self { caps }
763 }
764
765 #[must_use]
767 pub fn build(self) -> TerminalCapabilities {
768 self.caps
769 }
770
771 #[must_use]
775 pub const fn true_color(mut self, enabled: bool) -> Self {
776 self.caps.true_color = enabled;
777 self
778 }
779
780 #[must_use]
782 pub const fn colors_256(mut self, enabled: bool) -> Self {
783 self.caps.colors_256 = enabled;
784 self
785 }
786
787 #[must_use]
791 pub const fn sync_output(mut self, enabled: bool) -> Self {
792 self.caps.sync_output = enabled;
793 self
794 }
795
796 #[must_use]
798 pub const fn osc8_hyperlinks(mut self, enabled: bool) -> Self {
799 self.caps.osc8_hyperlinks = enabled;
800 self
801 }
802
803 #[must_use]
805 pub const fn scroll_region(mut self, enabled: bool) -> Self {
806 self.caps.scroll_region = enabled;
807 self
808 }
809
810 #[must_use]
814 pub const fn in_tmux(mut self, enabled: bool) -> Self {
815 self.caps.in_tmux = enabled;
816 self
817 }
818
819 #[must_use]
821 pub const fn in_screen(mut self, enabled: bool) -> Self {
822 self.caps.in_screen = enabled;
823 self
824 }
825
826 #[must_use]
828 pub const fn in_zellij(mut self, enabled: bool) -> Self {
829 self.caps.in_zellij = enabled;
830 self
831 }
832
833 #[must_use]
837 pub const fn kitty_keyboard(mut self, enabled: bool) -> Self {
838 self.caps.kitty_keyboard = enabled;
839 self
840 }
841
842 #[must_use]
844 pub const fn focus_events(mut self, enabled: bool) -> Self {
845 self.caps.focus_events = enabled;
846 self
847 }
848
849 #[must_use]
851 pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
852 self.caps.bracketed_paste = enabled;
853 self
854 }
855
856 #[must_use]
858 pub const fn mouse_sgr(mut self, enabled: bool) -> Self {
859 self.caps.mouse_sgr = enabled;
860 self
861 }
862
863 #[must_use]
867 pub const fn osc52_clipboard(mut self, enabled: bool) -> Self {
868 self.caps.osc52_clipboard = enabled;
869 self
870 }
871}
872
873impl TerminalCapabilities {
874 #[must_use]
880 pub fn detect() -> Self {
881 let value = env::var("FTUI_TEST_PROFILE").ok();
882 Self::detect_with_test_profile_override(value.as_deref())
883 }
884
885 fn detect_with_test_profile_override(value: Option<&str>) -> Self {
886 if let Some(value) = value
887 && let Ok(profile) = TerminalProfile::from_str(value.trim())
888 && profile != TerminalProfile::Detected
889 {
890 return Self::from_profile(profile);
891 }
892 let env = DetectInputs::from_env();
893 Self::detect_from_inputs(&env)
894 }
895
896 fn detect_from_inputs(env: &DetectInputs) -> Self {
897 let in_tmux = env.in_tmux;
899 let in_screen = env.in_screen;
900 let in_zellij = env.in_zellij;
901 let in_any_mux = in_tmux || in_screen || in_zellij;
902
903 let term = env.term.as_str();
904 let term_program = env.term_program.as_str();
905 let colorterm = env.colorterm.as_str();
906
907 let is_windows_terminal = env.wt_session;
909
910 let is_dumb = term == "dumb" || (term.is_empty() && !is_windows_terminal);
915
916 let is_kitty = env.kitty_window_id || term.contains("kitty");
918
919 let is_modern_terminal = MODERN_TERMINALS
921 .iter()
922 .any(|t| term_program.contains(t) || term.contains(&t.to_lowercase()))
923 || is_windows_terminal;
924
925 let true_color = !env.no_color
927 && !is_dumb
928 && (colorterm.contains("truecolor")
929 || colorterm.contains("24bit")
930 || is_modern_terminal
931 || is_kitty);
932
933 let colors_256 = !env.no_color
935 && !is_dumb
936 && (true_color || term.contains("256color") || term.contains("256"));
937
938 let sync_output = !is_dumb
940 && (is_kitty
941 || SYNC_OUTPUT_TERMINALS
942 .iter()
943 .any(|t| term_program.contains(t)));
944
945 let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
947
948 let scroll_region = !is_dumb;
950
951 let kitty_keyboard = is_kitty
953 || KITTY_KEYBOARD_TERMINALS
954 .iter()
955 .any(|t| term_program.contains(t) || term.contains(&t.to_lowercase()));
956
957 let focus_events = !is_dumb && (is_modern_terminal || is_kitty);
959
960 let bracketed_paste = !is_dumb;
962
963 let mouse_sgr = !is_dumb;
965
966 let osc52_clipboard = !is_dumb && !in_any_mux && (is_modern_terminal || is_kitty);
968
969 let unicode_box_drawing = !is_dumb;
971 let unicode_emoji = !is_dumb && (is_modern_terminal || is_kitty);
972 let double_width = !is_dumb;
973
974 Self {
975 profile: TerminalProfile::Detected,
976 true_color,
977 colors_256,
978 unicode_box_drawing,
979 unicode_emoji,
980 double_width,
981 sync_output,
982 osc8_hyperlinks,
983 scroll_region,
984 in_tmux,
985 in_screen,
986 in_zellij,
987 kitty_keyboard,
988 focus_events,
989 bracketed_paste,
990 mouse_sgr,
991 osc52_clipboard,
992 }
993 }
994
995 #[must_use]
1000 pub const fn basic() -> Self {
1001 Self {
1002 profile: TerminalProfile::Dumb,
1003 true_color: false,
1004 colors_256: false,
1005 unicode_box_drawing: false,
1006 unicode_emoji: false,
1007 double_width: false,
1008 sync_output: false,
1009 osc8_hyperlinks: false,
1010 scroll_region: false,
1011 in_tmux: false,
1012 in_screen: false,
1013 in_zellij: false,
1014 kitty_keyboard: false,
1015 focus_events: false,
1016 bracketed_paste: false,
1017 mouse_sgr: false,
1018 osc52_clipboard: false,
1019 }
1020 }
1021
1022 #[must_use]
1026 #[inline]
1027 pub const fn in_any_mux(&self) -> bool {
1028 self.in_tmux || self.in_screen || self.in_zellij
1029 }
1030
1031 #[must_use]
1033 #[inline]
1034 pub const fn has_color(&self) -> bool {
1035 self.true_color || self.colors_256
1036 }
1037
1038 #[must_use]
1040 pub const fn color_depth(&self) -> &'static str {
1041 if self.true_color {
1042 "truecolor"
1043 } else if self.colors_256 {
1044 "256"
1045 } else {
1046 "mono"
1047 }
1048 }
1049
1050 #[must_use]
1060 #[inline]
1061 pub const fn use_sync_output(&self) -> bool {
1062 if self.in_tmux || self.in_screen || self.in_zellij {
1063 return false;
1064 }
1065 self.sync_output
1066 }
1067
1068 #[must_use]
1073 #[inline]
1074 pub const fn use_scroll_region(&self) -> bool {
1075 if self.in_tmux || self.in_screen || self.in_zellij {
1076 return false;
1077 }
1078 self.scroll_region
1079 }
1080
1081 #[must_use]
1087 #[inline]
1088 pub const fn use_hyperlinks(&self) -> bool {
1089 if self.in_tmux || self.in_screen || self.in_zellij {
1090 return false;
1091 }
1092 self.osc8_hyperlinks
1093 }
1094
1095 #[must_use]
1100 #[inline]
1101 pub const fn use_clipboard(&self) -> bool {
1102 if self.in_tmux || self.in_screen || self.in_zellij {
1103 return false;
1104 }
1105 self.osc52_clipboard
1106 }
1107
1108 #[must_use]
1114 #[inline]
1115 pub const fn needs_passthrough_wrap(&self) -> bool {
1116 self.in_tmux || self.in_screen
1117 }
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122 use super::*;
1123
1124 fn detect_with_override(value: Option<&str>) -> TerminalCapabilities {
1125 TerminalCapabilities::detect_with_test_profile_override(value)
1126 }
1127
1128 #[test]
1129 fn basic_is_minimal() {
1130 let caps = TerminalCapabilities::basic();
1131 assert!(!caps.true_color);
1132 assert!(!caps.colors_256);
1133 assert!(!caps.sync_output);
1134 assert!(!caps.osc8_hyperlinks);
1135 assert!(!caps.scroll_region);
1136 assert!(!caps.in_tmux);
1137 assert!(!caps.in_screen);
1138 assert!(!caps.in_zellij);
1139 assert!(!caps.kitty_keyboard);
1140 assert!(!caps.focus_events);
1141 assert!(!caps.bracketed_paste);
1142 assert!(!caps.mouse_sgr);
1143 assert!(!caps.osc52_clipboard);
1144 }
1145
1146 #[test]
1147 fn basic_is_default() {
1148 let basic = TerminalCapabilities::basic();
1149 let default = TerminalCapabilities::default();
1150 assert_eq!(basic, default);
1151 }
1152
1153 #[test]
1154 fn in_any_mux_logic() {
1155 let mut caps = TerminalCapabilities::basic();
1156 assert!(!caps.in_any_mux());
1157
1158 caps.in_tmux = true;
1159 assert!(caps.in_any_mux());
1160
1161 caps.in_tmux = false;
1162 caps.in_screen = true;
1163 assert!(caps.in_any_mux());
1164
1165 caps.in_screen = false;
1166 caps.in_zellij = true;
1167 assert!(caps.in_any_mux());
1168 }
1169
1170 #[test]
1171 fn has_color_logic() {
1172 let mut caps = TerminalCapabilities::basic();
1173 assert!(!caps.has_color());
1174
1175 caps.colors_256 = true;
1176 assert!(caps.has_color());
1177
1178 caps.colors_256 = false;
1179 caps.true_color = true;
1180 assert!(caps.has_color());
1181 }
1182
1183 #[test]
1184 fn color_depth_strings() {
1185 let mut caps = TerminalCapabilities::basic();
1186 assert_eq!(caps.color_depth(), "mono");
1187
1188 caps.colors_256 = true;
1189 assert_eq!(caps.color_depth(), "256");
1190
1191 caps.true_color = true;
1192 assert_eq!(caps.color_depth(), "truecolor");
1193 }
1194
1195 #[test]
1196 fn detect_does_not_panic() {
1197 let _caps = TerminalCapabilities::detect();
1199 }
1200
1201 #[test]
1202 fn windows_terminal_not_dumb_when_term_missing() {
1203 let env = DetectInputs {
1204 no_color: false,
1205 term: String::new(),
1206 term_program: String::new(),
1207 colorterm: String::new(),
1208 in_tmux: false,
1209 in_screen: false,
1210 in_zellij: false,
1211 kitty_window_id: false,
1212 wt_session: true,
1213 };
1214
1215 let caps = TerminalCapabilities::detect_from_inputs(&env);
1216 assert!(caps.true_color, "WT_SESSION implies true color by default");
1217 assert!(caps.colors_256, "truecolor implies 256-color");
1218 assert!(
1219 caps.osc8_hyperlinks,
1220 "WT_SESSION implies OSC 8 hyperlink support by default"
1221 );
1222 assert!(
1223 caps.bracketed_paste,
1224 "WT_SESSION should not be treated as dumb"
1225 );
1226 assert!(caps.mouse_sgr, "WT_SESSION should not be treated as dumb");
1227 }
1228
1229 #[test]
1230 #[cfg(target_os = "windows")]
1231 fn detect_windows_terminal_from_wt_session() {
1232 let mut env = make_env("", "", "");
1233 env.wt_session = true;
1234 let caps = TerminalCapabilities::detect_from_inputs(&env);
1235 assert!(caps.true_color, "WT_SESSION implies true color");
1236 assert!(caps.colors_256, "WT_SESSION implies 256-color");
1237 assert!(caps.osc8_hyperlinks, "WT_SESSION implies OSC 8 support");
1238 }
1239
1240 #[test]
1241 fn no_color_disables_color_and_links() {
1242 let env = DetectInputs {
1243 no_color: true,
1244 term: "xterm-256color".to_string(),
1245 term_program: "WezTerm".to_string(),
1246 colorterm: "truecolor".to_string(),
1247 in_tmux: false,
1248 in_screen: false,
1249 in_zellij: false,
1250 kitty_window_id: false,
1251 wt_session: false,
1252 };
1253
1254 let caps = TerminalCapabilities::detect_from_inputs(&env);
1255 assert!(!caps.true_color, "NO_COLOR must disable true color");
1256 assert!(!caps.colors_256, "NO_COLOR must disable 256-color");
1257 assert!(
1258 !caps.osc8_hyperlinks,
1259 "NO_COLOR must disable OSC 8 hyperlinks"
1260 );
1261 }
1262
1263 #[test]
1266 fn use_sync_output_disabled_in_tmux() {
1267 let mut caps = TerminalCapabilities::basic();
1268 caps.sync_output = true;
1269 assert!(caps.use_sync_output());
1270
1271 caps.in_tmux = true;
1272 assert!(!caps.use_sync_output());
1273 }
1274
1275 #[test]
1276 fn use_sync_output_disabled_in_screen() {
1277 let mut caps = TerminalCapabilities::basic();
1278 caps.sync_output = true;
1279 caps.in_screen = true;
1280 assert!(!caps.use_sync_output());
1281 }
1282
1283 #[test]
1284 fn use_sync_output_disabled_in_zellij() {
1285 let mut caps = TerminalCapabilities::basic();
1286 caps.sync_output = true;
1287 caps.in_zellij = true;
1288 assert!(!caps.use_sync_output());
1289 }
1290
1291 #[test]
1292 fn use_scroll_region_disabled_in_mux() {
1293 let mut caps = TerminalCapabilities::basic();
1294 caps.scroll_region = true;
1295 assert!(caps.use_scroll_region());
1296
1297 caps.in_tmux = true;
1298 assert!(!caps.use_scroll_region());
1299
1300 caps.in_tmux = false;
1301 caps.in_screen = true;
1302 assert!(!caps.use_scroll_region());
1303
1304 caps.in_screen = false;
1305 caps.in_zellij = true;
1306 assert!(!caps.use_scroll_region());
1307 }
1308
1309 #[test]
1310 fn use_hyperlinks_disabled_in_mux() {
1311 let mut caps = TerminalCapabilities::basic();
1312 caps.osc8_hyperlinks = true;
1313 assert!(caps.use_hyperlinks());
1314
1315 caps.in_tmux = true;
1316 assert!(!caps.use_hyperlinks());
1317 }
1318
1319 #[test]
1320 fn use_clipboard_disabled_in_mux() {
1321 let mut caps = TerminalCapabilities::basic();
1322 caps.osc52_clipboard = true;
1323 assert!(caps.use_clipboard());
1324
1325 caps.in_screen = true;
1326 assert!(!caps.use_clipboard());
1327 }
1328
1329 #[test]
1330 fn needs_passthrough_wrap_only_for_tmux_screen() {
1331 let mut caps = TerminalCapabilities::basic();
1332 assert!(!caps.needs_passthrough_wrap());
1333
1334 caps.in_tmux = true;
1335 assert!(caps.needs_passthrough_wrap());
1336
1337 caps.in_tmux = false;
1338 caps.in_screen = true;
1339 assert!(caps.needs_passthrough_wrap());
1340
1341 caps.in_screen = false;
1343 caps.in_zellij = true;
1344 assert!(!caps.needs_passthrough_wrap());
1345 }
1346
1347 #[test]
1348 fn policies_return_false_when_capability_absent() {
1349 let caps = TerminalCapabilities::basic();
1351 assert!(!caps.use_sync_output());
1352 assert!(!caps.use_scroll_region());
1353 assert!(!caps.use_hyperlinks());
1354 assert!(!caps.use_clipboard());
1355 }
1356
1357 fn make_env(term: &str, term_program: &str, colorterm: &str) -> DetectInputs {
1360 DetectInputs {
1361 no_color: false,
1362 term: term.to_string(),
1363 term_program: term_program.to_string(),
1364 colorterm: colorterm.to_string(),
1365 in_tmux: false,
1366 in_screen: false,
1367 in_zellij: false,
1368 kitty_window_id: false,
1369 wt_session: false,
1370 }
1371 }
1372
1373 #[test]
1374 fn detect_dumb_terminal() {
1375 let env = make_env("dumb", "", "");
1376 let caps = TerminalCapabilities::detect_from_inputs(&env);
1377 assert!(!caps.true_color);
1378 assert!(!caps.colors_256);
1379 assert!(!caps.sync_output);
1380 assert!(!caps.osc8_hyperlinks);
1381 assert!(!caps.scroll_region);
1382 assert!(!caps.focus_events);
1383 assert!(!caps.bracketed_paste);
1384 assert!(!caps.mouse_sgr);
1385 }
1386
1387 #[test]
1388 fn detect_dumb_overrides_truecolor_env() {
1389 let env = make_env("dumb", "WezTerm", "truecolor");
1390 let caps = TerminalCapabilities::detect_from_inputs(&env);
1391 assert!(!caps.true_color, "dumb should override COLORTERM");
1392 assert!(!caps.colors_256);
1393 assert!(!caps.bracketed_paste);
1394 assert!(!caps.mouse_sgr);
1395 assert!(!caps.osc8_hyperlinks);
1396 }
1397
1398 #[test]
1399 fn detect_empty_term_is_dumb() {
1400 let env = make_env("", "", "");
1401 let caps = TerminalCapabilities::detect_from_inputs(&env);
1402 assert!(!caps.true_color);
1403 assert!(!caps.bracketed_paste);
1404 }
1405
1406 #[test]
1407 fn detect_xterm_256color() {
1408 let env = make_env("xterm-256color", "", "");
1409 let caps = TerminalCapabilities::detect_from_inputs(&env);
1410 assert!(caps.colors_256, "xterm-256color implies 256 color");
1411 assert!(!caps.true_color, "256color alone does not imply truecolor");
1412 assert!(caps.bracketed_paste);
1413 assert!(caps.mouse_sgr);
1414 assert!(caps.scroll_region);
1415 }
1416
1417 #[test]
1418 fn detect_colorterm_truecolor() {
1419 let env = make_env("xterm-256color", "", "truecolor");
1420 let caps = TerminalCapabilities::detect_from_inputs(&env);
1421 assert!(caps.true_color, "COLORTERM=truecolor enables truecolor");
1422 assert!(caps.colors_256, "truecolor implies 256-color");
1423 }
1424
1425 #[test]
1426 fn detect_colorterm_24bit() {
1427 let env = make_env("xterm-256color", "", "24bit");
1428 let caps = TerminalCapabilities::detect_from_inputs(&env);
1429 assert!(caps.true_color, "COLORTERM=24bit enables truecolor");
1430 }
1431
1432 #[test]
1433 fn detect_kitty_by_window_id() {
1434 let mut env = make_env("xterm-kitty", "", "");
1435 env.kitty_window_id = true;
1436 let caps = TerminalCapabilities::detect_from_inputs(&env);
1437 assert!(caps.true_color, "Kitty supports truecolor");
1438 assert!(
1439 caps.kitty_keyboard,
1440 "Kitty supports kitty keyboard protocol"
1441 );
1442 assert!(caps.sync_output, "Kitty supports sync output");
1443 }
1444
1445 #[test]
1446 fn detect_kitty_by_term() {
1447 let env = make_env("xterm-kitty", "", "");
1448 let caps = TerminalCapabilities::detect_from_inputs(&env);
1449 assert!(caps.true_color, "kitty TERM implies truecolor");
1450 assert!(caps.kitty_keyboard);
1451 }
1452
1453 #[test]
1454 fn detect_wezterm() {
1455 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1456 let caps = TerminalCapabilities::detect_from_inputs(&env);
1457 assert!(caps.true_color);
1458 assert!(caps.sync_output, "WezTerm supports sync output");
1459 assert!(caps.osc8_hyperlinks, "WezTerm supports hyperlinks");
1460 assert!(caps.kitty_keyboard, "WezTerm supports kitty keyboard");
1461 assert!(caps.focus_events);
1462 assert!(caps.osc52_clipboard);
1463 }
1464
1465 #[test]
1466 #[cfg(target_os = "macos")]
1467 fn detect_iterm2_from_term_program() {
1468 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1469 let caps = TerminalCapabilities::detect_from_inputs(&env);
1470 assert!(caps.true_color, "iTerm2 implies truecolor");
1471 assert!(caps.osc8_hyperlinks, "iTerm2 supports OSC 8 hyperlinks");
1472 }
1473
1474 #[test]
1475 fn detect_alacritty() {
1476 let env = make_env("alacritty", "Alacritty", "truecolor");
1477 let caps = TerminalCapabilities::detect_from_inputs(&env);
1478 assert!(caps.true_color);
1479 assert!(caps.sync_output);
1480 assert!(caps.osc8_hyperlinks);
1481 assert!(caps.kitty_keyboard);
1482 assert!(caps.focus_events);
1483 }
1484
1485 #[test]
1486 fn detect_ghostty() {
1487 let env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1488 let caps = TerminalCapabilities::detect_from_inputs(&env);
1489 assert!(caps.true_color);
1490 assert!(caps.sync_output);
1491 assert!(caps.osc8_hyperlinks);
1492 assert!(caps.kitty_keyboard);
1493 assert!(caps.focus_events);
1494 }
1495
1496 #[test]
1497 fn detect_iterm() {
1498 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1499 let caps = TerminalCapabilities::detect_from_inputs(&env);
1500 assert!(caps.true_color);
1501 assert!(caps.osc8_hyperlinks);
1502 assert!(caps.kitty_keyboard);
1503 assert!(caps.focus_events);
1504 }
1505
1506 #[test]
1507 fn detect_vscode_terminal() {
1508 let env = make_env("xterm-256color", "vscode", "truecolor");
1509 let caps = TerminalCapabilities::detect_from_inputs(&env);
1510 assert!(caps.true_color);
1511 assert!(caps.osc8_hyperlinks);
1512 assert!(caps.focus_events);
1513 }
1514
1515 #[test]
1518 fn detect_in_tmux() {
1519 let mut env = make_env("screen-256color", "", "");
1520 env.in_tmux = true;
1521 let caps = TerminalCapabilities::detect_from_inputs(&env);
1522 assert!(caps.in_tmux);
1523 assert!(caps.in_any_mux());
1524 assert!(caps.colors_256);
1525 assert!(!caps.osc52_clipboard, "clipboard disabled in tmux");
1526 }
1527
1528 #[test]
1529 fn detect_in_screen() {
1530 let mut env = make_env("screen", "", "");
1531 env.in_screen = true;
1532 let caps = TerminalCapabilities::detect_from_inputs(&env);
1533 assert!(caps.in_screen);
1534 assert!(caps.in_any_mux());
1535 assert!(caps.needs_passthrough_wrap());
1536 }
1537
1538 #[test]
1539 fn detect_in_zellij() {
1540 let mut env = make_env("xterm-256color", "", "truecolor");
1541 env.in_zellij = true;
1542 let caps = TerminalCapabilities::detect_from_inputs(&env);
1543 assert!(caps.in_zellij);
1544 assert!(caps.in_any_mux());
1545 assert!(
1546 !caps.needs_passthrough_wrap(),
1547 "Zellij handles passthrough natively"
1548 );
1549 assert!(!caps.osc52_clipboard, "clipboard disabled in mux");
1550 }
1551
1552 #[test]
1553 fn detect_modern_terminal_in_tmux() {
1554 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
1555 env.in_tmux = true;
1556 let caps = TerminalCapabilities::detect_from_inputs(&env);
1557 assert!(caps.true_color);
1559 assert!(caps.sync_output);
1560 assert!(!caps.use_sync_output());
1562 assert!(!caps.use_hyperlinks());
1563 assert!(!caps.use_scroll_region());
1564 }
1565
1566 #[test]
1569 fn no_color_overrides_everything() {
1570 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1571 env.no_color = true;
1572 let caps = TerminalCapabilities::detect_from_inputs(&env);
1573 assert!(!caps.true_color);
1574 assert!(!caps.colors_256);
1575 assert!(!caps.osc8_hyperlinks);
1576 assert!(caps.sync_output);
1578 assert!(caps.bracketed_paste);
1579 assert!(caps.mouse_sgr);
1580 }
1581
1582 #[test]
1585 fn unknown_term_program() {
1586 let env = make_env("xterm", "SomeUnknownTerminal", "");
1587 let caps = TerminalCapabilities::detect_from_inputs(&env);
1588 assert!(
1589 !caps.true_color,
1590 "unknown terminal should not assume truecolor"
1591 );
1592 assert!(!caps.osc8_hyperlinks);
1593 assert!(caps.bracketed_paste);
1595 assert!(caps.mouse_sgr);
1596 assert!(caps.scroll_region);
1597 }
1598
1599 #[test]
1600 fn all_mux_flags_simultaneous() {
1601 let mut env = make_env("screen", "", "");
1602 env.in_tmux = true;
1603 env.in_screen = true;
1604 env.in_zellij = true;
1605 let caps = TerminalCapabilities::detect_from_inputs(&env);
1606 assert!(caps.in_any_mux());
1607 assert!(caps.needs_passthrough_wrap());
1608 assert!(!caps.use_sync_output());
1609 assert!(!caps.use_hyperlinks());
1610 assert!(!caps.use_clipboard());
1611 }
1612
1613 #[test]
1616 fn detect_rio() {
1617 let env = make_env("xterm-256color", "Rio", "truecolor");
1618 let caps = TerminalCapabilities::detect_from_inputs(&env);
1619 assert!(caps.true_color);
1620 assert!(caps.osc8_hyperlinks);
1621 assert!(caps.kitty_keyboard);
1622 assert!(caps.focus_events);
1623 }
1624
1625 #[test]
1626 fn detect_contour() {
1627 let env = make_env("xterm-256color", "Contour", "truecolor");
1628 let caps = TerminalCapabilities::detect_from_inputs(&env);
1629 assert!(caps.true_color);
1630 assert!(caps.sync_output);
1631 assert!(caps.osc8_hyperlinks);
1632 assert!(caps.focus_events);
1633 }
1634
1635 #[test]
1636 fn detect_foot() {
1637 let env = make_env("foot", "foot", "truecolor");
1638 let caps = TerminalCapabilities::detect_from_inputs(&env);
1639 assert!(caps.kitty_keyboard, "foot supports kitty keyboard");
1640 }
1641
1642 #[test]
1643 fn detect_hyper() {
1644 let env = make_env("xterm-256color", "Hyper", "truecolor");
1645 let caps = TerminalCapabilities::detect_from_inputs(&env);
1646 assert!(caps.true_color);
1647 assert!(caps.osc8_hyperlinks);
1648 assert!(caps.focus_events);
1649 }
1650
1651 #[test]
1652 fn detect_linux_console() {
1653 let env = make_env("linux", "", "");
1654 let caps = TerminalCapabilities::detect_from_inputs(&env);
1655 assert!(!caps.true_color, "linux console doesn't support truecolor");
1656 assert!(!caps.colors_256, "linux console doesn't support 256 colors");
1657 assert!(caps.bracketed_paste);
1659 assert!(caps.mouse_sgr);
1660 assert!(caps.scroll_region);
1661 }
1662
1663 #[test]
1664 fn detect_xterm_direct() {
1665 let env = make_env("xterm", "", "");
1666 let caps = TerminalCapabilities::detect_from_inputs(&env);
1667 assert!(!caps.true_color, "plain xterm has no truecolor");
1668 assert!(!caps.colors_256, "plain xterm has no 256color");
1669 assert!(caps.bracketed_paste);
1670 assert!(caps.mouse_sgr);
1671 }
1672
1673 #[test]
1674 fn detect_screen_256color() {
1675 let env = make_env("screen-256color", "", "");
1676 let caps = TerminalCapabilities::detect_from_inputs(&env);
1677 assert!(caps.colors_256, "screen-256color has 256 colors");
1678 assert!(!caps.true_color);
1679 }
1680
1681 #[test]
1684 fn wezterm_without_colorterm() {
1685 let env = make_env("xterm-256color", "WezTerm", "");
1686 let caps = TerminalCapabilities::detect_from_inputs(&env);
1687 assert!(caps.true_color, "WezTerm is modern, implies truecolor");
1689 assert!(caps.sync_output);
1690 assert!(caps.osc8_hyperlinks);
1691 }
1692
1693 #[test]
1694 fn alacritty_via_term_only() {
1695 let env = make_env("alacritty", "", "");
1697 let caps = TerminalCapabilities::detect_from_inputs(&env);
1698 assert!(caps.true_color);
1700 assert!(caps.osc8_hyperlinks);
1701 }
1702
1703 #[test]
1706 fn kitty_via_term_without_window_id() {
1707 let env = make_env("xterm-kitty", "", "");
1708 let caps = TerminalCapabilities::detect_from_inputs(&env);
1709 assert!(caps.kitty_keyboard);
1710 assert!(caps.true_color);
1711 assert!(caps.sync_output);
1712 }
1713
1714 #[test]
1715 fn kitty_window_id_with_generic_term() {
1716 let mut env = make_env("xterm-256color", "", "");
1717 env.kitty_window_id = true;
1718 let caps = TerminalCapabilities::detect_from_inputs(&env);
1719 assert!(caps.kitty_keyboard);
1720 assert!(caps.true_color);
1721 }
1722
1723 #[test]
1726 fn use_clipboard_enabled_when_no_mux_and_modern() {
1727 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1728 let caps = TerminalCapabilities::detect_from_inputs(&env);
1729 assert!(caps.osc52_clipboard);
1730 assert!(caps.use_clipboard());
1731 }
1732
1733 #[test]
1734 fn use_clipboard_disabled_in_tmux_even_if_detected() {
1735 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1736 env.in_tmux = true;
1737 let caps = TerminalCapabilities::detect_from_inputs(&env);
1738 assert!(!caps.osc52_clipboard);
1740 assert!(!caps.use_clipboard());
1741 }
1742
1743 #[test]
1744 fn scroll_region_enabled_for_basic_xterm() {
1745 let env = make_env("xterm", "", "");
1746 let caps = TerminalCapabilities::detect_from_inputs(&env);
1747 assert!(caps.scroll_region);
1748 assert!(caps.use_scroll_region());
1749 }
1750
1751 #[test]
1752 fn no_color_preserves_non_visual_features() {
1753 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1754 env.no_color = true;
1755 let caps = TerminalCapabilities::detect_from_inputs(&env);
1756 assert!(!caps.true_color);
1758 assert!(!caps.colors_256);
1759 assert!(!caps.osc8_hyperlinks);
1760 assert!(caps.sync_output);
1762 assert!(caps.kitty_keyboard);
1763 assert!(caps.focus_events);
1764 assert!(caps.bracketed_paste);
1765 assert!(caps.mouse_sgr);
1766 }
1767
1768 #[test]
1771 fn colorterm_yes_not_truecolor() {
1772 let env = make_env("xterm-256color", "", "yes");
1773 let caps = TerminalCapabilities::detect_from_inputs(&env);
1774 assert!(!caps.true_color, "COLORTERM=yes is not truecolor");
1775 assert!(caps.colors_256, "TERM=xterm-256color implies 256");
1776 }
1777
1778 #[test]
1781 fn profile_enum_as_str() {
1782 assert_eq!(TerminalProfile::Modern.as_str(), "modern");
1783 assert_eq!(TerminalProfile::Xterm256Color.as_str(), "xterm-256color");
1784 assert_eq!(TerminalProfile::Vt100.as_str(), "vt100");
1785 assert_eq!(TerminalProfile::Dumb.as_str(), "dumb");
1786 assert_eq!(TerminalProfile::Tmux.as_str(), "tmux");
1787 assert_eq!(TerminalProfile::Screen.as_str(), "screen");
1788 assert_eq!(TerminalProfile::Kitty.as_str(), "kitty");
1789 }
1790
1791 #[test]
1792 fn profile_enum_from_str() {
1793 use std::str::FromStr;
1794 assert_eq!(
1795 TerminalProfile::from_str("modern"),
1796 Ok(TerminalProfile::Modern)
1797 );
1798 assert_eq!(
1799 TerminalProfile::from_str("xterm-256color"),
1800 Ok(TerminalProfile::Xterm256Color)
1801 );
1802 assert_eq!(
1803 TerminalProfile::from_str("xterm256color"),
1804 Ok(TerminalProfile::Xterm256Color)
1805 );
1806 assert_eq!(TerminalProfile::from_str("DUMB"), Ok(TerminalProfile::Dumb));
1807 assert!(TerminalProfile::from_str("unknown").is_err());
1808 }
1809
1810 #[test]
1811 fn profile_all_predefined() {
1812 let all = TerminalProfile::all_predefined();
1813 assert!(all.len() >= 10);
1814 assert!(all.contains(&TerminalProfile::Modern));
1815 assert!(all.contains(&TerminalProfile::Dumb));
1816 assert!(!all.contains(&TerminalProfile::Custom));
1817 assert!(!all.contains(&TerminalProfile::Detected));
1818 }
1819
1820 #[test]
1821 fn profile_modern_has_all_features() {
1822 let caps = TerminalCapabilities::modern();
1823 assert_eq!(caps.profile(), TerminalProfile::Modern);
1824 assert_eq!(caps.profile_name(), Some("modern"));
1825 assert!(caps.true_color);
1826 assert!(caps.colors_256);
1827 assert!(caps.sync_output);
1828 assert!(caps.osc8_hyperlinks);
1829 assert!(caps.scroll_region);
1830 assert!(caps.kitty_keyboard);
1831 assert!(caps.focus_events);
1832 assert!(caps.bracketed_paste);
1833 assert!(caps.mouse_sgr);
1834 assert!(caps.osc52_clipboard);
1835 assert!(!caps.in_any_mux());
1836 }
1837
1838 #[test]
1839 fn profile_xterm_256color() {
1840 let caps = TerminalCapabilities::xterm_256color();
1841 assert_eq!(caps.profile(), TerminalProfile::Xterm256Color);
1842 assert!(!caps.true_color);
1843 assert!(caps.colors_256);
1844 assert!(!caps.sync_output);
1845 assert!(!caps.osc8_hyperlinks);
1846 assert!(caps.scroll_region);
1847 assert!(caps.bracketed_paste);
1848 assert!(caps.mouse_sgr);
1849 }
1850
1851 #[test]
1852 fn profile_xterm_basic() {
1853 let caps = TerminalCapabilities::xterm();
1854 assert_eq!(caps.profile(), TerminalProfile::Xterm);
1855 assert!(!caps.true_color);
1856 assert!(!caps.colors_256);
1857 assert!(caps.scroll_region);
1858 }
1859
1860 #[test]
1861 fn profile_vt100_minimal() {
1862 let caps = TerminalCapabilities::vt100();
1863 assert_eq!(caps.profile(), TerminalProfile::Vt100);
1864 assert!(!caps.true_color);
1865 assert!(!caps.colors_256);
1866 assert!(caps.scroll_region);
1867 assert!(!caps.bracketed_paste);
1868 assert!(!caps.mouse_sgr);
1869 }
1870
1871 #[test]
1872 fn profile_dumb_no_features() {
1873 let caps = TerminalCapabilities::dumb();
1874 assert_eq!(caps.profile(), TerminalProfile::Dumb);
1875 assert!(!caps.true_color);
1876 assert!(!caps.colors_256);
1877 assert!(!caps.scroll_region);
1878 assert!(!caps.bracketed_paste);
1879 assert!(!caps.mouse_sgr);
1880 assert!(!caps.use_sync_output());
1881 assert!(!caps.use_scroll_region());
1882 }
1883
1884 #[test]
1885 fn profile_tmux_mux_flags() {
1886 let caps = TerminalCapabilities::tmux();
1887 assert_eq!(caps.profile(), TerminalProfile::Tmux);
1888 assert!(caps.in_tmux);
1889 assert!(!caps.in_screen);
1890 assert!(!caps.in_zellij);
1891 assert!(caps.in_any_mux());
1892 assert!(!caps.use_sync_output());
1894 assert!(!caps.use_scroll_region());
1895 assert!(!caps.use_hyperlinks());
1896 }
1897
1898 #[test]
1899 fn profile_screen_mux_flags() {
1900 let caps = TerminalCapabilities::screen();
1901 assert_eq!(caps.profile(), TerminalProfile::Screen);
1902 assert!(!caps.in_tmux);
1903 assert!(caps.in_screen);
1904 assert!(caps.in_any_mux());
1905 assert!(caps.needs_passthrough_wrap());
1906 }
1907
1908 #[test]
1909 fn profile_zellij_mux_flags() {
1910 let caps = TerminalCapabilities::zellij();
1911 assert_eq!(caps.profile(), TerminalProfile::Zellij);
1912 assert!(caps.in_zellij);
1913 assert!(caps.in_any_mux());
1914 assert!(caps.true_color);
1916 assert!(caps.focus_events);
1917 assert!(!caps.needs_passthrough_wrap());
1919 }
1920
1921 #[test]
1922 fn profile_kitty_full_features() {
1923 let caps = TerminalCapabilities::kitty();
1924 assert_eq!(caps.profile(), TerminalProfile::Kitty);
1925 assert!(caps.true_color);
1926 assert!(caps.sync_output);
1927 assert!(caps.kitty_keyboard);
1928 assert!(caps.osc8_hyperlinks);
1929 }
1930
1931 #[test]
1932 fn profile_windows_console() {
1933 let caps = TerminalCapabilities::windows_console();
1934 assert_eq!(caps.profile(), TerminalProfile::WindowsConsole);
1935 assert!(caps.true_color);
1936 assert!(caps.osc8_hyperlinks);
1937 assert!(caps.focus_events);
1938 }
1939
1940 #[test]
1941 fn profile_linux_console() {
1942 let caps = TerminalCapabilities::linux_console();
1943 assert_eq!(caps.profile(), TerminalProfile::LinuxConsole);
1944 assert!(!caps.true_color);
1945 assert!(!caps.colors_256);
1946 assert!(caps.scroll_region);
1947 }
1948
1949 #[test]
1950 fn from_profile_roundtrip() {
1951 for profile in TerminalProfile::all_predefined() {
1952 let caps = TerminalCapabilities::from_profile(*profile);
1953 assert_eq!(caps.profile(), *profile);
1954 }
1955 }
1956
1957 #[test]
1958 fn detected_profile_has_none_name() {
1959 let caps = detect_with_override(None);
1960 assert_eq!(caps.profile(), TerminalProfile::Detected);
1961 assert_eq!(caps.profile_name(), None);
1962 }
1963
1964 #[test]
1965 fn detect_respects_test_profile_env() {
1966 let caps = detect_with_override(Some("dumb"));
1967 assert_eq!(caps.profile(), TerminalProfile::Dumb);
1968 }
1969
1970 #[test]
1971 fn detect_ignores_invalid_test_profile() {
1972 let caps = detect_with_override(Some("not-a-real-profile"));
1973 assert_eq!(caps.profile(), TerminalProfile::Detected);
1974 }
1975
1976 #[test]
1977 fn basic_has_dumb_profile() {
1978 let caps = TerminalCapabilities::basic();
1979 assert_eq!(caps.profile(), TerminalProfile::Dumb);
1980 }
1981
1982 #[test]
1985 fn builder_starts_empty() {
1986 let caps = CapabilityProfileBuilder::new().build();
1987 assert_eq!(caps.profile(), TerminalProfile::Custom);
1988 assert!(!caps.true_color);
1989 assert!(!caps.colors_256);
1990 assert!(!caps.sync_output);
1991 assert!(!caps.scroll_region);
1992 assert!(!caps.mouse_sgr);
1993 }
1994
1995 #[test]
1996 fn builder_set_colors() {
1997 let caps = CapabilityProfileBuilder::new()
1998 .true_color(true)
1999 .colors_256(true)
2000 .build();
2001 assert!(caps.true_color);
2002 assert!(caps.colors_256);
2003 }
2004
2005 #[test]
2006 fn builder_set_advanced() {
2007 let caps = CapabilityProfileBuilder::new()
2008 .sync_output(true)
2009 .osc8_hyperlinks(true)
2010 .scroll_region(true)
2011 .build();
2012 assert!(caps.sync_output);
2013 assert!(caps.osc8_hyperlinks);
2014 assert!(caps.scroll_region);
2015 }
2016
2017 #[test]
2018 fn builder_set_mux() {
2019 let caps = CapabilityProfileBuilder::new()
2020 .in_tmux(true)
2021 .in_screen(false)
2022 .in_zellij(false)
2023 .build();
2024 assert!(caps.in_tmux);
2025 assert!(!caps.in_screen);
2026 assert!(caps.in_any_mux());
2027 }
2028
2029 #[test]
2030 fn builder_set_input() {
2031 let caps = CapabilityProfileBuilder::new()
2032 .kitty_keyboard(true)
2033 .focus_events(true)
2034 .bracketed_paste(true)
2035 .mouse_sgr(true)
2036 .build();
2037 assert!(caps.kitty_keyboard);
2038 assert!(caps.focus_events);
2039 assert!(caps.bracketed_paste);
2040 assert!(caps.mouse_sgr);
2041 }
2042
2043 #[test]
2044 fn builder_set_clipboard() {
2045 let caps = CapabilityProfileBuilder::new()
2046 .osc52_clipboard(true)
2047 .build();
2048 assert!(caps.osc52_clipboard);
2049 }
2050
2051 #[test]
2052 fn builder_from_profile() {
2053 let caps = CapabilityProfileBuilder::from_profile(TerminalProfile::Modern)
2054 .sync_output(false) .build();
2056 assert!(caps.true_color);
2058 assert!(caps.colors_256);
2059 assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
2061 assert_eq!(caps.profile(), TerminalProfile::Custom);
2063 }
2064
2065 #[test]
2066 fn builder_chain_multiple() {
2067 let caps = TerminalCapabilities::builder()
2068 .colors_256(true)
2069 .bracketed_paste(true)
2070 .mouse_sgr(true)
2071 .scroll_region(true)
2072 .build();
2073 assert!(caps.colors_256);
2074 assert!(caps.bracketed_paste);
2075 assert!(caps.mouse_sgr);
2076 assert!(caps.scroll_region);
2077 assert!(!caps.true_color);
2078 assert!(!caps.sync_output);
2079 }
2080
2081 #[test]
2082 fn builder_default() {
2083 let builder = CapabilityProfileBuilder::default();
2084 let caps = builder.build();
2085 assert_eq!(caps.profile(), TerminalProfile::Custom);
2086 }
2087
2088 #[test]
2097 fn mux_compatibility_matrix() {
2098 {
2103 let caps = TerminalCapabilities::modern();
2104 assert!(
2105 caps.use_sync_output(),
2106 "baseline: sync_output should be enabled"
2107 );
2108 assert!(
2109 caps.use_scroll_region(),
2110 "baseline: scroll_region should be enabled"
2111 );
2112 assert!(
2113 caps.use_hyperlinks(),
2114 "baseline: hyperlinks should be enabled"
2115 );
2116 assert!(!caps.needs_passthrough_wrap(), "baseline: no wrap needed");
2117 }
2118
2119 {
2121 let caps = TerminalCapabilities::tmux();
2122 assert!(!caps.use_sync_output(), "tmux: sync_output disabled");
2123 assert!(!caps.use_scroll_region(), "tmux: scroll_region disabled");
2124 assert!(!caps.use_hyperlinks(), "tmux: hyperlinks disabled");
2125 assert!(caps.needs_passthrough_wrap(), "tmux: needs wrap");
2126 }
2127
2128 {
2130 let caps = TerminalCapabilities::screen();
2131 assert!(!caps.use_sync_output(), "screen: sync_output disabled");
2132 assert!(!caps.use_scroll_region(), "screen: scroll_region disabled");
2133 assert!(!caps.use_hyperlinks(), "screen: hyperlinks disabled");
2134 assert!(caps.needs_passthrough_wrap(), "screen: needs wrap");
2135 }
2136
2137 {
2139 let caps = TerminalCapabilities::zellij();
2140 assert!(!caps.use_sync_output(), "zellij: sync_output disabled");
2141 assert!(!caps.use_scroll_region(), "zellij: scroll_region disabled");
2142 assert!(!caps.use_hyperlinks(), "zellij: hyperlinks disabled");
2143 assert!(
2144 !caps.needs_passthrough_wrap(),
2145 "zellij: no wrap needed (native passthrough)"
2146 );
2147 }
2148 }
2149
2150 #[test]
2152 fn modern_terminal_in_mux_matrix() {
2153 for (mux_name, in_tmux, in_screen, in_zellij) in [
2157 ("tmux", true, false, false),
2158 ("screen", false, true, false),
2159 ("zellij", false, false, true),
2160 ] {
2161 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2162 env.in_tmux = in_tmux;
2163 env.in_screen = in_screen;
2164 env.in_zellij = in_zellij;
2165 let caps = TerminalCapabilities::detect_from_inputs(&env);
2166
2167 assert!(
2169 caps.true_color,
2170 "{mux_name}: true_color detection should work"
2171 );
2172 assert!(
2173 caps.sync_output,
2174 "{mux_name}: sync_output detection should work"
2175 );
2176
2177 assert!(
2179 !caps.use_sync_output(),
2180 "{mux_name}: use_sync_output() should be false"
2181 );
2182 assert!(
2183 !caps.use_scroll_region(),
2184 "{mux_name}: use_scroll_region() should be false"
2185 );
2186 assert!(
2187 !caps.use_hyperlinks(),
2188 "{mux_name}: use_hyperlinks() should be false"
2189 );
2190 }
2191 }
2192
2193 #[test]
2195 fn profile_mux_invariant_matrix() {
2196 for profile in TerminalProfile::all_predefined() {
2198 let caps = TerminalCapabilities::from_profile(*profile);
2199 let name = profile.as_str();
2200
2201 let expected_mux = caps.in_tmux || caps.in_screen || caps.in_zellij;
2203 assert_eq!(
2204 caps.in_any_mux(),
2205 expected_mux,
2206 "{name}: in_any_mux() should match individual flags"
2207 );
2208
2209 if caps.in_any_mux() {
2211 assert!(
2212 !caps.use_sync_output(),
2213 "{name}: mux should disable use_sync_output()"
2214 );
2215 assert!(
2216 !caps.use_scroll_region(),
2217 "{name}: mux should disable use_scroll_region()"
2218 );
2219 assert!(
2220 !caps.use_hyperlinks(),
2221 "{name}: mux should disable use_hyperlinks()"
2222 );
2223 }
2224
2225 if caps.in_tmux || caps.in_screen {
2227 assert!(
2228 caps.needs_passthrough_wrap(),
2229 "{name}: tmux/screen should need passthrough wrap"
2230 );
2231 } else if caps.in_zellij {
2232 assert!(
2233 !caps.needs_passthrough_wrap(),
2234 "{name}: zellij should NOT need passthrough wrap"
2235 );
2236 }
2237 }
2238 }
2239
2240 #[test]
2242 fn fallback_ordering_matrix() {
2243 use crate::inline_mode::InlineStrategy;
2244
2245 let caps_full = TerminalCapabilities::builder()
2247 .sync_output(true)
2248 .scroll_region(true)
2249 .build();
2250 assert_eq!(
2251 InlineStrategy::select(&caps_full),
2252 InlineStrategy::ScrollRegion,
2253 "full capabilities should use ScrollRegion"
2254 );
2255
2256 let caps_hybrid = TerminalCapabilities::builder()
2258 .sync_output(false)
2259 .scroll_region(true)
2260 .build();
2261 assert_eq!(
2262 InlineStrategy::select(&caps_hybrid),
2263 InlineStrategy::Hybrid,
2264 "scroll without sync should use Hybrid"
2265 );
2266
2267 let caps_none = TerminalCapabilities::builder()
2269 .sync_output(false)
2270 .scroll_region(false)
2271 .build();
2272 assert_eq!(
2273 InlineStrategy::select(&caps_none),
2274 InlineStrategy::OverlayRedraw,
2275 "no capabilities should use OverlayRedraw"
2276 );
2277
2278 let caps_tmux = TerminalCapabilities::tmux();
2280 assert_eq!(
2281 InlineStrategy::select(&caps_tmux),
2282 InlineStrategy::OverlayRedraw,
2283 "tmux should force OverlayRedraw"
2284 );
2285 }
2286
2287 #[test]
2289 fn terminal_mux_strategy_matrix() {
2290 use crate::inline_mode::InlineStrategy;
2291
2292 struct TestCase {
2293 name: &'static str,
2294 profile: TerminalProfile,
2295 expected: InlineStrategy,
2296 }
2297
2298 let cases = [
2299 TestCase {
2300 name: "modern (no mux)",
2301 profile: TerminalProfile::Modern,
2302 expected: InlineStrategy::ScrollRegion,
2303 },
2304 TestCase {
2305 name: "kitty (no mux)",
2306 profile: TerminalProfile::Kitty,
2307 expected: InlineStrategy::ScrollRegion,
2308 },
2309 TestCase {
2310 name: "xterm-256color (no mux)",
2311 profile: TerminalProfile::Xterm256Color,
2312 expected: InlineStrategy::Hybrid, },
2314 TestCase {
2315 name: "xterm (no mux)",
2316 profile: TerminalProfile::Xterm,
2317 expected: InlineStrategy::Hybrid,
2318 },
2319 TestCase {
2320 name: "vt100 (no mux)",
2321 profile: TerminalProfile::Vt100,
2322 expected: InlineStrategy::Hybrid,
2323 },
2324 TestCase {
2325 name: "dumb",
2326 profile: TerminalProfile::Dumb,
2327 expected: InlineStrategy::OverlayRedraw, },
2329 TestCase {
2330 name: "tmux",
2331 profile: TerminalProfile::Tmux,
2332 expected: InlineStrategy::OverlayRedraw,
2333 },
2334 TestCase {
2335 name: "screen",
2336 profile: TerminalProfile::Screen,
2337 expected: InlineStrategy::OverlayRedraw,
2338 },
2339 TestCase {
2340 name: "zellij",
2341 profile: TerminalProfile::Zellij,
2342 expected: InlineStrategy::OverlayRedraw,
2343 },
2344 ];
2345
2346 for case in cases {
2347 let caps = TerminalCapabilities::from_profile(case.profile);
2348 let actual = InlineStrategy::select(&caps);
2349 assert_eq!(
2350 actual, case.expected,
2351 "{}: expected {:?}, got {:?}",
2352 case.name, case.expected, actual
2353 );
2354 }
2355 }
2356}
2357
2358#[cfg(test)]
2363mod proptests {
2364 use super::*;
2365 use proptest::prelude::*;
2366
2367 proptest! {
2368 #[test]
2370 fn prop_in_any_mux_consistent(
2371 in_tmux in any::<bool>(),
2372 in_screen in any::<bool>(),
2373 in_zellij in any::<bool>(),
2374 ) {
2375 let caps = TerminalCapabilities::builder()
2376 .in_tmux(in_tmux)
2377 .in_screen(in_screen)
2378 .in_zellij(in_zellij)
2379 .build();
2380
2381 let expected = in_tmux || in_screen || in_zellij;
2382 prop_assert_eq!(caps.in_any_mux(), expected);
2383 }
2384
2385 #[test]
2387 fn prop_mux_disables_sync_output(
2388 in_tmux in any::<bool>(),
2389 in_screen in any::<bool>(),
2390 in_zellij in any::<bool>(),
2391 sync_output in any::<bool>(),
2392 ) {
2393 let caps = TerminalCapabilities::builder()
2394 .in_tmux(in_tmux)
2395 .in_screen(in_screen)
2396 .in_zellij(in_zellij)
2397 .sync_output(sync_output)
2398 .build();
2399
2400 if caps.in_any_mux() {
2401 prop_assert!(!caps.use_sync_output(), "mux should disable sync_output policy");
2402 }
2403 }
2404
2405 #[test]
2407 fn prop_mux_disables_scroll_region(
2408 in_tmux in any::<bool>(),
2409 in_screen in any::<bool>(),
2410 in_zellij in any::<bool>(),
2411 scroll_region in any::<bool>(),
2412 ) {
2413 let caps = TerminalCapabilities::builder()
2414 .in_tmux(in_tmux)
2415 .in_screen(in_screen)
2416 .in_zellij(in_zellij)
2417 .scroll_region(scroll_region)
2418 .build();
2419
2420 if caps.in_any_mux() {
2421 prop_assert!(!caps.use_scroll_region(), "mux should disable scroll_region policy");
2422 }
2423 }
2424
2425 #[test]
2427 fn prop_mux_disables_hyperlinks(
2428 in_tmux in any::<bool>(),
2429 in_screen in any::<bool>(),
2430 in_zellij in any::<bool>(),
2431 osc8_hyperlinks in any::<bool>(),
2432 ) {
2433 let caps = TerminalCapabilities::builder()
2434 .in_tmux(in_tmux)
2435 .in_screen(in_screen)
2436 .in_zellij(in_zellij)
2437 .osc8_hyperlinks(osc8_hyperlinks)
2438 .build();
2439
2440 if caps.in_any_mux() {
2441 prop_assert!(!caps.use_hyperlinks(), "mux should disable hyperlinks policy");
2442 }
2443 }
2444
2445 #[test]
2447 fn prop_passthrough_wrap_logic(
2448 in_tmux in any::<bool>(),
2449 in_screen in any::<bool>(),
2450 in_zellij in any::<bool>(),
2451 ) {
2452 let caps = TerminalCapabilities::builder()
2453 .in_tmux(in_tmux)
2454 .in_screen(in_screen)
2455 .in_zellij(in_zellij)
2456 .build();
2457
2458 let expected = in_tmux || in_screen; prop_assert_eq!(caps.needs_passthrough_wrap(), expected);
2460 }
2461
2462 #[test]
2464 fn prop_policy_false_when_capability_off(
2465 in_tmux in any::<bool>(),
2466 in_screen in any::<bool>(),
2467 in_zellij in any::<bool>(),
2468 ) {
2469 let caps = TerminalCapabilities::builder()
2470 .in_tmux(in_tmux)
2471 .in_screen(in_screen)
2472 .in_zellij(in_zellij)
2473 .sync_output(false)
2474 .scroll_region(false)
2475 .osc8_hyperlinks(false)
2476 .osc52_clipboard(false)
2477 .build();
2478
2479 prop_assert!(!caps.use_sync_output(), "sync_output=false implies use_sync_output()=false");
2480 prop_assert!(!caps.use_scroll_region(), "scroll_region=false implies use_scroll_region()=false");
2481 prop_assert!(!caps.use_hyperlinks(), "osc8_hyperlinks=false implies use_hyperlinks()=false");
2482 prop_assert!(!caps.use_clipboard(), "osc52_clipboard=false implies use_clipboard()=false");
2483 }
2484
2485 #[test]
2487 fn prop_no_color_preserves_non_visual(no_color in any::<bool>()) {
2488 let env = DetectInputs {
2489 no_color,
2490 term: "xterm-256color".to_string(),
2491 term_program: "WezTerm".to_string(),
2492 colorterm: "truecolor".to_string(),
2493 in_tmux: false,
2494 in_screen: false,
2495 in_zellij: false,
2496 kitty_window_id: false,
2497 wt_session: false,
2498 };
2499 let caps = TerminalCapabilities::detect_from_inputs(&env);
2500
2501 if no_color {
2502 prop_assert!(!caps.true_color, "NO_COLOR disables true_color");
2503 prop_assert!(!caps.colors_256, "NO_COLOR disables colors_256");
2504 prop_assert!(!caps.osc8_hyperlinks, "NO_COLOR disables hyperlinks");
2505 }
2506
2507 prop_assert!(caps.sync_output, "sync_output preserved despite NO_COLOR");
2509 prop_assert!(caps.bracketed_paste, "bracketed_paste preserved despite NO_COLOR");
2510 }
2511 }
2512}