1#![forbid(unsafe_code)]
2
3use std::env;
117use std::ffi::OsString;
118use std::str::FromStr;
119
120fn normalize_terminal_env_value(value: &str) -> String {
121 value.trim().to_ascii_lowercase()
122}
123
124fn is_dumb_terminal(term: &str, windows_terminal: bool) -> bool {
125 term == "dumb" || (term.is_empty() && !windows_terminal)
126}
127
128fn colorterm_declares_true_color(colorterm: &str) -> bool {
129 matches!(colorterm, "truecolor" | "24bit")
130}
131
132fn term_declares_true_color(term: &str) -> bool {
133 let direct_color = term.rsplit_once("-direct").is_some_and(|(base, suffix)| {
134 !base.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit())
135 });
136
137 direct_color || term.ends_with("-truecolor") || term == "24bit" || term.ends_with("-24bit")
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
145#[repr(u8)]
146pub enum ColorDepth {
147 #[default]
149 Mono,
150 Ansi16,
152 Ansi256,
154 TrueColor,
156}
157
158impl ColorDepth {
159 #[must_use]
161 pub fn detect() -> Self {
162 TerminalCapabilities::detect().color_depth
163 }
164
165 #[must_use]
173 pub fn detect_from_env(
174 no_color: Option<&str>,
175 colorterm: Option<&str>,
176 term: Option<&str>,
177 ) -> Self {
178 let term = normalize_terminal_env_value(term.unwrap_or_default());
179 let colorterm = normalize_terminal_env_value(colorterm.unwrap_or_default());
180 Self::detect_normalized(
181 no_color.is_some(),
182 is_dumb_terminal(&term, false),
183 &colorterm,
184 &term,
185 false,
186 )
187 }
188
189 fn detect_normalized(
190 no_color: bool,
191 is_dumb: bool,
192 colorterm: &str,
193 term: &str,
194 inferred_true_color: bool,
195 ) -> Self {
196 if no_color || is_dumb || term == "vt100" {
197 Self::Mono
198 } else if term == "linux" {
199 Self::Ansi16
200 } else if colorterm_declares_true_color(colorterm)
201 || term_declares_true_color(term)
202 || inferred_true_color
203 {
204 Self::TrueColor
205 } else if term.contains("256color") || term.contains("256") {
206 Self::Ansi256
207 } else {
208 Self::Ansi16
209 }
210 }
211
212 #[must_use]
214 pub const fn as_str(self) -> &'static str {
215 match self {
216 Self::Mono => "mono",
217 Self::Ansi16 => "ansi16",
218 Self::Ansi256 => "ansi256",
219 Self::TrueColor => "truecolor",
220 }
221 }
222
223 #[must_use]
225 pub const fn supports_color(self) -> bool {
226 !matches!(self, Self::Mono)
227 }
228
229 #[must_use]
231 pub const fn supports_256_colors(self) -> bool {
232 matches!(self, Self::Ansi256 | Self::TrueColor)
233 }
234
235 #[must_use]
237 pub const fn supports_true_color(self) -> bool {
238 matches!(self, Self::TrueColor)
239 }
240}
241
242impl std::fmt::Display for ColorDepth {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 f.write_str(self.as_str())
245 }
246}
247
248#[derive(Debug, Clone)]
249struct DetectInputs {
250 no_color: bool,
251 term: String,
252 term_program: String,
253 colorterm: String,
254 in_tmux: bool,
255 in_screen: bool,
256 in_zellij: bool,
257 wezterm_unix_socket: bool,
258 wezterm_pane: bool,
259 wezterm_executable: bool,
260 kitty_window_id: bool,
261 wt_session: bool,
262}
263
264impl DetectInputs {
265 fn from_env() -> Self {
266 Self::from_env_with(|key| env::var_os(key))
267 }
268
269 fn from_env_with<F>(get_env: F) -> Self
270 where
271 F: Fn(&str) -> Option<OsString>,
272 {
273 let text = |key| {
274 get_env(key)
275 .and_then(|value| value.into_string().ok())
276 .unwrap_or_default()
277 };
278 let utf8_present = |key| get_env(key).is_some_and(|value| value.into_string().is_ok());
279
280 Self {
281 no_color: get_env("NO_COLOR").is_some(),
282 term: text("TERM"),
283 term_program: text("TERM_PROGRAM"),
284 colorterm: text("COLORTERM"),
285 in_tmux: utf8_present("TMUX"),
286 in_screen: utf8_present("STY"),
287 in_zellij: utf8_present("ZELLIJ"),
288 wezterm_unix_socket: utf8_present("WEZTERM_UNIX_SOCKET"),
289 wezterm_pane: utf8_present("WEZTERM_PANE"),
290 wezterm_executable: utf8_present("WEZTERM_EXECUTABLE"),
291 kitty_window_id: utf8_present("KITTY_WINDOW_ID"),
292 wt_session: utf8_present("WT_SESSION"),
293 }
294 }
295}
296
297const MODERN_TERMINALS: &[&str] = &[
299 "iTerm.app",
300 "WezTerm",
301 "Alacritty",
302 "Ghostty",
303 "kitty",
304 "Rio",
305 "Hyper",
306 "Contour",
307 "vscode",
308 "Black Box",
309];
310
311const KITTY_KEYBOARD_TERMINALS: &[&str] = &[
313 "iTerm.app",
314 "WezTerm",
315 "Alacritty",
316 "Ghostty",
317 "Rio",
318 "kitty",
319 "foot",
320 "Black Box",
321];
322
323const SYNC_OUTPUT_TERMINALS: &[&str] = &["Alacritty", "Ghostty", "kitty", "Contour"];
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333pub enum TerminalProfile {
334 Modern,
336 Xterm256Color,
338 Xterm,
340 Vt100,
342 Dumb,
344 Screen,
346 Tmux,
348 Zellij,
350 WindowsConsole,
352 Kitty,
354 LinuxConsole,
356 Custom,
358 Detected,
360}
361
362impl TerminalProfile {
363 #[must_use]
365 pub const fn as_str(&self) -> &'static str {
366 match self {
367 Self::Modern => "modern",
368 Self::Xterm256Color => "xterm-256color",
369 Self::Xterm => "xterm",
370 Self::Vt100 => "vt100",
371 Self::Dumb => "dumb",
372 Self::Screen => "screen",
373 Self::Tmux => "tmux",
374 Self::Zellij => "zellij",
375 Self::WindowsConsole => "windows-console",
376 Self::Kitty => "kitty",
377 Self::LinuxConsole => "linux",
378 Self::Custom => "custom",
379 Self::Detected => "detected",
380 }
381 }
382
383 #[must_use]
385 pub const fn all_predefined() -> &'static [Self] {
386 &[
387 Self::Modern,
388 Self::Xterm256Color,
389 Self::Xterm,
390 Self::Vt100,
391 Self::Dumb,
392 Self::Screen,
393 Self::Tmux,
394 Self::Zellij,
395 Self::WindowsConsole,
396 Self::Kitty,
397 Self::LinuxConsole,
398 ]
399 }
400}
401
402impl std::str::FromStr for TerminalProfile {
403 type Err = ();
404
405 fn from_str(s: &str) -> Result<Self, Self::Err> {
406 match s.to_lowercase().as_str() {
407 "modern" => Ok(Self::Modern),
408 "xterm-256color" | "xterm256color" | "xterm-256" => Ok(Self::Xterm256Color),
409 "xterm" => Ok(Self::Xterm),
410 "vt100" => Ok(Self::Vt100),
411 "dumb" => Ok(Self::Dumb),
412 "screen" | "screen-256color" => Ok(Self::Screen),
413 "tmux" | "tmux-256color" => Ok(Self::Tmux),
414 "zellij" => Ok(Self::Zellij),
415 "windows-console" | "windows" | "conhost" => Ok(Self::WindowsConsole),
416 "kitty" | "xterm-kitty" => Ok(Self::Kitty),
417 "linux" | "linux-console" => Ok(Self::LinuxConsole),
418 "custom" => Ok(Self::Custom),
419 "detected" | "auto" => Ok(Self::Detected),
420 _ => Err(()),
421 }
422 }
423}
424
425impl std::fmt::Display for TerminalProfile {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 write!(f, "{}", self.as_str())
428 }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448pub struct TerminalCapabilities {
449 profile: TerminalProfile,
451
452 pub color_depth: ColorDepth,
455
456 pub unicode_box_drawing: bool,
459 pub unicode_emoji: bool,
461 pub double_width: bool,
463
464 pub sync_output: bool,
467 pub osc8_hyperlinks: bool,
469 pub scroll_region: bool,
471
472 pub in_tmux: bool,
475 pub in_screen: bool,
477 pub in_zellij: bool,
479 pub in_wezterm_mux: bool,
487
488 pub kitty_keyboard: bool,
491 pub focus_events: bool,
493 pub bracketed_paste: bool,
495 pub mouse_sgr: bool,
497
498 pub osc52_clipboard: bool,
501}
502
503impl Default for TerminalCapabilities {
504 fn default() -> Self {
505 Self::basic()
506 }
507}
508
509impl TerminalCapabilities {
514 #[must_use]
518 pub const fn profile(&self) -> TerminalProfile {
519 self.profile
520 }
521
522 #[must_use]
527 pub fn profile_name(&self) -> Option<&'static str> {
528 match self.profile {
529 TerminalProfile::Detected => None,
530 p => Some(p.as_str()),
531 }
532 }
533
534 #[must_use]
536 pub fn from_profile(profile: TerminalProfile) -> Self {
537 match profile {
538 TerminalProfile::Modern => Self::modern(),
539 TerminalProfile::Xterm256Color => Self::xterm_256color(),
540 TerminalProfile::Xterm => Self::xterm(),
541 TerminalProfile::Vt100 => Self::vt100(),
542 TerminalProfile::Dumb => Self::dumb(),
543 TerminalProfile::Screen => Self::screen(),
544 TerminalProfile::Tmux => Self::tmux(),
545 TerminalProfile::Zellij => Self::zellij(),
546 TerminalProfile::WindowsConsole => Self::windows_console(),
547 TerminalProfile::Kitty => Self::kitty(),
548 TerminalProfile::LinuxConsole => Self::linux_console(),
549 TerminalProfile::Custom => {
550 let mut caps = Self::basic();
553 caps.profile = TerminalProfile::Custom;
554 caps
555 }
556 TerminalProfile::Detected => Self::detect(),
557 }
558 }
559
560 #[must_use]
567 pub const fn modern() -> Self {
568 Self {
569 profile: TerminalProfile::Modern,
570 color_depth: ColorDepth::TrueColor,
571 unicode_box_drawing: true,
572 unicode_emoji: true,
573 double_width: true,
574 sync_output: true,
575 osc8_hyperlinks: true,
576 scroll_region: true,
577 in_tmux: false,
578 in_screen: false,
579 in_zellij: false,
580 in_wezterm_mux: false,
581 kitty_keyboard: true,
582 focus_events: true,
583 bracketed_paste: true,
584 mouse_sgr: true,
585 osc52_clipboard: true,
586 }
587 }
588
589 #[must_use]
594 pub const fn xterm_256color() -> Self {
595 Self {
596 profile: TerminalProfile::Xterm256Color,
597 color_depth: ColorDepth::Ansi256,
598 unicode_box_drawing: true,
599 unicode_emoji: true,
600 double_width: true,
601 sync_output: false,
602 osc8_hyperlinks: false,
603 scroll_region: true,
604 in_tmux: false,
605 in_screen: false,
606 in_zellij: false,
607 in_wezterm_mux: false,
608 kitty_keyboard: false,
609 focus_events: false,
610 bracketed_paste: true,
611 mouse_sgr: true,
612 osc52_clipboard: false,
613 }
614 }
615
616 #[must_use]
620 pub const fn xterm() -> Self {
621 Self {
622 profile: TerminalProfile::Xterm,
623 color_depth: ColorDepth::Ansi16,
624 unicode_box_drawing: true,
625 unicode_emoji: false,
626 double_width: true,
627 sync_output: false,
628 osc8_hyperlinks: false,
629 scroll_region: true,
630 in_tmux: false,
631 in_screen: false,
632 in_zellij: false,
633 in_wezterm_mux: false,
634 kitty_keyboard: false,
635 focus_events: false,
636 bracketed_paste: true,
637 mouse_sgr: true,
638 osc52_clipboard: false,
639 }
640 }
641
642 #[must_use]
646 pub const fn vt100() -> Self {
647 Self {
648 profile: TerminalProfile::Vt100,
649 color_depth: ColorDepth::Mono,
650 unicode_box_drawing: false,
651 unicode_emoji: false,
652 double_width: false,
653 sync_output: false,
654 osc8_hyperlinks: false,
655 scroll_region: true,
656 in_tmux: false,
657 in_screen: false,
658 in_zellij: false,
659 in_wezterm_mux: false,
660 kitty_keyboard: false,
661 focus_events: false,
662 bracketed_paste: false,
663 mouse_sgr: false,
664 osc52_clipboard: false,
665 }
666 }
667
668 #[must_use]
672 pub const fn dumb() -> Self {
673 Self {
674 profile: TerminalProfile::Dumb,
675 color_depth: ColorDepth::Mono,
676 unicode_box_drawing: false,
677 unicode_emoji: false,
678 double_width: false,
679 sync_output: false,
680 osc8_hyperlinks: false,
681 scroll_region: false,
682 in_tmux: false,
683 in_screen: false,
684 in_zellij: false,
685 in_wezterm_mux: false,
686 kitty_keyboard: false,
687 focus_events: false,
688 bracketed_paste: false,
689 mouse_sgr: false,
690 osc52_clipboard: false,
691 }
692 }
693
694 #[must_use]
699 pub const fn screen() -> Self {
700 Self {
701 profile: TerminalProfile::Screen,
702 color_depth: ColorDepth::Ansi256,
703 unicode_box_drawing: true,
704 unicode_emoji: true,
705 double_width: true,
706 sync_output: false,
707 osc8_hyperlinks: false,
708 scroll_region: true,
709 in_tmux: false,
710 in_screen: true,
711 in_zellij: false,
712 in_wezterm_mux: false,
713 kitty_keyboard: false,
714 focus_events: false,
715 bracketed_paste: true,
716 mouse_sgr: true,
717 osc52_clipboard: false,
718 }
719 }
720
721 #[must_use]
726 pub const fn tmux() -> Self {
727 Self {
728 profile: TerminalProfile::Tmux,
729 color_depth: ColorDepth::Ansi256,
730 unicode_box_drawing: true,
731 unicode_emoji: true,
732 double_width: true,
733 sync_output: false,
734 osc8_hyperlinks: false,
735 scroll_region: true,
736 in_tmux: true,
737 in_screen: false,
738 in_zellij: false,
739 in_wezterm_mux: false,
740 kitty_keyboard: false,
741 focus_events: false,
742 bracketed_paste: true,
743 mouse_sgr: true,
744 osc52_clipboard: false,
745 }
746 }
747
748 #[must_use]
752 pub const fn zellij() -> Self {
753 Self {
754 profile: TerminalProfile::Zellij,
755 color_depth: ColorDepth::TrueColor,
756 unicode_box_drawing: true,
757 unicode_emoji: true,
758 double_width: true,
759 sync_output: false,
760 osc8_hyperlinks: false,
761 scroll_region: true,
762 in_tmux: false,
763 in_screen: false,
764 in_zellij: true,
765 in_wezterm_mux: false,
766 kitty_keyboard: false,
767 focus_events: true,
768 bracketed_paste: true,
769 mouse_sgr: true,
770 osc52_clipboard: false,
771 }
772 }
773
774 #[must_use]
778 pub const fn windows_console() -> Self {
779 Self {
780 profile: TerminalProfile::WindowsConsole,
781 color_depth: ColorDepth::TrueColor,
782 unicode_box_drawing: true,
783 unicode_emoji: true,
784 double_width: true,
785 sync_output: false,
786 osc8_hyperlinks: true,
787 scroll_region: true,
788 in_tmux: false,
789 in_screen: false,
790 in_zellij: false,
791 in_wezterm_mux: false,
792 kitty_keyboard: false,
793 focus_events: true,
794 bracketed_paste: true,
795 mouse_sgr: true,
796 osc52_clipboard: true,
797 }
798 }
799
800 #[must_use]
804 pub const fn kitty() -> Self {
805 Self {
806 profile: TerminalProfile::Kitty,
807 color_depth: ColorDepth::TrueColor,
808 unicode_box_drawing: true,
809 unicode_emoji: true,
810 double_width: true,
811 sync_output: true,
812 osc8_hyperlinks: true,
813 scroll_region: true,
814 in_tmux: false,
815 in_screen: false,
816 in_zellij: false,
817 in_wezterm_mux: false,
818 kitty_keyboard: true,
819 focus_events: true,
820 bracketed_paste: true,
821 mouse_sgr: true,
822 osc52_clipboard: true,
823 }
824 }
825
826 #[must_use]
830 pub const fn linux_console() -> Self {
831 Self {
832 profile: TerminalProfile::LinuxConsole,
833 color_depth: ColorDepth::Ansi16,
834 unicode_box_drawing: true,
835 unicode_emoji: false,
836 double_width: false,
837 sync_output: false,
838 osc8_hyperlinks: false,
839 scroll_region: true,
840 in_tmux: false,
841 in_screen: false,
842 in_zellij: false,
843 in_wezterm_mux: false,
844 kitty_keyboard: false,
845 focus_events: false,
846 bracketed_paste: true,
847 mouse_sgr: true,
848 osc52_clipboard: false,
849 }
850 }
851
852 pub fn builder() -> CapabilityProfileBuilder {
856 CapabilityProfileBuilder::new()
857 }
858}
859
860#[derive(Debug, Clone)]
883#[must_use]
884pub struct CapabilityProfileBuilder {
885 caps: TerminalCapabilities,
886}
887
888impl Default for CapabilityProfileBuilder {
889 fn default() -> Self {
890 Self::new()
891 }
892}
893
894impl CapabilityProfileBuilder {
895 pub fn new() -> Self {
897 Self {
898 caps: TerminalCapabilities {
899 profile: TerminalProfile::Custom,
900 color_depth: ColorDepth::Mono,
901 unicode_box_drawing: false,
902 unicode_emoji: false,
903 double_width: false,
904 sync_output: false,
905 osc8_hyperlinks: false,
906 scroll_region: false,
907 in_tmux: false,
908 in_screen: false,
909 in_zellij: false,
910 in_wezterm_mux: false,
911 kitty_keyboard: false,
912 focus_events: false,
913 bracketed_paste: false,
914 mouse_sgr: false,
915 osc52_clipboard: false,
916 },
917 }
918 }
919
920 pub fn from_profile(profile: TerminalProfile) -> Self {
922 let mut caps = TerminalCapabilities::from_profile(profile);
923 caps.profile = TerminalProfile::Custom;
924 Self { caps }
925 }
926
927 #[must_use]
929 pub fn build(self) -> TerminalCapabilities {
930 self.caps
931 }
932
933 pub const fn color_depth(mut self, depth: ColorDepth) -> Self {
937 self.caps.color_depth = depth;
938 self
939 }
940
941 pub const fn sync_output(mut self, enabled: bool) -> Self {
945 self.caps.sync_output = enabled;
946 self
947 }
948
949 pub const fn osc8_hyperlinks(mut self, enabled: bool) -> Self {
951 self.caps.osc8_hyperlinks = enabled;
952 self
953 }
954
955 pub const fn scroll_region(mut self, enabled: bool) -> Self {
957 self.caps.scroll_region = enabled;
958 self
959 }
960
961 pub const fn in_tmux(mut self, enabled: bool) -> Self {
965 self.caps.in_tmux = enabled;
966 self
967 }
968
969 pub const fn in_screen(mut self, enabled: bool) -> Self {
971 self.caps.in_screen = enabled;
972 self
973 }
974
975 pub const fn in_zellij(mut self, enabled: bool) -> Self {
977 self.caps.in_zellij = enabled;
978 self
979 }
980
981 pub const fn in_wezterm_mux(mut self, enabled: bool) -> Self {
983 self.caps.in_wezterm_mux = enabled;
984 self
985 }
986
987 pub const fn kitty_keyboard(mut self, enabled: bool) -> Self {
991 self.caps.kitty_keyboard = enabled;
992 self
993 }
994
995 pub const fn focus_events(mut self, enabled: bool) -> Self {
997 self.caps.focus_events = enabled;
998 self
999 }
1000
1001 pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
1003 self.caps.bracketed_paste = enabled;
1004 self
1005 }
1006
1007 pub const fn mouse_sgr(mut self, enabled: bool) -> Self {
1009 self.caps.mouse_sgr = enabled;
1010 self
1011 }
1012
1013 pub const fn osc52_clipboard(mut self, enabled: bool) -> Self {
1017 self.caps.osc52_clipboard = enabled;
1018 self
1019 }
1020}
1021
1022impl TerminalCapabilities {
1023 #[must_use]
1029 pub fn detect() -> Self {
1030 let value = env::var("FTUI_TEST_PROFILE").ok();
1031 Self::detect_with_test_profile_override(value.as_deref())
1032 }
1033
1034 fn detect_with_test_profile_override(value: Option<&str>) -> Self {
1035 if let Some(value) = value
1036 && let Ok(profile) = TerminalProfile::from_str(value.trim())
1037 && profile != TerminalProfile::Detected
1038 {
1039 return Self::from_profile(profile);
1040 }
1041 let env = DetectInputs::from_env();
1042 Self::detect_from_inputs(&env)
1043 }
1044
1045 fn detect_from_inputs(env: &DetectInputs) -> Self {
1046 let term = normalize_terminal_env_value(&env.term);
1047 let term_program = normalize_terminal_env_value(&env.term_program);
1048 let colorterm = normalize_terminal_env_value(&env.colorterm);
1049
1050 let in_tmux = env.in_tmux || term.starts_with("tmux");
1057 let in_screen = env.in_screen || term.starts_with("screen");
1058 let in_zellij = env.in_zellij;
1059
1060 let term_program_is_wezterm = term_program.contains("wezterm");
1064 let term_is_wezterm = term.contains("wezterm");
1065 let in_wezterm_mux = term_program_is_wezterm
1066 || term_is_wezterm
1067 || env.wezterm_unix_socket
1068 || env.wezterm_pane
1069 || env.wezterm_executable;
1070 let in_any_mux = in_tmux || in_screen || in_zellij || in_wezterm_mux;
1071
1072 let is_windows_terminal = env.wt_session;
1074
1075 let is_dumb = is_dumb_terminal(&term, is_windows_terminal);
1080
1081 let is_kitty = env.kitty_window_id || term.contains("kitty");
1083
1084 let is_modern_terminal = MODERN_TERMINALS.iter().any(|t| {
1086 let t_lower = t.to_ascii_lowercase();
1087 term_program.contains(&t_lower) || term.contains(&t_lower)
1088 }) || is_windows_terminal;
1089
1090 let color_depth = ColorDepth::detect_normalized(
1091 env.no_color,
1092 is_dumb,
1093 &colorterm,
1094 &term,
1095 is_modern_terminal || is_kitty,
1096 );
1097
1098 let is_wezterm = term_program_is_wezterm || term_is_wezterm || env.wezterm_executable;
1101
1102 let sync_output = !is_dumb
1104 && !is_wezterm
1105 && (is_kitty
1106 || SYNC_OUTPUT_TERMINALS.iter().any(|t| {
1107 let t_lower = t.to_ascii_lowercase();
1108 term_program.contains(&t_lower)
1109 }));
1110
1111 let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
1113
1114 let scroll_region = !is_dumb;
1116
1117 let kitty_keyboard = !is_dumb
1122 && (is_kitty
1123 || KITTY_KEYBOARD_TERMINALS.iter().any(|t| {
1124 let t_lower = t.to_ascii_lowercase();
1125 term_program.contains(&t_lower) || term.contains(&t_lower)
1126 }));
1127
1128 let focus_events = !is_dumb && (is_modern_terminal || is_kitty);
1130
1131 let bracketed_paste = !is_dumb;
1133
1134 let mouse_sgr = !is_dumb;
1136
1137 let osc52_clipboard = !is_dumb && !in_any_mux && (is_modern_terminal || is_kitty);
1139
1140 let unicode_box_drawing = !is_dumb;
1142 let unicode_emoji = !is_dumb && (is_modern_terminal || is_kitty);
1143 let double_width = !is_dumb && term != "linux";
1144
1145 Self {
1146 profile: TerminalProfile::Detected,
1147 color_depth,
1148 unicode_box_drawing,
1149 unicode_emoji,
1150 double_width,
1151 sync_output,
1152 osc8_hyperlinks,
1153 scroll_region,
1154 in_tmux,
1155 in_screen,
1156 in_zellij,
1157 in_wezterm_mux,
1158 kitty_keyboard,
1159 focus_events,
1160 bracketed_paste,
1161 mouse_sgr,
1162 osc52_clipboard,
1163 }
1164 }
1165
1166 #[must_use]
1171 pub const fn basic() -> Self {
1172 Self {
1173 profile: TerminalProfile::Dumb,
1174 color_depth: ColorDepth::Mono,
1175 unicode_box_drawing: false,
1176 unicode_emoji: false,
1177 double_width: false,
1178 sync_output: false,
1179 osc8_hyperlinks: false,
1180 scroll_region: false,
1181 in_tmux: false,
1182 in_screen: false,
1183 in_zellij: false,
1184 in_wezterm_mux: false,
1185 kitty_keyboard: false,
1186 focus_events: false,
1187 bracketed_paste: false,
1188 mouse_sgr: false,
1189 osc52_clipboard: false,
1190 }
1191 }
1192
1193 #[must_use]
1197 #[inline]
1198 pub const fn in_any_mux(&self) -> bool {
1199 self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux
1200 }
1201
1202 #[must_use]
1204 #[inline]
1205 pub const fn has_color(&self) -> bool {
1206 self.color_depth.supports_color()
1207 }
1208
1209 #[must_use]
1211 pub const fn supports_256_colors(&self) -> bool {
1212 self.color_depth.supports_256_colors()
1213 }
1214
1215 #[must_use]
1217 pub const fn supports_true_color(&self) -> bool {
1218 self.color_depth.supports_true_color()
1219 }
1220
1221 #[must_use]
1232 #[inline]
1233 pub const fn use_sync_output(&self) -> bool {
1234 if self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux {
1235 return false;
1236 }
1237 self.sync_output
1238 }
1239
1240 #[must_use]
1245 #[inline]
1246 pub const fn use_scroll_region(&self) -> bool {
1247 if self.in_any_mux() {
1248 return false;
1249 }
1250 self.scroll_region
1251 }
1252
1253 #[must_use]
1258 #[inline]
1259 pub const fn use_hyperlinks(&self) -> bool {
1260 if self.in_any_mux() {
1261 return false;
1262 }
1263 self.osc8_hyperlinks
1264 }
1265
1266 #[must_use]
1271 #[inline]
1272 pub const fn use_clipboard(&self) -> bool {
1273 if self.in_any_mux() {
1274 return false;
1275 }
1276 self.osc52_clipboard
1277 }
1278
1279 #[must_use]
1285 #[inline]
1286 pub const fn needs_passthrough_wrap(&self) -> bool {
1287 self.in_tmux || self.in_screen
1288 }
1289}
1290
1291pub struct SharedCapabilities {
1314 inner: crate::read_optimized::ArcSwapStore<TerminalCapabilities>,
1315}
1316
1317impl SharedCapabilities {
1318 pub fn new(caps: TerminalCapabilities) -> Self {
1320 Self {
1321 inner: crate::read_optimized::ArcSwapStore::new(caps),
1322 }
1323 }
1324
1325 pub fn detect() -> Self {
1327 Self::new(TerminalCapabilities::detect())
1328 }
1329
1330 #[inline]
1332 pub fn load(&self) -> TerminalCapabilities {
1333 crate::read_optimized::ReadOptimized::load(&self.inner)
1334 }
1335
1336 #[inline]
1338 pub fn store(&self, caps: TerminalCapabilities) {
1339 crate::read_optimized::ReadOptimized::store(&self.inner, caps);
1340 }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345 use super::*;
1346
1347 fn detect_with_override(value: Option<&str>) -> TerminalCapabilities {
1348 TerminalCapabilities::detect_with_test_profile_override(value)
1349 }
1350
1351 #[test]
1352 fn basic_is_minimal() {
1353 let caps = TerminalCapabilities::basic();
1354 assert_eq!(caps.color_depth, ColorDepth::Mono);
1355 assert!(!caps.sync_output);
1356 assert!(!caps.osc8_hyperlinks);
1357 assert!(!caps.scroll_region);
1358 assert!(!caps.in_tmux);
1359 assert!(!caps.in_screen);
1360 assert!(!caps.in_zellij);
1361 assert!(!caps.kitty_keyboard);
1362 assert!(!caps.focus_events);
1363 assert!(!caps.bracketed_paste);
1364 assert!(!caps.mouse_sgr);
1365 assert!(!caps.osc52_clipboard);
1366 }
1367
1368 #[test]
1369 fn basic_is_default() {
1370 let basic = TerminalCapabilities::basic();
1371 let default = TerminalCapabilities::default();
1372 assert_eq!(basic, default);
1373 }
1374
1375 #[test]
1376 fn in_any_mux_logic() {
1377 let mut caps = TerminalCapabilities::basic();
1378 assert!(!caps.in_any_mux());
1379
1380 caps.in_tmux = true;
1381 assert!(caps.in_any_mux());
1382
1383 caps.in_tmux = false;
1384 caps.in_screen = true;
1385 assert!(caps.in_any_mux());
1386
1387 caps.in_screen = false;
1388 caps.in_zellij = true;
1389 assert!(caps.in_any_mux());
1390
1391 caps.in_zellij = false;
1392 caps.in_wezterm_mux = true;
1393 assert!(caps.in_any_mux());
1394 }
1395
1396 #[test]
1397 fn has_color_logic() {
1398 let mut caps = TerminalCapabilities::basic();
1399 assert!(!caps.has_color());
1400
1401 caps.color_depth = ColorDepth::Ansi16;
1402 assert!(caps.has_color());
1403
1404 caps.color_depth = ColorDepth::TrueColor;
1405 assert!(caps.has_color());
1406 }
1407
1408 #[test]
1409 fn color_depth_identifiers() {
1410 let mut caps = TerminalCapabilities::basic();
1411 assert_eq!(caps.color_depth.as_str(), "mono");
1412
1413 caps.color_depth = ColorDepth::Ansi16;
1414 assert_eq!(caps.color_depth.as_str(), "ansi16");
1415
1416 caps.color_depth = ColorDepth::Ansi256;
1417 assert_eq!(caps.color_depth.as_str(), "ansi256");
1418
1419 caps.color_depth = ColorDepth::TrueColor;
1420 assert_eq!(caps.color_depth.as_str(), "truecolor");
1421 }
1422
1423 #[test]
1424 fn explicit_color_depth_detection_contract() {
1425 let cases = [
1426 (Some(""), Some("truecolor"), Some("xterm"), ColorDepth::Mono),
1427 (None, Some("truecolor"), Some("dumb"), ColorDepth::Mono),
1428 (None, Some("truecolor"), Some(" DUMB "), ColorDepth::Mono),
1429 (None, Some("truecolor"), Some("vt100"), ColorDepth::Mono),
1430 (None, None, Some("xterm"), ColorDepth::Ansi16),
1431 (None, None, Some("xterm-256color"), ColorDepth::Ansi256),
1432 (None, Some("24bit"), Some("xterm"), ColorDepth::TrueColor),
1433 (None, None, Some(" XTERM-DIRECT "), ColorDepth::TrueColor),
1434 (None, None, Some("xterm-direct2"), ColorDepth::TrueColor),
1435 (None, None, Some("xterm-direct16"), ColorDepth::TrueColor),
1436 (None, None, Some("xterm-direct256"), ColorDepth::TrueColor),
1437 (None, None, Some("xterm-truecolor"), ColorDepth::TrueColor),
1438 (None, None, Some("xterm-24bit"), ColorDepth::TrueColor),
1439 (None, None, Some("xterm-no24bit"), ColorDepth::Ansi16),
1440 (
1441 None,
1442 Some("nottruecolor"),
1443 Some("xterm"),
1444 ColorDepth::Ansi16,
1445 ),
1446 (None, Some("no24bit"), Some("xterm"), ColorDepth::Ansi16),
1447 (None, Some("truecolor"), Some("linux"), ColorDepth::Ansi16),
1448 ];
1449
1450 for (no_color, colorterm, term, expected) in cases {
1451 assert_eq!(
1452 ColorDepth::detect_from_env(no_color, colorterm, term),
1453 expected,
1454 "NO_COLOR={no_color:?} COLORTERM={colorterm:?} TERM={term:?}"
1455 );
1456 }
1457 }
1458
1459 #[test]
1460 fn detect_does_not_panic() {
1461 let _caps = TerminalCapabilities::detect();
1463 }
1464
1465 #[test]
1466 fn windows_terminal_not_dumb_when_term_missing() {
1467 let env = DetectInputs {
1468 no_color: false,
1469 term: String::new(),
1470 term_program: String::new(),
1471 colorterm: String::new(),
1472 in_tmux: false,
1473 in_screen: false,
1474 in_zellij: false,
1475 wezterm_unix_socket: false,
1476 wezterm_pane: false,
1477 wezterm_executable: false,
1478 kitty_window_id: false,
1479 wt_session: true,
1480 };
1481
1482 let caps = TerminalCapabilities::detect_from_inputs(&env);
1483 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1484 assert!(
1485 caps.osc8_hyperlinks,
1486 "WT_SESSION implies OSC 8 hyperlink support by default"
1487 );
1488 assert!(
1489 caps.bracketed_paste,
1490 "WT_SESSION should not be treated as dumb"
1491 );
1492 assert!(caps.mouse_sgr, "WT_SESSION should not be treated as dumb");
1493 }
1494
1495 #[test]
1496 #[cfg(target_os = "windows")]
1497 fn detect_windows_terminal_from_wt_session() {
1498 let mut env = make_env("", "", "");
1499 env.wt_session = true;
1500 let caps = TerminalCapabilities::detect_from_inputs(&env);
1501 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1502 assert!(caps.osc8_hyperlinks, "WT_SESSION implies OSC 8 support");
1503 }
1504
1505 #[test]
1506 fn no_color_disables_color_and_links() {
1507 let env = DetectInputs {
1508 no_color: true,
1509 term: "xterm-256color".to_string(),
1510 term_program: "WezTerm".to_string(),
1511 colorterm: "truecolor".to_string(),
1512 in_tmux: false,
1513 in_screen: false,
1514 in_zellij: false,
1515 wezterm_unix_socket: false,
1516 wezterm_pane: false,
1517 wezterm_executable: false,
1518 kitty_window_id: false,
1519 wt_session: false,
1520 };
1521
1522 let caps = TerminalCapabilities::detect_from_inputs(&env);
1523 assert_eq!(caps.color_depth, ColorDepth::Mono);
1524 assert!(
1525 !caps.osc8_hyperlinks,
1526 "NO_COLOR must disable OSC 8 hyperlinks"
1527 );
1528 }
1529
1530 #[cfg(unix)]
1531 #[test]
1532 fn non_utf8_no_color_presence_disables_color_without_global_env_mutation() {
1533 use std::os::unix::ffi::OsStringExt as _;
1534
1535 let non_utf8 = OsString::from_vec(vec![0xff]);
1536 let env = DetectInputs::from_env_with(|key| match key {
1537 "NO_COLOR" => Some(non_utf8.clone()),
1538 "TERM" => Some(OsString::from("xterm-direct")),
1539 "TERM_PROGRAM" => Some(OsString::from("WezTerm")),
1540 "COLORTERM" => Some(OsString::from("truecolor")),
1541 _ => None,
1542 });
1543
1544 assert!(env.no_color, "NO_COLOR is a presence-only contract");
1545 let caps = TerminalCapabilities::detect_from_inputs(&env);
1546 assert_eq!(caps.color_depth, ColorDepth::Mono);
1547 assert!(!caps.osc8_hyperlinks);
1548 }
1549
1550 #[test]
1553 fn use_sync_output_disabled_in_tmux() {
1554 let mut caps = TerminalCapabilities::basic();
1555 caps.sync_output = true;
1556 assert!(caps.use_sync_output());
1557
1558 caps.in_tmux = true;
1559 assert!(!caps.use_sync_output());
1560 }
1561
1562 #[test]
1563 fn use_sync_output_disabled_in_screen() {
1564 let mut caps = TerminalCapabilities::basic();
1565 caps.sync_output = true;
1566 caps.in_screen = true;
1567 assert!(!caps.use_sync_output());
1568 }
1569
1570 #[test]
1571 fn use_sync_output_disabled_in_zellij() {
1572 let mut caps = TerminalCapabilities::basic();
1573 caps.sync_output = true;
1574 caps.in_zellij = true;
1575 assert!(!caps.use_sync_output());
1576 }
1577
1578 #[test]
1579 fn use_sync_output_disabled_in_wezterm_mux() {
1580 let mut caps = TerminalCapabilities::basic();
1581 caps.sync_output = true;
1582 caps.in_wezterm_mux = true;
1583 assert!(!caps.use_sync_output());
1584 }
1585
1586 #[test]
1587 fn use_scroll_region_disabled_in_mux() {
1588 let mut caps = TerminalCapabilities::basic();
1589 caps.scroll_region = true;
1590 assert!(caps.use_scroll_region());
1591
1592 caps.in_tmux = true;
1593 assert!(!caps.use_scroll_region());
1594
1595 caps.in_tmux = false;
1596 caps.in_screen = true;
1597 assert!(!caps.use_scroll_region());
1598
1599 caps.in_screen = false;
1600 caps.in_zellij = true;
1601 assert!(!caps.use_scroll_region());
1602
1603 caps.in_zellij = false;
1604 caps.in_wezterm_mux = true;
1605 assert!(!caps.use_scroll_region());
1606 }
1607
1608 #[test]
1609 fn use_hyperlinks_disabled_in_mux() {
1610 let mut caps = TerminalCapabilities::basic();
1611 caps.osc8_hyperlinks = true;
1612 assert!(caps.use_hyperlinks());
1613
1614 caps.in_tmux = true;
1615 assert!(!caps.use_hyperlinks());
1616
1617 caps.in_tmux = false;
1618 caps.in_wezterm_mux = true;
1619 assert!(!caps.use_hyperlinks());
1620 }
1621
1622 #[test]
1623 fn use_clipboard_disabled_in_mux() {
1624 let mut caps = TerminalCapabilities::basic();
1625 caps.osc52_clipboard = true;
1626 assert!(caps.use_clipboard());
1627
1628 caps.in_screen = true;
1629 assert!(!caps.use_clipboard());
1630
1631 caps.in_screen = false;
1632 caps.in_wezterm_mux = true;
1633 assert!(!caps.use_clipboard());
1634 }
1635
1636 #[test]
1637 fn needs_passthrough_wrap_only_for_tmux_screen() {
1638 let mut caps = TerminalCapabilities::basic();
1639 assert!(!caps.needs_passthrough_wrap());
1640
1641 caps.in_tmux = true;
1642 assert!(caps.needs_passthrough_wrap());
1643
1644 caps.in_tmux = false;
1645 caps.in_screen = true;
1646 assert!(caps.needs_passthrough_wrap());
1647
1648 caps.in_screen = false;
1650 caps.in_zellij = true;
1651 assert!(!caps.needs_passthrough_wrap());
1652 }
1653
1654 #[test]
1655 fn policies_return_false_when_capability_absent() {
1656 let caps = TerminalCapabilities::basic();
1658 assert!(!caps.use_sync_output());
1659 assert!(!caps.use_scroll_region());
1660 assert!(!caps.use_hyperlinks());
1661 assert!(!caps.use_clipboard());
1662 }
1663
1664 fn make_env(term: &str, term_program: &str, colorterm: &str) -> DetectInputs {
1667 DetectInputs {
1668 no_color: false,
1669 term: term.to_string(),
1670 term_program: term_program.to_string(),
1671 colorterm: colorterm.to_string(),
1672 in_tmux: false,
1673 in_screen: false,
1674 in_zellij: false,
1675 wezterm_unix_socket: false,
1676 wezterm_pane: false,
1677 wezterm_executable: false,
1678 kitty_window_id: false,
1679 wt_session: false,
1680 }
1681 }
1682
1683 #[test]
1684 fn detect_dumb_terminal() {
1685 for term in ["dumb", "DUMB", " dumb "] {
1686 let env = make_env(term, " WezTerm ", " TRUECOLOR ");
1687 let caps = TerminalCapabilities::detect_from_inputs(&env);
1688 assert_eq!(caps.color_depth, ColorDepth::Mono, "TERM={term:?}");
1689 assert!(!caps.sync_output, "TERM={term:?}");
1690 assert!(!caps.osc8_hyperlinks, "TERM={term:?}");
1691 assert!(!caps.scroll_region, "TERM={term:?}");
1692 assert!(!caps.focus_events, "TERM={term:?}");
1693 assert!(!caps.bracketed_paste, "TERM={term:?}");
1694 assert!(!caps.mouse_sgr, "TERM={term:?}");
1695 }
1696 }
1697
1698 #[test]
1699 fn detect_dumb_overrides_truecolor_env() {
1700 let env = make_env("dumb", "WezTerm", "truecolor");
1701 let caps = TerminalCapabilities::detect_from_inputs(&env);
1702 assert_eq!(caps.color_depth, ColorDepth::Mono);
1703 assert!(!caps.bracketed_paste);
1704 assert!(!caps.mouse_sgr);
1705 assert!(!caps.osc8_hyperlinks);
1706 }
1707
1708 #[test]
1709 fn detect_dumb_disables_kitty_keyboard() {
1710 for term_program in ["kitty", "ghostty", "iTerm.app", "WezTerm"] {
1714 let env = make_env("dumb", term_program, "truecolor");
1715 let caps = TerminalCapabilities::detect_from_inputs(&env);
1716 assert!(
1717 !caps.kitty_keyboard,
1718 "TERM=dumb + TERM_PROGRAM={term_program} must disable kitty keyboard"
1719 );
1720 }
1721 let env = make_env("xterm-kitty", "kitty", "truecolor");
1723 let caps = TerminalCapabilities::detect_from_inputs(&env);
1724 assert!(caps.kitty_keyboard);
1725 }
1726
1727 #[test]
1728 fn detect_term_identity_counts_as_mux_evidence() {
1729 let env = make_env("tmux-256color", "", "");
1734 let caps = TerminalCapabilities::detect_from_inputs(&env);
1735 assert!(caps.in_tmux, "TERM=tmux-256color implies tmux");
1736 assert!(caps.in_any_mux());
1737 assert!(!caps.use_scroll_region());
1738
1739 let env = make_env("screen-256color", "", "");
1740 let caps = TerminalCapabilities::detect_from_inputs(&env);
1741 assert!(caps.in_screen, "TERM=screen-256color implies screen");
1742 assert!(caps.in_any_mux());
1743 assert!(!caps.use_scroll_region());
1744
1745 let env = make_env("xterm-256color", "", "");
1747 let caps = TerminalCapabilities::detect_from_inputs(&env);
1748 assert!(!caps.in_tmux);
1749 assert!(!caps.in_screen);
1750 }
1751
1752 #[test]
1753 fn from_profile_custom_roundtrips() {
1754 let caps = TerminalCapabilities::from_profile(TerminalProfile::Custom);
1755 assert_eq!(caps.profile(), TerminalProfile::Custom);
1756 let basic = TerminalCapabilities::basic();
1758 assert_eq!(caps.color_depth, basic.color_depth);
1759 assert_eq!(caps.scroll_region, basic.scroll_region);
1760 }
1761
1762 #[test]
1763 fn detect_empty_term_is_dumb() {
1764 let env = make_env("", "", "");
1765 let caps = TerminalCapabilities::detect_from_inputs(&env);
1766 assert_eq!(caps.color_depth, ColorDepth::Mono);
1767 assert!(!caps.bracketed_paste);
1768 }
1769
1770 #[test]
1771 fn detect_xterm_256color() {
1772 let env = make_env("xterm-256color", "", "");
1773 let caps = TerminalCapabilities::detect_from_inputs(&env);
1774 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
1775 assert!(caps.bracketed_paste);
1776 assert!(caps.mouse_sgr);
1777 assert!(caps.scroll_region);
1778 }
1779
1780 #[test]
1781 fn detect_colorterm_truecolor() {
1782 let env = make_env("xterm-256color", "", "truecolor");
1783 let caps = TerminalCapabilities::detect_from_inputs(&env);
1784 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1785 }
1786
1787 #[test]
1788 fn detect_colorterm_24bit() {
1789 let env = make_env("xterm-256color", "", "24bit");
1790 let caps = TerminalCapabilities::detect_from_inputs(&env);
1791 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1792 }
1793
1794 #[test]
1795 fn detect_kitty_by_window_id() {
1796 let mut env = make_env("xterm-kitty", "", "");
1797 env.kitty_window_id = true;
1798 let caps = TerminalCapabilities::detect_from_inputs(&env);
1799 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1800 assert!(
1801 caps.kitty_keyboard,
1802 "Kitty supports kitty keyboard protocol"
1803 );
1804 assert!(caps.sync_output, "Kitty supports sync output");
1805 }
1806
1807 #[test]
1808 fn detect_kitty_by_term() {
1809 let env = make_env("xterm-kitty", "", "");
1810 let caps = TerminalCapabilities::detect_from_inputs(&env);
1811 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1812 assert!(caps.kitty_keyboard);
1813 }
1814
1815 #[test]
1816 fn detect_wezterm() {
1817 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1818 let caps = TerminalCapabilities::detect_from_inputs(&env);
1819 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1820 assert!(
1821 caps.in_wezterm_mux,
1822 "WezTerm identity is treated as conservative mux evidence"
1823 );
1824 assert!(caps.in_any_mux());
1825 assert!(
1826 !caps.sync_output,
1827 "WezTerm sync output is hard-disabled as a safety fallback"
1828 );
1829 assert!(caps.osc8_hyperlinks, "WezTerm supports hyperlinks");
1830 assert!(caps.kitty_keyboard, "WezTerm supports kitty keyboard");
1831 assert!(caps.focus_events);
1832 assert!(
1833 !caps.osc52_clipboard,
1834 "conservative mux policy should disable raw OSC52 detection"
1835 );
1836 assert!(!caps.use_scroll_region());
1837 assert!(!caps.use_hyperlinks());
1838 assert!(!caps.use_clipboard());
1839 }
1840
1841 #[test]
1842 fn detect_wezterm_mux_socket_disables_sync_policy() {
1843 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1844 env.wezterm_unix_socket = true;
1845 let caps = TerminalCapabilities::detect_from_inputs(&env);
1846 assert!(!caps.sync_output);
1847 assert!(caps.in_wezterm_mux, "wezterm mux marker should be detected");
1848 assert!(
1849 caps.in_any_mux(),
1850 "wezterm mux must participate in in_any_mux()"
1851 );
1852 assert!(
1853 !caps.use_sync_output(),
1854 "policy must suppress sync output in wezterm mux sessions"
1855 );
1856 assert!(
1857 !caps.use_scroll_region(),
1858 "policy must suppress scroll region in wezterm mux sessions"
1859 );
1860 assert!(
1861 !caps.use_hyperlinks(),
1862 "policy must suppress hyperlinks in wezterm mux sessions"
1863 );
1864 assert!(
1865 !caps.use_clipboard(),
1866 "policy must suppress clipboard in wezterm mux sessions"
1867 );
1868 }
1869
1870 #[test]
1871 fn detect_wezterm_mux_socket_without_term_program_disables_sync_policy() {
1872 let mut env = make_env("xterm-256color", "", "truecolor");
1873 env.wezterm_unix_socket = true;
1874 let caps = TerminalCapabilities::detect_from_inputs(&env);
1875 assert!(
1876 caps.in_wezterm_mux,
1877 "socket marker alone must detect wezterm mux"
1878 );
1879 assert!(
1880 caps.in_any_mux(),
1881 "wezterm mux must participate in in_any_mux()"
1882 );
1883 assert!(
1884 !caps.use_sync_output(),
1885 "policy must suppress sync output when wezterm mux socket is present"
1886 );
1887 }
1888
1889 #[test]
1890 fn detect_wezterm_mux_pane_disables_sync_policy() {
1891 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1892 env.wezterm_pane = true;
1893 let caps = TerminalCapabilities::detect_from_inputs(&env);
1894 assert!(!caps.sync_output);
1895 assert!(
1896 caps.in_wezterm_mux,
1897 "wezterm pane marker should be detected"
1898 );
1899 assert!(
1900 caps.in_any_mux(),
1901 "wezterm mux must participate in in_any_mux()"
1902 );
1903 assert!(
1904 !caps.use_sync_output(),
1905 "policy must suppress sync output when wezterm pane marker is present"
1906 );
1907 }
1908
1909 #[test]
1910 fn detect_wezterm_mux_pane_without_term_program_disables_sync_policy() {
1911 let mut env = make_env("xterm-256color", "", "truecolor");
1912 env.wezterm_pane = true;
1913 let caps = TerminalCapabilities::detect_from_inputs(&env);
1914 assert!(
1915 caps.in_wezterm_mux,
1916 "pane marker alone must detect wezterm mux"
1917 );
1918 assert!(
1919 caps.in_any_mux(),
1920 "wezterm mux must participate in in_any_mux()"
1921 );
1922 assert!(
1923 !caps.use_sync_output(),
1924 "policy must suppress sync output when wezterm pane marker is present"
1925 );
1926 }
1927
1928 #[test]
1929 fn detect_wezterm_executable_without_term_program_is_conservative_mux() {
1930 let mut env = make_env("xterm-256color", "", "truecolor");
1931 env.wezterm_executable = true;
1932 let caps = TerminalCapabilities::detect_from_inputs(&env);
1933 assert!(
1934 caps.in_wezterm_mux,
1935 "WEZTERM_EXECUTABLE fallback should conservatively mark mux context"
1936 );
1937 assert!(
1938 !caps.use_sync_output(),
1939 "fallback mux detection must suppress sync output policy"
1940 );
1941 }
1942
1943 #[test]
1944 fn detect_wezterm_executable_overrides_explicit_non_wezterm_program() {
1945 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1946 env.wezterm_executable = true;
1947 let caps = TerminalCapabilities::detect_from_inputs(&env);
1948 assert!(
1949 caps.in_wezterm_mux,
1950 "WEZTERM_EXECUTABLE should conservatively force wezterm mux policy"
1951 );
1952 assert!(
1953 !caps.sync_output,
1954 "raw sync_output capability should be disabled under conservative wezterm marker handling"
1955 );
1956 assert!(
1957 !caps.use_sync_output(),
1958 "mux policy should disable sync output under WEZTERM_EXECUTABLE marker"
1959 );
1960 }
1961
1962 #[test]
1963 fn detect_wezterm_socket_overrides_explicit_non_wezterm_program() {
1964 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1965 env.wezterm_unix_socket = true;
1966 let caps = TerminalCapabilities::detect_from_inputs(&env);
1967 assert!(
1968 caps.in_wezterm_mux,
1969 "WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
1970 );
1971 assert!(
1972 !caps.use_sync_output(),
1973 "mux policy should disable sync output with socket marker"
1974 );
1975 }
1976
1977 #[test]
1978 fn detect_wezterm_pane_overrides_explicit_non_wezterm_program() {
1979 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1980 env.wezterm_pane = true;
1981 let caps = TerminalCapabilities::detect_from_inputs(&env);
1982 assert!(
1983 caps.in_wezterm_mux,
1984 "WEZTERM_PANE should conservatively force wezterm mux policy"
1985 );
1986 assert!(
1987 !caps.use_sync_output(),
1988 "mux policy should disable sync output with pane marker"
1989 );
1990 }
1991
1992 #[test]
1993 fn detect_wezterm_socket_overrides_explicit_non_wezterm_term_identity() {
1994 let mut env = make_env("xterm-ghostty", "", "truecolor");
1995 env.wezterm_unix_socket = true;
1996 let caps = TerminalCapabilities::detect_from_inputs(&env);
1997 assert!(
1998 caps.in_wezterm_mux,
1999 "WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
2000 );
2001 assert!(
2002 !caps.use_sync_output(),
2003 "mux policy should disable sync output with socket marker"
2004 );
2005 }
2006
2007 #[test]
2008 #[cfg(target_os = "macos")]
2009 fn detect_iterm2_from_term_program() {
2010 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
2011 let caps = TerminalCapabilities::detect_from_inputs(&env);
2012 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2013 assert!(caps.osc8_hyperlinks, "iTerm2 supports OSC 8 hyperlinks");
2014 }
2015
2016 #[test]
2017 fn detect_alacritty() {
2018 let env = make_env("alacritty", "Alacritty", "truecolor");
2019 let caps = TerminalCapabilities::detect_from_inputs(&env);
2020 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2021 assert!(caps.sync_output);
2022 assert!(caps.osc8_hyperlinks);
2023 assert!(caps.kitty_keyboard);
2024 assert!(caps.focus_events);
2025 }
2026
2027 #[test]
2028 fn detect_ghostty() {
2029 let env = make_env("xterm-ghostty", "Ghostty", "truecolor");
2030 let caps = TerminalCapabilities::detect_from_inputs(&env);
2031 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2032 assert!(caps.sync_output);
2033 assert!(caps.osc8_hyperlinks);
2034 assert!(caps.kitty_keyboard);
2035 assert!(caps.focus_events);
2036 }
2037
2038 #[test]
2039 fn detect_iterm() {
2040 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
2041 let caps = TerminalCapabilities::detect_from_inputs(&env);
2042 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2043 assert!(caps.osc8_hyperlinks);
2044 assert!(caps.kitty_keyboard);
2045 assert!(caps.focus_events);
2046 }
2047
2048 #[test]
2049 fn detect_vscode_terminal() {
2050 let env = make_env("xterm-256color", "vscode", "truecolor");
2051 let caps = TerminalCapabilities::detect_from_inputs(&env);
2052 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2053 assert!(caps.osc8_hyperlinks);
2054 assert!(caps.focus_events);
2055 }
2056
2057 #[test]
2060 fn detect_in_tmux() {
2061 let mut env = make_env("screen-256color", "", "");
2062 env.in_tmux = true;
2063 let caps = TerminalCapabilities::detect_from_inputs(&env);
2064 assert!(caps.in_tmux);
2065 assert!(caps.in_any_mux());
2066 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2067 assert!(!caps.osc52_clipboard, "clipboard disabled in tmux");
2068 }
2069
2070 #[test]
2071 fn detect_in_screen() {
2072 let mut env = make_env("screen", "", "");
2073 env.in_screen = true;
2074 let caps = TerminalCapabilities::detect_from_inputs(&env);
2075 assert!(caps.in_screen);
2076 assert!(caps.in_any_mux());
2077 assert!(caps.needs_passthrough_wrap());
2078 }
2079
2080 #[test]
2081 fn detect_in_zellij() {
2082 let mut env = make_env("xterm-256color", "", "truecolor");
2083 env.in_zellij = true;
2084 let caps = TerminalCapabilities::detect_from_inputs(&env);
2085 assert!(caps.in_zellij);
2086 assert!(caps.in_any_mux());
2087 assert!(
2088 !caps.needs_passthrough_wrap(),
2089 "Zellij handles passthrough natively"
2090 );
2091 assert!(!caps.osc52_clipboard, "clipboard disabled in mux");
2092 }
2093
2094 #[test]
2095 fn detect_modern_terminal_in_tmux() {
2096 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2097 env.in_tmux = true;
2098 let caps = TerminalCapabilities::detect_from_inputs(&env);
2099 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2101 assert!(!caps.sync_output);
2102 assert!(!caps.use_sync_output());
2104 assert!(!caps.use_hyperlinks());
2105 assert!(!caps.use_scroll_region());
2106 }
2107
2108 #[test]
2111 fn no_color_overrides_everything() {
2112 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2113 env.no_color = true;
2114 let caps = TerminalCapabilities::detect_from_inputs(&env);
2115 assert_eq!(caps.color_depth, ColorDepth::Mono);
2116 assert!(!caps.osc8_hyperlinks);
2117 assert!(!caps.sync_output);
2119 assert!(caps.bracketed_paste);
2120 assert!(caps.mouse_sgr);
2121 }
2122
2123 #[test]
2126 fn unknown_term_program() {
2127 let env = make_env("xterm", "SomeUnknownTerminal", "");
2128 let caps = TerminalCapabilities::detect_from_inputs(&env);
2129 assert!(
2130 !caps.supports_true_color(),
2131 "unknown terminal should not assume truecolor"
2132 );
2133 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2134 assert!(!caps.osc8_hyperlinks);
2135 assert!(caps.bracketed_paste);
2137 assert!(caps.mouse_sgr);
2138 assert!(caps.scroll_region);
2139 }
2140
2141 #[test]
2142 fn all_mux_flags_simultaneous() {
2143 let mut env = make_env("screen", "", "");
2144 env.in_tmux = true;
2145 env.in_screen = true;
2146 env.in_zellij = true;
2147 let caps = TerminalCapabilities::detect_from_inputs(&env);
2148 assert!(caps.in_any_mux());
2149 assert!(caps.needs_passthrough_wrap());
2150 assert!(!caps.use_sync_output());
2151 assert!(!caps.use_hyperlinks());
2152 assert!(!caps.use_clipboard());
2153 }
2154
2155 #[test]
2158 fn detect_rio() {
2159 let env = make_env("xterm-256color", "Rio", "truecolor");
2160 let caps = TerminalCapabilities::detect_from_inputs(&env);
2161 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2162 assert!(caps.osc8_hyperlinks);
2163 assert!(caps.kitty_keyboard);
2164 assert!(caps.focus_events);
2165 }
2166
2167 #[test]
2168 fn detect_contour() {
2169 let env = make_env("xterm-256color", "Contour", "truecolor");
2170 let caps = TerminalCapabilities::detect_from_inputs(&env);
2171 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2172 assert!(caps.sync_output);
2173 assert!(caps.osc8_hyperlinks);
2174 assert!(caps.focus_events);
2175 }
2176
2177 #[test]
2178 fn detect_foot() {
2179 let env = make_env("foot", "foot", "truecolor");
2180 let caps = TerminalCapabilities::detect_from_inputs(&env);
2181 assert!(caps.kitty_keyboard, "foot supports kitty keyboard");
2182 }
2183
2184 #[test]
2185 fn detect_hyper() {
2186 let env = make_env("xterm-256color", "Hyper", "truecolor");
2187 let caps = TerminalCapabilities::detect_from_inputs(&env);
2188 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2189 assert!(caps.osc8_hyperlinks);
2190 assert!(caps.focus_events);
2191 }
2192
2193 #[test]
2194 fn detect_linux_console() {
2195 let env = make_env("linux", "", "");
2196 let detected = TerminalCapabilities::detect_from_inputs(&env);
2197 let mut expected = TerminalCapabilities::linux_console();
2198 expected.profile = TerminalProfile::Detected;
2199
2200 assert_eq!(
2201 detected, expected,
2202 "the predefined Linux console profile must match TERM=linux detection"
2203 );
2204
2205 let policy = crate::glyph_policy::GlyphPolicy::from_env_with(
2206 |key| (key == "TERM").then(|| "linux".to_string()),
2207 &detected,
2208 );
2209 assert_eq!(policy.mode, crate::glyph_policy::GlyphMode::Ascii);
2210 assert!(!policy.double_width);
2211 }
2212
2213 #[test]
2214 fn detect_linux_console_caps_color_despite_conflicting_positive_signals() {
2215 let expected = TerminalCapabilities::linux_console().color_depth;
2216
2217 for (term_program, colorterm) in [
2218 ("WezTerm", "truecolor"),
2219 ("kitty", "24bit"),
2220 ("Ghostty", ""),
2221 ] {
2222 let env = make_env(" linux ", term_program, colorterm);
2223 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2224 let explicit = ColorDepth::detect_from_env(None, Some(colorterm), Some(" linux "));
2225
2226 assert_eq!(detected, expected, "TERM_PROGRAM={term_program:?}");
2227 assert_eq!(explicit, expected, "COLORTERM={colorterm:?}");
2228 }
2229 }
2230
2231 #[test]
2232 fn detect_direct_color_term_variants() {
2233 for term in [
2234 "xterm-direct",
2235 "xterm-direct2",
2236 "xterm-direct16",
2237 "xterm-direct256",
2238 "xterm-truecolor",
2239 "xterm-24bit",
2240 " XTERM-DIRECT ",
2241 ] {
2242 let env = make_env(term, "", "");
2243 let caps = TerminalCapabilities::detect_from_inputs(&env);
2244 assert_eq!(caps.color_depth, ColorDepth::TrueColor, "TERM={term:?}");
2245 assert!(caps.bracketed_paste, "TERM={term:?}");
2246 assert!(caps.mouse_sgr, "TERM={term:?}");
2247 }
2248 }
2249
2250 #[test]
2251 fn direct_color_detection_rejects_negated_and_partial_tokens() {
2252 for term in [
2253 "xterm-indirect",
2254 "xterm-direct-color",
2255 "xterm-no24bit",
2256 "xterm-24bit-disabled",
2257 ] {
2258 let env = make_env(term, "", "");
2259 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2260 let explicit = ColorDepth::detect_from_env(None, None, Some(term));
2261
2262 assert_eq!(detected, ColorDepth::Ansi16, "TERM={term:?}");
2263 assert_eq!(explicit, detected, "TERM={term:?}");
2264 }
2265
2266 for colorterm in [
2267 "nottruecolor",
2268 "truecolor-disabled",
2269 "no24bit",
2270 "24bit-disabled",
2271 ] {
2272 let env = make_env("xterm", "", colorterm);
2273 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2274 let explicit = ColorDepth::detect_from_env(None, Some(colorterm), Some("xterm"));
2275
2276 assert_eq!(detected, ColorDepth::Ansi16, "COLORTERM={colorterm:?}");
2277 assert_eq!(explicit, detected, "COLORTERM={colorterm:?}");
2278 }
2279 }
2280
2281 #[test]
2282 fn detect_plain_xterm_is_ansi16() {
2283 let env = make_env("xterm", "", "");
2284 let caps = TerminalCapabilities::detect_from_inputs(&env);
2285 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2286 assert!(caps.bracketed_paste);
2287 assert!(caps.mouse_sgr);
2288 }
2289
2290 #[test]
2291 fn explicit_and_full_color_detection_agree() {
2292 for (no_color, colorterm, term) in [
2293 (false, "", "xterm"),
2294 (false, "", "xterm-256color"),
2295 (false, "", " XTERM-DIRECT "),
2296 (false, "", "xterm-direct2"),
2297 (false, "", "xterm-direct16"),
2298 (false, "", "xterm-direct256"),
2299 (false, "24BIT", "xterm"),
2300 (false, "no24bit", "xterm"),
2301 (false, "truecolor", "linux"),
2302 (false, "truecolor", " VT100 "),
2303 (false, "truecolor", " DUMB "),
2304 (true, "truecolor", "xterm-direct"),
2305 ] {
2306 let mut env = make_env(term, "", colorterm);
2307 env.no_color = no_color;
2308 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2309 let explicit =
2310 ColorDepth::detect_from_env(no_color.then_some(""), Some(colorterm), Some(term));
2311 assert_eq!(
2312 detected, explicit,
2313 "NO_COLOR={no_color} COLORTERM={colorterm:?} TERM={term:?}"
2314 );
2315 }
2316 }
2317
2318 #[test]
2319 fn detect_vt100_is_monochrome() {
2320 let env = make_env("vt100", "", "");
2321 let caps = TerminalCapabilities::detect_from_inputs(&env);
2322 assert_eq!(caps.color_depth, ColorDepth::Mono);
2323 }
2324
2325 #[test]
2326 fn detect_screen_256color() {
2327 let env = make_env("screen-256color", "", "");
2328 let caps = TerminalCapabilities::detect_from_inputs(&env);
2329 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2330 }
2331
2332 #[test]
2335 fn wezterm_without_colorterm() {
2336 let env = make_env("xterm-256color", "WezTerm", "");
2337 let caps = TerminalCapabilities::detect_from_inputs(&env);
2338 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2340 assert!(!caps.sync_output);
2341 assert!(caps.osc8_hyperlinks);
2342 }
2343
2344 #[test]
2345 fn alacritty_via_term_only() {
2346 let env = make_env("alacritty", "", "");
2348 let caps = TerminalCapabilities::detect_from_inputs(&env);
2349 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2351 assert!(caps.osc8_hyperlinks);
2352 }
2353
2354 #[test]
2357 fn kitty_via_term_without_window_id() {
2358 let env = make_env("xterm-kitty", "", "");
2359 let caps = TerminalCapabilities::detect_from_inputs(&env);
2360 assert!(caps.kitty_keyboard);
2361 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2362 assert!(caps.sync_output);
2363 }
2364
2365 #[test]
2366 fn kitty_window_id_with_generic_term() {
2367 let mut env = make_env("xterm-256color", "", "");
2368 env.kitty_window_id = true;
2369 let caps = TerminalCapabilities::detect_from_inputs(&env);
2370 assert!(caps.kitty_keyboard);
2371 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2372 }
2373
2374 #[test]
2377 fn use_clipboard_enabled_when_no_mux_and_modern() {
2378 let env = make_env("xterm-256color", "Alacritty", "truecolor");
2379 let caps = TerminalCapabilities::detect_from_inputs(&env);
2380 assert!(caps.osc52_clipboard);
2381 assert!(caps.use_clipboard());
2382 }
2383
2384 #[test]
2385 fn use_clipboard_disabled_in_tmux_even_if_detected() {
2386 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2387 env.in_tmux = true;
2388 let caps = TerminalCapabilities::detect_from_inputs(&env);
2389 assert!(!caps.osc52_clipboard);
2391 assert!(!caps.use_clipboard());
2392 }
2393
2394 #[test]
2395 fn scroll_region_enabled_for_basic_xterm() {
2396 let env = make_env("xterm", "", "");
2397 let caps = TerminalCapabilities::detect_from_inputs(&env);
2398 assert!(caps.scroll_region);
2399 assert!(caps.use_scroll_region());
2400 }
2401
2402 #[test]
2403 fn no_color_preserves_non_visual_features() {
2404 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2405 env.no_color = true;
2406 let caps = TerminalCapabilities::detect_from_inputs(&env);
2407 assert_eq!(caps.color_depth, ColorDepth::Mono);
2409 assert!(!caps.osc8_hyperlinks);
2410 assert!(!caps.sync_output);
2412 assert!(caps.kitty_keyboard);
2413 assert!(caps.focus_events);
2414 assert!(caps.bracketed_paste);
2415 assert!(caps.mouse_sgr);
2416 }
2417
2418 #[test]
2421 fn colorterm_yes_not_truecolor() {
2422 let env = make_env("xterm-256color", "", "yes");
2423 let caps = TerminalCapabilities::detect_from_inputs(&env);
2424 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2425 }
2426
2427 #[test]
2430 fn profile_enum_as_str() {
2431 assert_eq!(TerminalProfile::Modern.as_str(), "modern");
2432 assert_eq!(TerminalProfile::Xterm256Color.as_str(), "xterm-256color");
2433 assert_eq!(TerminalProfile::Vt100.as_str(), "vt100");
2434 assert_eq!(TerminalProfile::Dumb.as_str(), "dumb");
2435 assert_eq!(TerminalProfile::Tmux.as_str(), "tmux");
2436 assert_eq!(TerminalProfile::Screen.as_str(), "screen");
2437 assert_eq!(TerminalProfile::Kitty.as_str(), "kitty");
2438 }
2439
2440 #[test]
2441 fn profile_enum_from_str() {
2442 use std::str::FromStr;
2443 assert_eq!(
2444 TerminalProfile::from_str("modern"),
2445 Ok(TerminalProfile::Modern)
2446 );
2447 assert_eq!(
2448 TerminalProfile::from_str("xterm-256color"),
2449 Ok(TerminalProfile::Xterm256Color)
2450 );
2451 assert_eq!(
2452 TerminalProfile::from_str("xterm256color"),
2453 Ok(TerminalProfile::Xterm256Color)
2454 );
2455 assert_eq!(TerminalProfile::from_str("DUMB"), Ok(TerminalProfile::Dumb));
2456 assert!(TerminalProfile::from_str("unknown").is_err());
2457 }
2458
2459 #[test]
2460 fn profile_all_predefined() {
2461 let all = TerminalProfile::all_predefined();
2462 assert!(all.len() >= 10);
2463 assert!(all.contains(&TerminalProfile::Modern));
2464 assert!(all.contains(&TerminalProfile::Dumb));
2465 assert!(!all.contains(&TerminalProfile::Custom));
2466 assert!(!all.contains(&TerminalProfile::Detected));
2467 }
2468
2469 #[test]
2470 fn profile_modern_has_all_features() {
2471 let caps = TerminalCapabilities::modern();
2472 assert_eq!(caps.profile(), TerminalProfile::Modern);
2473 assert_eq!(caps.profile_name(), Some("modern"));
2474 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2475 assert!(caps.sync_output);
2476 assert!(caps.osc8_hyperlinks);
2477 assert!(caps.scroll_region);
2478 assert!(caps.kitty_keyboard);
2479 assert!(caps.focus_events);
2480 assert!(caps.bracketed_paste);
2481 assert!(caps.mouse_sgr);
2482 assert!(caps.osc52_clipboard);
2483 assert!(!caps.in_any_mux());
2484 }
2485
2486 #[test]
2487 fn profile_xterm_256color() {
2488 let caps = TerminalCapabilities::xterm_256color();
2489 assert_eq!(caps.profile(), TerminalProfile::Xterm256Color);
2490 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2491 assert!(!caps.sync_output);
2492 assert!(!caps.osc8_hyperlinks);
2493 assert!(caps.scroll_region);
2494 assert!(caps.bracketed_paste);
2495 assert!(caps.mouse_sgr);
2496 }
2497
2498 #[test]
2499 fn profile_xterm_basic() {
2500 let caps = TerminalCapabilities::xterm();
2501 assert_eq!(caps.profile(), TerminalProfile::Xterm);
2502 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2503 assert!(caps.scroll_region);
2504 }
2505
2506 #[test]
2507 fn profile_vt100_minimal() {
2508 let caps = TerminalCapabilities::vt100();
2509 assert_eq!(caps.profile(), TerminalProfile::Vt100);
2510 assert_eq!(caps.color_depth, ColorDepth::Mono);
2511 assert!(caps.scroll_region);
2512 assert!(!caps.bracketed_paste);
2513 assert!(!caps.mouse_sgr);
2514 }
2515
2516 #[test]
2517 fn profile_dumb_no_features() {
2518 let caps = TerminalCapabilities::dumb();
2519 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2520 assert_eq!(caps.color_depth, ColorDepth::Mono);
2521 assert!(!caps.scroll_region);
2522 assert!(!caps.bracketed_paste);
2523 assert!(!caps.mouse_sgr);
2524 assert!(!caps.use_sync_output());
2525 assert!(!caps.use_scroll_region());
2526 }
2527
2528 #[test]
2529 fn profile_tmux_mux_flags() {
2530 let caps = TerminalCapabilities::tmux();
2531 assert_eq!(caps.profile(), TerminalProfile::Tmux);
2532 assert!(caps.in_tmux);
2533 assert!(!caps.in_screen);
2534 assert!(!caps.in_zellij);
2535 assert!(caps.in_any_mux());
2536 assert!(!caps.use_sync_output());
2538 assert!(!caps.use_scroll_region());
2539 assert!(!caps.use_hyperlinks());
2540 }
2541
2542 #[test]
2543 fn profile_screen_mux_flags() {
2544 let caps = TerminalCapabilities::screen();
2545 assert_eq!(caps.profile(), TerminalProfile::Screen);
2546 assert!(!caps.in_tmux);
2547 assert!(caps.in_screen);
2548 assert!(caps.in_any_mux());
2549 assert!(caps.needs_passthrough_wrap());
2550 }
2551
2552 #[test]
2553 fn profile_zellij_mux_flags() {
2554 let caps = TerminalCapabilities::zellij();
2555 assert_eq!(caps.profile(), TerminalProfile::Zellij);
2556 assert!(caps.in_zellij);
2557 assert!(caps.in_any_mux());
2558 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2560 assert!(caps.focus_events);
2561 assert!(!caps.needs_passthrough_wrap());
2563 }
2564
2565 #[test]
2566 fn profile_kitty_full_features() {
2567 let caps = TerminalCapabilities::kitty();
2568 assert_eq!(caps.profile(), TerminalProfile::Kitty);
2569 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2570 assert!(caps.sync_output);
2571 assert!(caps.kitty_keyboard);
2572 assert!(caps.osc8_hyperlinks);
2573 }
2574
2575 #[test]
2576 fn profile_windows_console() {
2577 let caps = TerminalCapabilities::windows_console();
2578 assert_eq!(caps.profile(), TerminalProfile::WindowsConsole);
2579 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2580 assert!(caps.osc8_hyperlinks);
2581 assert!(caps.focus_events);
2582 }
2583
2584 #[test]
2585 fn profile_linux_console() {
2586 let caps = TerminalCapabilities::linux_console();
2587 assert_eq!(caps.profile(), TerminalProfile::LinuxConsole);
2588 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2589 assert!(!caps.double_width);
2590 assert!(caps.scroll_region);
2591 }
2592
2593 #[test]
2594 fn from_profile_roundtrip() {
2595 for profile in TerminalProfile::all_predefined() {
2596 let caps = TerminalCapabilities::from_profile(*profile);
2597 assert_eq!(caps.profile(), *profile);
2598 }
2599 }
2600
2601 #[test]
2602 fn detected_profile_has_none_name() {
2603 let caps = detect_with_override(None);
2604 assert_eq!(caps.profile(), TerminalProfile::Detected);
2605 assert_eq!(caps.profile_name(), None);
2606 }
2607
2608 #[test]
2609 fn detect_respects_test_profile_env() {
2610 let caps = detect_with_override(Some("dumb"));
2611 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2612 }
2613
2614 #[test]
2615 fn detect_ignores_invalid_test_profile() {
2616 let caps = detect_with_override(Some("not-a-real-profile"));
2617 assert_eq!(caps.profile(), TerminalProfile::Detected);
2618 }
2619
2620 #[test]
2621 fn basic_has_dumb_profile() {
2622 let caps = TerminalCapabilities::basic();
2623 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2624 }
2625
2626 #[test]
2629 fn builder_starts_empty() {
2630 let caps = CapabilityProfileBuilder::new().build();
2631 assert_eq!(caps.profile(), TerminalProfile::Custom);
2632 assert_eq!(caps.color_depth, ColorDepth::Mono);
2633 assert!(!caps.sync_output);
2634 assert!(!caps.scroll_region);
2635 assert!(!caps.mouse_sgr);
2636 }
2637
2638 #[test]
2639 fn builder_sets_one_canonical_color_depth() {
2640 let caps = CapabilityProfileBuilder::new()
2641 .color_depth(ColorDepth::TrueColor)
2642 .build();
2643 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2644 }
2645
2646 #[test]
2647 fn builder_set_advanced() {
2648 let caps = CapabilityProfileBuilder::new()
2649 .sync_output(true)
2650 .osc8_hyperlinks(true)
2651 .scroll_region(true)
2652 .build();
2653 assert!(caps.sync_output);
2654 assert!(caps.osc8_hyperlinks);
2655 assert!(caps.scroll_region);
2656 }
2657
2658 #[test]
2659 fn builder_set_mux() {
2660 let caps = CapabilityProfileBuilder::new()
2661 .in_tmux(true)
2662 .in_screen(false)
2663 .in_zellij(false)
2664 .build();
2665 assert!(caps.in_tmux);
2666 assert!(!caps.in_screen);
2667 assert!(caps.in_any_mux());
2668 }
2669
2670 #[test]
2671 fn builder_set_input() {
2672 let caps = CapabilityProfileBuilder::new()
2673 .kitty_keyboard(true)
2674 .focus_events(true)
2675 .bracketed_paste(true)
2676 .mouse_sgr(true)
2677 .build();
2678 assert!(caps.kitty_keyboard);
2679 assert!(caps.focus_events);
2680 assert!(caps.bracketed_paste);
2681 assert!(caps.mouse_sgr);
2682 }
2683
2684 #[test]
2685 fn builder_set_clipboard() {
2686 let caps = CapabilityProfileBuilder::new()
2687 .osc52_clipboard(true)
2688 .build();
2689 assert!(caps.osc52_clipboard);
2690 }
2691
2692 #[test]
2693 fn builder_from_profile() {
2694 let caps = CapabilityProfileBuilder::from_profile(TerminalProfile::Modern)
2695 .sync_output(false) .build();
2697 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2699 assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
2701 assert_eq!(caps.profile(), TerminalProfile::Custom);
2703 }
2704
2705 #[test]
2706 fn builder_chain_multiple() {
2707 let caps = TerminalCapabilities::builder()
2708 .color_depth(ColorDepth::Ansi256)
2709 .bracketed_paste(true)
2710 .mouse_sgr(true)
2711 .scroll_region(true)
2712 .build();
2713 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2714 assert!(caps.bracketed_paste);
2715 assert!(caps.mouse_sgr);
2716 assert!(caps.scroll_region);
2717 assert!(!caps.supports_true_color());
2718 assert!(!caps.sync_output);
2719 }
2720
2721 #[test]
2722 fn builder_default() {
2723 let builder = CapabilityProfileBuilder::default();
2724 let caps = builder.build();
2725 assert_eq!(caps.profile(), TerminalProfile::Custom);
2726 }
2727
2728 #[test]
2737 fn mux_compatibility_matrix() {
2738 {
2743 let caps = TerminalCapabilities::modern();
2744 assert!(
2745 caps.use_sync_output(),
2746 "baseline: sync_output should be enabled"
2747 );
2748 assert!(
2749 caps.use_scroll_region(),
2750 "baseline: scroll_region should be enabled"
2751 );
2752 assert!(
2753 caps.use_hyperlinks(),
2754 "baseline: hyperlinks should be enabled"
2755 );
2756 assert!(!caps.needs_passthrough_wrap(), "baseline: no wrap needed");
2757 }
2758
2759 {
2761 let caps = TerminalCapabilities::tmux();
2762 assert!(!caps.use_sync_output(), "tmux: sync_output disabled");
2763 assert!(!caps.use_scroll_region(), "tmux: scroll_region disabled");
2764 assert!(!caps.use_hyperlinks(), "tmux: hyperlinks disabled");
2765 assert!(caps.needs_passthrough_wrap(), "tmux: needs wrap");
2766 }
2767
2768 {
2770 let caps = TerminalCapabilities::screen();
2771 assert!(!caps.use_sync_output(), "screen: sync_output disabled");
2772 assert!(!caps.use_scroll_region(), "screen: scroll_region disabled");
2773 assert!(!caps.use_hyperlinks(), "screen: hyperlinks disabled");
2774 assert!(caps.needs_passthrough_wrap(), "screen: needs wrap");
2775 }
2776
2777 {
2779 let caps = TerminalCapabilities::zellij();
2780 assert!(!caps.use_sync_output(), "zellij: sync_output disabled");
2781 assert!(!caps.use_scroll_region(), "zellij: scroll_region disabled");
2782 assert!(!caps.use_hyperlinks(), "zellij: hyperlinks disabled");
2783 assert!(
2784 !caps.needs_passthrough_wrap(),
2785 "zellij: no wrap needed (native passthrough)"
2786 );
2787 }
2788
2789 {
2791 let caps = TerminalCapabilities::builder()
2792 .in_wezterm_mux(true)
2793 .sync_output(true)
2794 .scroll_region(true)
2795 .osc8_hyperlinks(true)
2796 .build();
2797 assert!(!caps.use_sync_output(), "wezterm mux: sync_output disabled");
2798 assert!(
2799 !caps.use_scroll_region(),
2800 "wezterm mux: scroll_region disabled"
2801 );
2802 assert!(!caps.use_hyperlinks(), "wezterm mux: hyperlinks disabled");
2803 assert!(
2804 !caps.needs_passthrough_wrap(),
2805 "wezterm mux: no wrap needed"
2806 );
2807 }
2808 }
2809
2810 #[test]
2812 fn modern_terminal_in_mux_matrix() {
2813 for (mux_name, in_tmux, in_screen, in_zellij, in_wezterm_mux) in [
2817 ("tmux", true, false, false, false),
2818 ("screen", false, true, false, false),
2819 ("zellij", false, false, true, false),
2820 ("wezterm-mux", false, false, false, true),
2821 ] {
2822 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2823 env.in_tmux = in_tmux;
2824 env.in_screen = in_screen;
2825 env.in_zellij = in_zellij;
2826 env.wezterm_unix_socket = in_wezterm_mux;
2827 let caps = TerminalCapabilities::detect_from_inputs(&env);
2828
2829 assert!(
2831 caps.supports_true_color(),
2832 "{mux_name}: true_color detection should work"
2833 );
2834 assert!(
2835 !caps.sync_output,
2836 "{mux_name}: sync_output hard-disabled for WezTerm safety"
2837 );
2838
2839 assert!(
2841 !caps.use_sync_output(),
2842 "{mux_name}: use_sync_output() should be false"
2843 );
2844 assert!(
2845 !caps.use_scroll_region(),
2846 "{mux_name}: use_scroll_region() should be false"
2847 );
2848 assert!(
2849 !caps.use_hyperlinks(),
2850 "{mux_name}: use_hyperlinks() should be false"
2851 );
2852 }
2853 }
2854
2855 #[test]
2857 fn profile_mux_invariant_matrix() {
2858 for profile in TerminalProfile::all_predefined() {
2860 let caps = TerminalCapabilities::from_profile(*profile);
2861 let name = profile.as_str();
2862
2863 let expected_mux =
2865 caps.in_tmux || caps.in_screen || caps.in_zellij || caps.in_wezterm_mux;
2866 assert_eq!(
2867 caps.in_any_mux(),
2868 expected_mux,
2869 "{name}: in_any_mux() should match individual flags"
2870 );
2871
2872 if caps.in_any_mux() {
2874 assert!(
2875 !caps.use_sync_output(),
2876 "{name}: mux should disable use_sync_output()"
2877 );
2878 assert!(
2879 !caps.use_scroll_region(),
2880 "{name}: mux should disable use_scroll_region()"
2881 );
2882 assert!(
2883 !caps.use_hyperlinks(),
2884 "{name}: mux should disable use_hyperlinks()"
2885 );
2886 }
2887
2888 if caps.in_tmux || caps.in_screen {
2890 assert!(
2891 caps.needs_passthrough_wrap(),
2892 "{name}: tmux/screen should need passthrough wrap"
2893 );
2894 } else if caps.in_zellij {
2895 assert!(
2896 !caps.needs_passthrough_wrap(),
2897 "{name}: zellij should NOT need passthrough wrap"
2898 );
2899 }
2900 }
2901 }
2902
2903 #[test]
2905 fn fallback_ordering_matrix() {
2906 use crate::inline_mode::InlineStrategy;
2907
2908 let caps_full = TerminalCapabilities::builder()
2910 .sync_output(true)
2911 .scroll_region(true)
2912 .build();
2913 assert_eq!(
2914 InlineStrategy::select(&caps_full),
2915 InlineStrategy::ScrollRegion,
2916 "full capabilities should use ScrollRegion"
2917 );
2918
2919 let caps_hybrid = TerminalCapabilities::builder()
2921 .sync_output(false)
2922 .scroll_region(true)
2923 .build();
2924 assert_eq!(
2925 InlineStrategy::select(&caps_hybrid),
2926 InlineStrategy::Hybrid,
2927 "scroll without sync should use Hybrid"
2928 );
2929
2930 let caps_none = TerminalCapabilities::builder()
2932 .sync_output(false)
2933 .scroll_region(false)
2934 .build();
2935 assert_eq!(
2936 InlineStrategy::select(&caps_none),
2937 InlineStrategy::OverlayRedraw,
2938 "no capabilities should use OverlayRedraw"
2939 );
2940
2941 let caps_tmux = TerminalCapabilities::tmux();
2943 assert_eq!(
2944 InlineStrategy::select(&caps_tmux),
2945 InlineStrategy::OverlayRedraw,
2946 "tmux should force OverlayRedraw"
2947 );
2948 }
2949
2950 #[test]
2952 fn terminal_mux_strategy_matrix() {
2953 use crate::inline_mode::InlineStrategy;
2954
2955 struct TestCase {
2956 name: &'static str,
2957 profile: TerminalProfile,
2958 expected: InlineStrategy,
2959 }
2960
2961 let cases = [
2962 TestCase {
2963 name: "modern (no mux)",
2964 profile: TerminalProfile::Modern,
2965 expected: InlineStrategy::ScrollRegion,
2966 },
2967 TestCase {
2968 name: "kitty (no mux)",
2969 profile: TerminalProfile::Kitty,
2970 expected: InlineStrategy::ScrollRegion,
2971 },
2972 TestCase {
2973 name: "xterm-256color (no mux)",
2974 profile: TerminalProfile::Xterm256Color,
2975 expected: InlineStrategy::Hybrid, },
2977 TestCase {
2978 name: "xterm (no mux)",
2979 profile: TerminalProfile::Xterm,
2980 expected: InlineStrategy::Hybrid,
2981 },
2982 TestCase {
2983 name: "vt100 (no mux)",
2984 profile: TerminalProfile::Vt100,
2985 expected: InlineStrategy::Hybrid,
2986 },
2987 TestCase {
2988 name: "dumb",
2989 profile: TerminalProfile::Dumb,
2990 expected: InlineStrategy::OverlayRedraw, },
2992 TestCase {
2993 name: "tmux",
2994 profile: TerminalProfile::Tmux,
2995 expected: InlineStrategy::OverlayRedraw,
2996 },
2997 TestCase {
2998 name: "screen",
2999 profile: TerminalProfile::Screen,
3000 expected: InlineStrategy::OverlayRedraw,
3001 },
3002 TestCase {
3003 name: "zellij",
3004 profile: TerminalProfile::Zellij,
3005 expected: InlineStrategy::OverlayRedraw,
3006 },
3007 ];
3008
3009 for case in cases {
3010 let caps = TerminalCapabilities::from_profile(case.profile);
3011 let actual = InlineStrategy::select(&caps);
3012 assert_eq!(
3013 actual, case.expected,
3014 "{}: expected {:?}, got {:?}",
3015 case.name, case.expected, actual
3016 );
3017 }
3018 }
3019
3020 #[test]
3023 fn shared_caps_load_returns_initial() {
3024 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
3025 assert_eq!(shared.load().color_depth, ColorDepth::TrueColor);
3026 assert!(shared.load().sync_output);
3027 }
3028
3029 #[test]
3030 fn shared_caps_store_replaces_value() {
3031 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
3032 shared.store(TerminalCapabilities::dumb());
3033 let loaded = shared.load();
3034 assert_eq!(loaded.color_depth, ColorDepth::Mono);
3035 assert!(!loaded.sync_output);
3036 }
3037
3038 #[test]
3039 fn shared_caps_concurrent_read_write() {
3040 use std::sync::{Arc, Barrier};
3041 use std::thread;
3042
3043 let shared = Arc::new(SharedCapabilities::new(TerminalCapabilities::basic()));
3044 let barrier = Arc::new(Barrier::new(5)); let readers: Vec<_> = (0..4)
3047 .map(|_| {
3048 let s = Arc::clone(&shared);
3049 let b = Arc::clone(&barrier);
3050 thread::spawn(move || {
3051 b.wait();
3052 for _ in 0..10_000 {
3053 let caps = s.load();
3054 let _ = caps.use_sync_output();
3056 let _ = caps.color_depth;
3057 }
3058 })
3059 })
3060 .collect();
3061
3062 let writer = {
3063 let s = Arc::clone(&shared);
3064 let b = Arc::clone(&barrier);
3065 thread::spawn(move || {
3066 b.wait();
3067 for i in 0..1_000 {
3068 if i % 2 == 0 {
3069 s.store(TerminalCapabilities::modern());
3070 } else {
3071 s.store(TerminalCapabilities::dumb());
3072 }
3073 }
3074 })
3075 };
3076
3077 writer.join().unwrap();
3078 for h in readers {
3079 h.join().unwrap();
3080 }
3081 }
3082}
3083
3084#[cfg(test)]
3089mod proptests {
3090 use super::*;
3091 use proptest::prelude::*;
3092
3093 proptest! {
3094 #[test]
3096 fn prop_in_any_mux_consistent(
3097 in_tmux in any::<bool>(),
3098 in_screen in any::<bool>(),
3099 in_zellij in any::<bool>(),
3100 in_wezterm_mux in any::<bool>(),
3101 ) {
3102 let caps = TerminalCapabilities::builder()
3103 .in_tmux(in_tmux)
3104 .in_screen(in_screen)
3105 .in_zellij(in_zellij)
3106 .in_wezterm_mux(in_wezterm_mux)
3107 .build();
3108
3109 let expected = in_tmux || in_screen || in_zellij || in_wezterm_mux;
3110 prop_assert_eq!(caps.in_any_mux(), expected);
3111 }
3112
3113 #[test]
3115 fn prop_mux_disables_sync_output(
3116 in_tmux in any::<bool>(),
3117 in_screen in any::<bool>(),
3118 in_zellij in any::<bool>(),
3119 in_wezterm_mux in any::<bool>(),
3120 sync_output in any::<bool>(),
3121 ) {
3122 let caps = TerminalCapabilities::builder()
3123 .in_tmux(in_tmux)
3124 .in_screen(in_screen)
3125 .in_zellij(in_zellij)
3126 .in_wezterm_mux(in_wezterm_mux)
3127 .sync_output(sync_output)
3128 .build();
3129
3130 if caps.in_any_mux() {
3131 prop_assert!(!caps.use_sync_output(), "mux should disable sync_output policy");
3132 }
3133 }
3134
3135 #[test]
3137 fn prop_mux_disables_scroll_region(
3138 in_tmux in any::<bool>(),
3139 in_screen in any::<bool>(),
3140 in_zellij in any::<bool>(),
3141 in_wezterm_mux in any::<bool>(),
3142 scroll_region in any::<bool>(),
3143 ) {
3144 let caps = TerminalCapabilities::builder()
3145 .in_tmux(in_tmux)
3146 .in_screen(in_screen)
3147 .in_zellij(in_zellij)
3148 .in_wezterm_mux(in_wezterm_mux)
3149 .scroll_region(scroll_region)
3150 .build();
3151
3152 if caps.in_any_mux() {
3153 prop_assert!(!caps.use_scroll_region(), "mux should disable scroll_region policy");
3154 }
3155 }
3156
3157 #[test]
3159 fn prop_mux_disables_hyperlinks(
3160 in_tmux in any::<bool>(),
3161 in_screen in any::<bool>(),
3162 in_zellij in any::<bool>(),
3163 in_wezterm_mux in any::<bool>(),
3164 osc8_hyperlinks in any::<bool>(),
3165 ) {
3166 let caps = TerminalCapabilities::builder()
3167 .in_tmux(in_tmux)
3168 .in_screen(in_screen)
3169 .in_zellij(in_zellij)
3170 .in_wezterm_mux(in_wezterm_mux)
3171 .osc8_hyperlinks(osc8_hyperlinks)
3172 .build();
3173
3174 if caps.in_any_mux() {
3175 prop_assert!(!caps.use_hyperlinks(), "mux should disable hyperlinks policy");
3176 }
3177 }
3178
3179 #[test]
3181 fn prop_passthrough_wrap_logic(
3182 in_tmux in any::<bool>(),
3183 in_screen in any::<bool>(),
3184 in_zellij in any::<bool>(),
3185 in_wezterm_mux in any::<bool>(),
3186 ) {
3187 let caps = TerminalCapabilities::builder()
3188 .in_tmux(in_tmux)
3189 .in_screen(in_screen)
3190 .in_zellij(in_zellij)
3191 .in_wezterm_mux(in_wezterm_mux)
3192 .build();
3193
3194 let expected = in_tmux || in_screen; prop_assert_eq!(caps.needs_passthrough_wrap(), expected);
3196 }
3197
3198 #[test]
3200 fn prop_policy_false_when_capability_off(
3201 in_tmux in any::<bool>(),
3202 in_screen in any::<bool>(),
3203 in_zellij in any::<bool>(),
3204 in_wezterm_mux in any::<bool>(),
3205 ) {
3206 let caps = TerminalCapabilities::builder()
3207 .in_tmux(in_tmux)
3208 .in_screen(in_screen)
3209 .in_zellij(in_zellij)
3210 .in_wezterm_mux(in_wezterm_mux)
3211 .sync_output(false)
3212 .scroll_region(false)
3213 .osc8_hyperlinks(false)
3214 .osc52_clipboard(false)
3215 .build();
3216
3217 prop_assert!(!caps.use_sync_output(), "sync_output=false implies use_sync_output()=false");
3218 prop_assert!(!caps.use_scroll_region(), "scroll_region=false implies use_scroll_region()=false");
3219 prop_assert!(!caps.use_hyperlinks(), "osc8_hyperlinks=false implies use_hyperlinks()=false");
3220 prop_assert!(!caps.use_clipboard(), "osc52_clipboard=false implies use_clipboard()=false");
3221 }
3222
3223 #[test]
3225 fn prop_no_color_preserves_non_visual(no_color in any::<bool>()) {
3226 let env = DetectInputs {
3227 no_color,
3228 term: "xterm-256color".to_string(),
3229 term_program: "WezTerm".to_string(),
3230 colorterm: "truecolor".to_string(),
3231 in_tmux: false,
3232 in_screen: false,
3233 in_zellij: false,
3234 wezterm_unix_socket: false,
3235 wezterm_pane: false,
3236 wezterm_executable: false,
3237 kitty_window_id: false,
3238 wt_session: false,
3239 };
3240 let caps = TerminalCapabilities::detect_from_inputs(&env);
3241
3242 if no_color {
3243 prop_assert_eq!(caps.color_depth, ColorDepth::Mono);
3244 prop_assert!(!caps.osc8_hyperlinks, "NO_COLOR disables hyperlinks");
3245 }
3246
3247 prop_assert!(
3249 !caps.sync_output,
3250 "WezTerm sync_output stays disabled despite NO_COLOR"
3251 );
3252 prop_assert!(caps.bracketed_paste, "bracketed_paste preserved despite NO_COLOR");
3253 }
3254 }
3255}