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 lc_terminal: String,
256 colorterm: String,
257 in_tmux: bool,
258 in_screen: bool,
259 in_zellij: bool,
260 wezterm_unix_socket: bool,
261 wezterm_pane: bool,
262 wezterm_executable: bool,
263 kitty_window_id: bool,
264 wt_session: bool,
265}
266
267impl DetectInputs {
268 fn from_env() -> Self {
269 Self::from_env_with(|key| env::var_os(key))
270 }
271
272 fn from_env_with<F>(get_env: F) -> Self
273 where
274 F: Fn(&str) -> Option<OsString>,
275 {
276 let text = |key| {
277 get_env(key)
278 .and_then(|value| value.into_string().ok())
279 .unwrap_or_default()
280 };
281 let utf8_present = |key| get_env(key).is_some_and(|value| value.into_string().is_ok());
282
283 Self {
284 no_color: get_env("NO_COLOR").is_some(),
285 term: text("TERM"),
286 term_program: text("TERM_PROGRAM"),
287 lc_terminal: text("LC_TERMINAL"),
288 colorterm: text("COLORTERM"),
289 in_tmux: utf8_present("TMUX"),
290 in_screen: utf8_present("STY"),
291 in_zellij: utf8_present("ZELLIJ"),
292 wezterm_unix_socket: utf8_present("WEZTERM_UNIX_SOCKET"),
293 wezterm_pane: utf8_present("WEZTERM_PANE"),
294 wezterm_executable: utf8_present("WEZTERM_EXECUTABLE"),
295 kitty_window_id: utf8_present("KITTY_WINDOW_ID"),
296 wt_session: utf8_present("WT_SESSION"),
297 }
298 }
299}
300
301const MODERN_TERMINALS: &[&str] = &[
303 "iTerm.app",
304 "WezTerm",
305 "Alacritty",
306 "Ghostty",
307 "kitty",
308 "Rio",
309 "Hyper",
310 "Contour",
311 "vscode",
312 "Black Box",
313];
314
315const KITTY_KEYBOARD_TERMINALS: &[&str] = &[
317 "iTerm.app",
318 "WezTerm",
319 "Alacritty",
320 "Ghostty",
321 "Rio",
322 "kitty",
323 "foot",
324 "Black Box",
325];
326
327const SYNC_OUTPUT_TERMINALS: &[&str] = &["Alacritty", "Ghostty", "kitty", "Contour"];
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
337pub enum TerminalProfile {
338 Modern,
340 Xterm256Color,
342 Xterm,
344 Vt100,
346 Dumb,
348 Screen,
350 Tmux,
352 Zellij,
354 WindowsConsole,
356 Kitty,
358 LinuxConsole,
360 Custom,
362 Detected,
364}
365
366impl TerminalProfile {
367 #[must_use]
369 pub const fn as_str(&self) -> &'static str {
370 match self {
371 Self::Modern => "modern",
372 Self::Xterm256Color => "xterm-256color",
373 Self::Xterm => "xterm",
374 Self::Vt100 => "vt100",
375 Self::Dumb => "dumb",
376 Self::Screen => "screen",
377 Self::Tmux => "tmux",
378 Self::Zellij => "zellij",
379 Self::WindowsConsole => "windows-console",
380 Self::Kitty => "kitty",
381 Self::LinuxConsole => "linux",
382 Self::Custom => "custom",
383 Self::Detected => "detected",
384 }
385 }
386
387 #[must_use]
389 pub const fn all_predefined() -> &'static [Self] {
390 &[
391 Self::Modern,
392 Self::Xterm256Color,
393 Self::Xterm,
394 Self::Vt100,
395 Self::Dumb,
396 Self::Screen,
397 Self::Tmux,
398 Self::Zellij,
399 Self::WindowsConsole,
400 Self::Kitty,
401 Self::LinuxConsole,
402 ]
403 }
404}
405
406impl std::str::FromStr for TerminalProfile {
407 type Err = ();
408
409 fn from_str(s: &str) -> Result<Self, Self::Err> {
410 match s.to_lowercase().as_str() {
411 "modern" => Ok(Self::Modern),
412 "xterm-256color" | "xterm256color" | "xterm-256" => Ok(Self::Xterm256Color),
413 "xterm" => Ok(Self::Xterm),
414 "vt100" => Ok(Self::Vt100),
415 "dumb" => Ok(Self::Dumb),
416 "screen" | "screen-256color" => Ok(Self::Screen),
417 "tmux" | "tmux-256color" => Ok(Self::Tmux),
418 "zellij" => Ok(Self::Zellij),
419 "windows-console" | "windows" | "conhost" => Ok(Self::WindowsConsole),
420 "kitty" | "xterm-kitty" => Ok(Self::Kitty),
421 "linux" | "linux-console" => Ok(Self::LinuxConsole),
422 "custom" => Ok(Self::Custom),
423 "detected" | "auto" => Ok(Self::Detected),
424 _ => Err(()),
425 }
426 }
427}
428
429impl std::fmt::Display for TerminalProfile {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 write!(f, "{}", self.as_str())
432 }
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452pub struct TerminalCapabilities {
453 profile: TerminalProfile,
455
456 pub color_depth: ColorDepth,
459
460 pub unicode_box_drawing: bool,
463 pub unicode_emoji: bool,
465 pub double_width: bool,
467
468 pub sync_output: bool,
471 pub osc8_hyperlinks: bool,
473 pub scroll_region: bool,
475
476 pub in_tmux: bool,
479 pub in_screen: bool,
481 pub in_zellij: bool,
483 pub in_wezterm_mux: bool,
491
492 pub kitty_keyboard: bool,
495 pub focus_events: bool,
497 pub bracketed_paste: bool,
499 pub mouse_sgr: bool,
501
502 pub osc52_clipboard: bool,
505}
506
507impl Default for TerminalCapabilities {
508 fn default() -> Self {
509 Self::basic()
510 }
511}
512
513impl TerminalCapabilities {
518 #[must_use]
522 pub const fn profile(&self) -> TerminalProfile {
523 self.profile
524 }
525
526 #[must_use]
531 pub fn profile_name(&self) -> Option<&'static str> {
532 match self.profile {
533 TerminalProfile::Detected => None,
534 p => Some(p.as_str()),
535 }
536 }
537
538 #[must_use]
540 pub fn from_profile(profile: TerminalProfile) -> Self {
541 match profile {
542 TerminalProfile::Modern => Self::modern(),
543 TerminalProfile::Xterm256Color => Self::xterm_256color(),
544 TerminalProfile::Xterm => Self::xterm(),
545 TerminalProfile::Vt100 => Self::vt100(),
546 TerminalProfile::Dumb => Self::dumb(),
547 TerminalProfile::Screen => Self::screen(),
548 TerminalProfile::Tmux => Self::tmux(),
549 TerminalProfile::Zellij => Self::zellij(),
550 TerminalProfile::WindowsConsole => Self::windows_console(),
551 TerminalProfile::Kitty => Self::kitty(),
552 TerminalProfile::LinuxConsole => Self::linux_console(),
553 TerminalProfile::Custom => {
554 let mut caps = Self::basic();
557 caps.profile = TerminalProfile::Custom;
558 caps
559 }
560 TerminalProfile::Detected => Self::detect(),
561 }
562 }
563
564 #[must_use]
571 pub const fn modern() -> Self {
572 Self {
573 profile: TerminalProfile::Modern,
574 color_depth: ColorDepth::TrueColor,
575 unicode_box_drawing: true,
576 unicode_emoji: true,
577 double_width: true,
578 sync_output: true,
579 osc8_hyperlinks: true,
580 scroll_region: true,
581 in_tmux: false,
582 in_screen: false,
583 in_zellij: false,
584 in_wezterm_mux: false,
585 kitty_keyboard: true,
586 focus_events: true,
587 bracketed_paste: true,
588 mouse_sgr: true,
589 osc52_clipboard: true,
590 }
591 }
592
593 #[must_use]
598 pub const fn xterm_256color() -> Self {
599 Self {
600 profile: TerminalProfile::Xterm256Color,
601 color_depth: ColorDepth::Ansi256,
602 unicode_box_drawing: true,
603 unicode_emoji: true,
604 double_width: true,
605 sync_output: false,
606 osc8_hyperlinks: false,
607 scroll_region: true,
608 in_tmux: false,
609 in_screen: false,
610 in_zellij: false,
611 in_wezterm_mux: false,
612 kitty_keyboard: false,
613 focus_events: false,
614 bracketed_paste: true,
615 mouse_sgr: true,
616 osc52_clipboard: false,
617 }
618 }
619
620 #[must_use]
624 pub const fn xterm() -> Self {
625 Self {
626 profile: TerminalProfile::Xterm,
627 color_depth: ColorDepth::Ansi16,
628 unicode_box_drawing: true,
629 unicode_emoji: false,
630 double_width: true,
631 sync_output: false,
632 osc8_hyperlinks: false,
633 scroll_region: true,
634 in_tmux: false,
635 in_screen: false,
636 in_zellij: false,
637 in_wezterm_mux: false,
638 kitty_keyboard: false,
639 focus_events: false,
640 bracketed_paste: true,
641 mouse_sgr: true,
642 osc52_clipboard: false,
643 }
644 }
645
646 #[must_use]
650 pub const fn vt100() -> Self {
651 Self {
652 profile: TerminalProfile::Vt100,
653 color_depth: ColorDepth::Mono,
654 unicode_box_drawing: false,
655 unicode_emoji: false,
656 double_width: false,
657 sync_output: false,
658 osc8_hyperlinks: false,
659 scroll_region: true,
660 in_tmux: false,
661 in_screen: false,
662 in_zellij: false,
663 in_wezterm_mux: false,
664 kitty_keyboard: false,
665 focus_events: false,
666 bracketed_paste: false,
667 mouse_sgr: false,
668 osc52_clipboard: false,
669 }
670 }
671
672 #[must_use]
676 pub const fn dumb() -> Self {
677 Self {
678 profile: TerminalProfile::Dumb,
679 color_depth: ColorDepth::Mono,
680 unicode_box_drawing: false,
681 unicode_emoji: false,
682 double_width: false,
683 sync_output: false,
684 osc8_hyperlinks: false,
685 scroll_region: false,
686 in_tmux: false,
687 in_screen: false,
688 in_zellij: false,
689 in_wezterm_mux: false,
690 kitty_keyboard: false,
691 focus_events: false,
692 bracketed_paste: false,
693 mouse_sgr: false,
694 osc52_clipboard: false,
695 }
696 }
697
698 #[must_use]
703 pub const fn screen() -> Self {
704 Self {
705 profile: TerminalProfile::Screen,
706 color_depth: ColorDepth::Ansi256,
707 unicode_box_drawing: true,
708 unicode_emoji: true,
709 double_width: true,
710 sync_output: false,
711 osc8_hyperlinks: false,
712 scroll_region: true,
713 in_tmux: false,
714 in_screen: true,
715 in_zellij: false,
716 in_wezterm_mux: false,
717 kitty_keyboard: false,
718 focus_events: false,
719 bracketed_paste: true,
720 mouse_sgr: true,
721 osc52_clipboard: false,
722 }
723 }
724
725 #[must_use]
730 pub const fn tmux() -> Self {
731 Self {
732 profile: TerminalProfile::Tmux,
733 color_depth: ColorDepth::Ansi256,
734 unicode_box_drawing: true,
735 unicode_emoji: true,
736 double_width: true,
737 sync_output: false,
738 osc8_hyperlinks: false,
739 scroll_region: true,
740 in_tmux: true,
741 in_screen: false,
742 in_zellij: false,
743 in_wezterm_mux: false,
744 kitty_keyboard: false,
745 focus_events: false,
746 bracketed_paste: true,
747 mouse_sgr: true,
748 osc52_clipboard: false,
749 }
750 }
751
752 #[must_use]
756 pub const fn zellij() -> Self {
757 Self {
758 profile: TerminalProfile::Zellij,
759 color_depth: ColorDepth::TrueColor,
760 unicode_box_drawing: true,
761 unicode_emoji: true,
762 double_width: true,
763 sync_output: false,
764 osc8_hyperlinks: false,
765 scroll_region: true,
766 in_tmux: false,
767 in_screen: false,
768 in_zellij: true,
769 in_wezterm_mux: false,
770 kitty_keyboard: false,
771 focus_events: true,
772 bracketed_paste: true,
773 mouse_sgr: true,
774 osc52_clipboard: false,
775 }
776 }
777
778 #[must_use]
782 pub const fn windows_console() -> Self {
783 Self {
784 profile: TerminalProfile::WindowsConsole,
785 color_depth: ColorDepth::TrueColor,
786 unicode_box_drawing: true,
787 unicode_emoji: true,
788 double_width: true,
789 sync_output: false,
790 osc8_hyperlinks: true,
791 scroll_region: true,
792 in_tmux: false,
793 in_screen: false,
794 in_zellij: false,
795 in_wezterm_mux: false,
796 kitty_keyboard: false,
797 focus_events: true,
798 bracketed_paste: true,
799 mouse_sgr: true,
800 osc52_clipboard: true,
801 }
802 }
803
804 #[must_use]
808 pub const fn kitty() -> Self {
809 Self {
810 profile: TerminalProfile::Kitty,
811 color_depth: ColorDepth::TrueColor,
812 unicode_box_drawing: true,
813 unicode_emoji: true,
814 double_width: true,
815 sync_output: true,
816 osc8_hyperlinks: true,
817 scroll_region: true,
818 in_tmux: false,
819 in_screen: false,
820 in_zellij: false,
821 in_wezterm_mux: false,
822 kitty_keyboard: true,
823 focus_events: true,
824 bracketed_paste: true,
825 mouse_sgr: true,
826 osc52_clipboard: true,
827 }
828 }
829
830 #[must_use]
834 pub const fn linux_console() -> Self {
835 Self {
836 profile: TerminalProfile::LinuxConsole,
837 color_depth: ColorDepth::Ansi16,
838 unicode_box_drawing: true,
839 unicode_emoji: false,
840 double_width: false,
841 sync_output: false,
842 osc8_hyperlinks: false,
843 scroll_region: true,
844 in_tmux: false,
845 in_screen: false,
846 in_zellij: false,
847 in_wezterm_mux: false,
848 kitty_keyboard: false,
849 focus_events: false,
850 bracketed_paste: true,
851 mouse_sgr: true,
852 osc52_clipboard: false,
853 }
854 }
855
856 pub fn builder() -> CapabilityProfileBuilder {
860 CapabilityProfileBuilder::new()
861 }
862}
863
864#[derive(Debug, Clone)]
887#[must_use]
888pub struct CapabilityProfileBuilder {
889 caps: TerminalCapabilities,
890}
891
892impl Default for CapabilityProfileBuilder {
893 fn default() -> Self {
894 Self::new()
895 }
896}
897
898impl CapabilityProfileBuilder {
899 pub fn new() -> Self {
901 Self {
902 caps: TerminalCapabilities {
903 profile: TerminalProfile::Custom,
904 color_depth: ColorDepth::Mono,
905 unicode_box_drawing: false,
906 unicode_emoji: false,
907 double_width: false,
908 sync_output: false,
909 osc8_hyperlinks: false,
910 scroll_region: false,
911 in_tmux: false,
912 in_screen: false,
913 in_zellij: false,
914 in_wezterm_mux: false,
915 kitty_keyboard: false,
916 focus_events: false,
917 bracketed_paste: false,
918 mouse_sgr: false,
919 osc52_clipboard: false,
920 },
921 }
922 }
923
924 pub fn from_profile(profile: TerminalProfile) -> Self {
926 let mut caps = TerminalCapabilities::from_profile(profile);
927 caps.profile = TerminalProfile::Custom;
928 Self { caps }
929 }
930
931 #[must_use]
933 pub fn build(self) -> TerminalCapabilities {
934 self.caps
935 }
936
937 pub const fn color_depth(mut self, depth: ColorDepth) -> Self {
941 self.caps.color_depth = depth;
942 self
943 }
944
945 pub const fn sync_output(mut self, enabled: bool) -> Self {
949 self.caps.sync_output = enabled;
950 self
951 }
952
953 pub const fn osc8_hyperlinks(mut self, enabled: bool) -> Self {
955 self.caps.osc8_hyperlinks = enabled;
956 self
957 }
958
959 pub const fn scroll_region(mut self, enabled: bool) -> Self {
961 self.caps.scroll_region = enabled;
962 self
963 }
964
965 pub const fn in_tmux(mut self, enabled: bool) -> Self {
969 self.caps.in_tmux = enabled;
970 self
971 }
972
973 pub const fn in_screen(mut self, enabled: bool) -> Self {
975 self.caps.in_screen = enabled;
976 self
977 }
978
979 pub const fn in_zellij(mut self, enabled: bool) -> Self {
981 self.caps.in_zellij = enabled;
982 self
983 }
984
985 pub const fn in_wezterm_mux(mut self, enabled: bool) -> Self {
987 self.caps.in_wezterm_mux = enabled;
988 self
989 }
990
991 pub const fn kitty_keyboard(mut self, enabled: bool) -> Self {
995 self.caps.kitty_keyboard = enabled;
996 self
997 }
998
999 pub const fn focus_events(mut self, enabled: bool) -> Self {
1001 self.caps.focus_events = enabled;
1002 self
1003 }
1004
1005 pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
1007 self.caps.bracketed_paste = enabled;
1008 self
1009 }
1010
1011 pub const fn mouse_sgr(mut self, enabled: bool) -> Self {
1013 self.caps.mouse_sgr = enabled;
1014 self
1015 }
1016
1017 pub const fn osc52_clipboard(mut self, enabled: bool) -> Self {
1021 self.caps.osc52_clipboard = enabled;
1022 self
1023 }
1024}
1025
1026impl TerminalCapabilities {
1027 #[must_use]
1033 pub fn detect() -> Self {
1034 let value = env::var("FTUI_TEST_PROFILE").ok();
1035 Self::detect_with_test_profile_override(value.as_deref())
1036 }
1037
1038 fn detect_with_test_profile_override(value: Option<&str>) -> Self {
1039 if let Some(value) = value
1040 && let Ok(profile) = TerminalProfile::from_str(value.trim())
1041 && profile != TerminalProfile::Detected
1042 {
1043 return Self::from_profile(profile);
1044 }
1045 let env = DetectInputs::from_env();
1046 Self::detect_from_inputs(&env)
1047 }
1048
1049 fn detect_from_inputs(env: &DetectInputs) -> Self {
1050 let term = normalize_terminal_env_value(&env.term);
1051 let term_program = {
1055 let program = normalize_terminal_env_value(&env.term_program);
1056 if program.is_empty()
1057 && normalize_terminal_env_value(&env.lc_terminal).contains("iterm")
1058 {
1059 "iterm.app".to_string()
1060 } else {
1061 program
1062 }
1063 };
1064 let colorterm = normalize_terminal_env_value(&env.colorterm);
1065
1066 let in_tmux = env.in_tmux || term.starts_with("tmux");
1073 let in_screen = env.in_screen || term.starts_with("screen");
1074 let in_zellij = env.in_zellij;
1075
1076 let term_program_is_wezterm = term_program.contains("wezterm");
1080 let term_is_wezterm = term.contains("wezterm");
1081 let in_wezterm_mux = term_program_is_wezterm
1082 || term_is_wezterm
1083 || env.wezterm_unix_socket
1084 || env.wezterm_pane
1085 || env.wezterm_executable;
1086 let in_any_mux = in_tmux || in_screen || in_zellij || in_wezterm_mux;
1087
1088 let is_windows_terminal = env.wt_session;
1090
1091 let is_dumb = is_dumb_terminal(&term, is_windows_terminal);
1096
1097 let is_kitty = env.kitty_window_id || term.contains("kitty");
1099
1100 let is_modern_terminal = MODERN_TERMINALS.iter().any(|t| {
1102 let t_lower = t.to_ascii_lowercase();
1103 term_program.contains(&t_lower) || term.contains(&t_lower)
1104 }) || is_windows_terminal;
1105
1106 let color_depth = ColorDepth::detect_normalized(
1107 env.no_color,
1108 is_dumb,
1109 &colorterm,
1110 &term,
1111 is_modern_terminal || is_kitty,
1112 );
1113
1114 let is_wezterm = term_program_is_wezterm || term_is_wezterm || env.wezterm_executable;
1117
1118 let sync_output = !is_dumb
1124 && !is_wezterm
1125 && (is_kitty
1126 || SYNC_OUTPUT_TERMINALS.iter().any(|t| {
1127 let t_lower = t.to_ascii_lowercase();
1128 term_program.contains(&t_lower) || term.contains(&t_lower)
1129 }));
1130
1131 let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
1133
1134 let scroll_region = !is_dumb;
1136
1137 let kitty_keyboard = !is_dumb
1142 && (is_kitty
1143 || KITTY_KEYBOARD_TERMINALS.iter().any(|t| {
1144 let t_lower = t.to_ascii_lowercase();
1145 term_program.contains(&t_lower) || term.contains(&t_lower)
1146 }));
1147
1148 let focus_events = !is_dumb && (is_modern_terminal || is_kitty);
1150
1151 let bracketed_paste = !is_dumb;
1153
1154 let mouse_sgr = !is_dumb;
1156
1157 let osc52_clipboard = !is_dumb && !in_any_mux && (is_modern_terminal || is_kitty);
1159
1160 let unicode_box_drawing = !is_dumb;
1162 let unicode_emoji = !is_dumb && (is_modern_terminal || is_kitty);
1163 let double_width = !is_dumb && term != "linux";
1164
1165 Self {
1166 profile: TerminalProfile::Detected,
1167 color_depth,
1168 unicode_box_drawing,
1169 unicode_emoji,
1170 double_width,
1171 sync_output,
1172 osc8_hyperlinks,
1173 scroll_region,
1174 in_tmux,
1175 in_screen,
1176 in_zellij,
1177 in_wezterm_mux,
1178 kitty_keyboard,
1179 focus_events,
1180 bracketed_paste,
1181 mouse_sgr,
1182 osc52_clipboard,
1183 }
1184 }
1185
1186 #[must_use]
1191 pub const fn basic() -> Self {
1192 Self {
1193 profile: TerminalProfile::Dumb,
1194 color_depth: ColorDepth::Mono,
1195 unicode_box_drawing: false,
1196 unicode_emoji: false,
1197 double_width: false,
1198 sync_output: false,
1199 osc8_hyperlinks: false,
1200 scroll_region: false,
1201 in_tmux: false,
1202 in_screen: false,
1203 in_zellij: false,
1204 in_wezterm_mux: false,
1205 kitty_keyboard: false,
1206 focus_events: false,
1207 bracketed_paste: false,
1208 mouse_sgr: false,
1209 osc52_clipboard: false,
1210 }
1211 }
1212
1213 #[must_use]
1217 #[inline]
1218 pub const fn in_any_mux(&self) -> bool {
1219 self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux
1220 }
1221
1222 #[must_use]
1224 #[inline]
1225 pub const fn has_color(&self) -> bool {
1226 self.color_depth.supports_color()
1227 }
1228
1229 #[must_use]
1231 pub const fn supports_256_colors(&self) -> bool {
1232 self.color_depth.supports_256_colors()
1233 }
1234
1235 #[must_use]
1237 pub const fn supports_true_color(&self) -> bool {
1238 self.color_depth.supports_true_color()
1239 }
1240
1241 #[must_use]
1252 #[inline]
1253 pub const fn use_sync_output(&self) -> bool {
1254 if self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux {
1255 return false;
1256 }
1257 self.sync_output
1258 }
1259
1260 #[must_use]
1265 #[inline]
1266 pub const fn use_scroll_region(&self) -> bool {
1267 if self.in_any_mux() {
1268 return false;
1269 }
1270 self.scroll_region
1271 }
1272
1273 #[must_use]
1278 #[inline]
1279 pub const fn use_hyperlinks(&self) -> bool {
1280 if self.in_any_mux() {
1281 return false;
1282 }
1283 self.osc8_hyperlinks
1284 }
1285
1286 #[must_use]
1291 #[inline]
1292 pub const fn use_clipboard(&self) -> bool {
1293 if self.in_any_mux() {
1294 return false;
1295 }
1296 self.osc52_clipboard
1297 }
1298
1299 #[must_use]
1305 #[inline]
1306 pub const fn needs_passthrough_wrap(&self) -> bool {
1307 self.in_tmux || self.in_screen
1308 }
1309}
1310
1311pub struct SharedCapabilities {
1334 inner: crate::read_optimized::ArcSwapStore<TerminalCapabilities>,
1335}
1336
1337impl SharedCapabilities {
1338 pub fn new(caps: TerminalCapabilities) -> Self {
1340 Self {
1341 inner: crate::read_optimized::ArcSwapStore::new(caps),
1342 }
1343 }
1344
1345 pub fn detect() -> Self {
1347 Self::new(TerminalCapabilities::detect())
1348 }
1349
1350 #[inline]
1352 pub fn load(&self) -> TerminalCapabilities {
1353 crate::read_optimized::ReadOptimized::load(&self.inner)
1354 }
1355
1356 #[inline]
1358 pub fn store(&self, caps: TerminalCapabilities) {
1359 crate::read_optimized::ReadOptimized::store(&self.inner, caps);
1360 }
1361}
1362
1363#[cfg(test)]
1364mod tests {
1365 use super::*;
1366
1367 fn detect_with_override(value: Option<&str>) -> TerminalCapabilities {
1368 TerminalCapabilities::detect_with_test_profile_override(value)
1369 }
1370
1371 #[test]
1372 fn basic_is_minimal() {
1373 let caps = TerminalCapabilities::basic();
1374 assert_eq!(caps.color_depth, ColorDepth::Mono);
1375 assert!(!caps.sync_output);
1376 assert!(!caps.osc8_hyperlinks);
1377 assert!(!caps.scroll_region);
1378 assert!(!caps.in_tmux);
1379 assert!(!caps.in_screen);
1380 assert!(!caps.in_zellij);
1381 assert!(!caps.kitty_keyboard);
1382 assert!(!caps.focus_events);
1383 assert!(!caps.bracketed_paste);
1384 assert!(!caps.mouse_sgr);
1385 assert!(!caps.osc52_clipboard);
1386 }
1387
1388 #[test]
1389 fn basic_is_default() {
1390 let basic = TerminalCapabilities::basic();
1391 let default = TerminalCapabilities::default();
1392 assert_eq!(basic, default);
1393 }
1394
1395 #[test]
1396 fn in_any_mux_logic() {
1397 let mut caps = TerminalCapabilities::basic();
1398 assert!(!caps.in_any_mux());
1399
1400 caps.in_tmux = true;
1401 assert!(caps.in_any_mux());
1402
1403 caps.in_tmux = false;
1404 caps.in_screen = true;
1405 assert!(caps.in_any_mux());
1406
1407 caps.in_screen = false;
1408 caps.in_zellij = true;
1409 assert!(caps.in_any_mux());
1410
1411 caps.in_zellij = false;
1412 caps.in_wezterm_mux = true;
1413 assert!(caps.in_any_mux());
1414 }
1415
1416 #[test]
1417 fn has_color_logic() {
1418 let mut caps = TerminalCapabilities::basic();
1419 assert!(!caps.has_color());
1420
1421 caps.color_depth = ColorDepth::Ansi16;
1422 assert!(caps.has_color());
1423
1424 caps.color_depth = ColorDepth::TrueColor;
1425 assert!(caps.has_color());
1426 }
1427
1428 #[test]
1429 fn color_depth_identifiers() {
1430 let mut caps = TerminalCapabilities::basic();
1431 assert_eq!(caps.color_depth.as_str(), "mono");
1432
1433 caps.color_depth = ColorDepth::Ansi16;
1434 assert_eq!(caps.color_depth.as_str(), "ansi16");
1435
1436 caps.color_depth = ColorDepth::Ansi256;
1437 assert_eq!(caps.color_depth.as_str(), "ansi256");
1438
1439 caps.color_depth = ColorDepth::TrueColor;
1440 assert_eq!(caps.color_depth.as_str(), "truecolor");
1441 }
1442
1443 #[test]
1444 fn explicit_color_depth_detection_contract() {
1445 let cases = [
1446 (Some(""), Some("truecolor"), Some("xterm"), ColorDepth::Mono),
1447 (None, Some("truecolor"), Some("dumb"), ColorDepth::Mono),
1448 (None, Some("truecolor"), Some(" DUMB "), ColorDepth::Mono),
1449 (None, Some("truecolor"), Some("vt100"), ColorDepth::Mono),
1450 (None, None, Some("xterm"), ColorDepth::Ansi16),
1451 (None, None, Some("xterm-256color"), ColorDepth::Ansi256),
1452 (None, Some("24bit"), Some("xterm"), ColorDepth::TrueColor),
1453 (None, None, Some(" XTERM-DIRECT "), ColorDepth::TrueColor),
1454 (None, None, Some("xterm-direct2"), ColorDepth::TrueColor),
1455 (None, None, Some("xterm-direct16"), ColorDepth::TrueColor),
1456 (None, None, Some("xterm-direct256"), ColorDepth::TrueColor),
1457 (None, None, Some("xterm-truecolor"), ColorDepth::TrueColor),
1458 (None, None, Some("xterm-24bit"), ColorDepth::TrueColor),
1459 (None, None, Some("xterm-no24bit"), ColorDepth::Ansi16),
1460 (
1461 None,
1462 Some("nottruecolor"),
1463 Some("xterm"),
1464 ColorDepth::Ansi16,
1465 ),
1466 (None, Some("no24bit"), Some("xterm"), ColorDepth::Ansi16),
1467 (None, Some("truecolor"), Some("linux"), ColorDepth::Ansi16),
1468 ];
1469
1470 for (no_color, colorterm, term, expected) in cases {
1471 assert_eq!(
1472 ColorDepth::detect_from_env(no_color, colorterm, term),
1473 expected,
1474 "NO_COLOR={no_color:?} COLORTERM={colorterm:?} TERM={term:?}"
1475 );
1476 }
1477 }
1478
1479 #[test]
1480 fn detect_does_not_panic() {
1481 let _caps = TerminalCapabilities::detect();
1483 }
1484
1485 #[test]
1486 fn windows_terminal_not_dumb_when_term_missing() {
1487 let env = DetectInputs {
1488 no_color: false,
1489 term: String::new(),
1490 term_program: String::new(),
1491 lc_terminal: String::new(),
1492 colorterm: String::new(),
1493 in_tmux: false,
1494 in_screen: false,
1495 in_zellij: false,
1496 wezterm_unix_socket: false,
1497 wezterm_pane: false,
1498 wezterm_executable: false,
1499 kitty_window_id: false,
1500 wt_session: true,
1501 };
1502
1503 let caps = TerminalCapabilities::detect_from_inputs(&env);
1504 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1505 assert!(
1506 caps.osc8_hyperlinks,
1507 "WT_SESSION implies OSC 8 hyperlink support by default"
1508 );
1509 assert!(
1510 caps.bracketed_paste,
1511 "WT_SESSION should not be treated as dumb"
1512 );
1513 assert!(caps.mouse_sgr, "WT_SESSION should not be treated as dumb");
1514 }
1515
1516 #[test]
1517 #[cfg(target_os = "windows")]
1518 fn detect_windows_terminal_from_wt_session() {
1519 let mut env = make_env("", "", "");
1520 env.wt_session = true;
1521 let caps = TerminalCapabilities::detect_from_inputs(&env);
1522 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1523 assert!(caps.osc8_hyperlinks, "WT_SESSION implies OSC 8 support");
1524 }
1525
1526 #[test]
1527 fn no_color_disables_color_and_links() {
1528 let env = DetectInputs {
1529 no_color: true,
1530 term: "xterm-256color".to_string(),
1531 term_program: "WezTerm".to_string(),
1532 lc_terminal: String::new(),
1533 colorterm: "truecolor".to_string(),
1534 in_tmux: false,
1535 in_screen: false,
1536 in_zellij: false,
1537 wezterm_unix_socket: false,
1538 wezterm_pane: false,
1539 wezterm_executable: false,
1540 kitty_window_id: false,
1541 wt_session: false,
1542 };
1543
1544 let caps = TerminalCapabilities::detect_from_inputs(&env);
1545 assert_eq!(caps.color_depth, ColorDepth::Mono);
1546 assert!(
1547 !caps.osc8_hyperlinks,
1548 "NO_COLOR must disable OSC 8 hyperlinks"
1549 );
1550 }
1551
1552 #[cfg(unix)]
1553 #[test]
1554 fn non_utf8_no_color_presence_disables_color_without_global_env_mutation() {
1555 use std::os::unix::ffi::OsStringExt as _;
1556
1557 let non_utf8 = OsString::from_vec(vec![0xff]);
1558 let env = DetectInputs::from_env_with(|key| match key {
1559 "NO_COLOR" => Some(non_utf8.clone()),
1560 "TERM" => Some(OsString::from("xterm-direct")),
1561 "TERM_PROGRAM" => Some(OsString::from("WezTerm")),
1562 "COLORTERM" => Some(OsString::from("truecolor")),
1563 _ => None,
1564 });
1565
1566 assert!(env.no_color, "NO_COLOR is a presence-only contract");
1567 let caps = TerminalCapabilities::detect_from_inputs(&env);
1568 assert_eq!(caps.color_depth, ColorDepth::Mono);
1569 assert!(!caps.osc8_hyperlinks);
1570 }
1571
1572 #[test]
1575 fn use_sync_output_disabled_in_tmux() {
1576 let mut caps = TerminalCapabilities::basic();
1577 caps.sync_output = true;
1578 assert!(caps.use_sync_output());
1579
1580 caps.in_tmux = true;
1581 assert!(!caps.use_sync_output());
1582 }
1583
1584 #[test]
1585 fn use_sync_output_disabled_in_screen() {
1586 let mut caps = TerminalCapabilities::basic();
1587 caps.sync_output = true;
1588 caps.in_screen = true;
1589 assert!(!caps.use_sync_output());
1590 }
1591
1592 #[test]
1593 fn use_sync_output_disabled_in_zellij() {
1594 let mut caps = TerminalCapabilities::basic();
1595 caps.sync_output = true;
1596 caps.in_zellij = true;
1597 assert!(!caps.use_sync_output());
1598 }
1599
1600 #[test]
1601 fn use_sync_output_disabled_in_wezterm_mux() {
1602 let mut caps = TerminalCapabilities::basic();
1603 caps.sync_output = true;
1604 caps.in_wezterm_mux = true;
1605 assert!(!caps.use_sync_output());
1606 }
1607
1608 #[test]
1609 fn use_scroll_region_disabled_in_mux() {
1610 let mut caps = TerminalCapabilities::basic();
1611 caps.scroll_region = true;
1612 assert!(caps.use_scroll_region());
1613
1614 caps.in_tmux = true;
1615 assert!(!caps.use_scroll_region());
1616
1617 caps.in_tmux = false;
1618 caps.in_screen = true;
1619 assert!(!caps.use_scroll_region());
1620
1621 caps.in_screen = false;
1622 caps.in_zellij = true;
1623 assert!(!caps.use_scroll_region());
1624
1625 caps.in_zellij = false;
1626 caps.in_wezterm_mux = true;
1627 assert!(!caps.use_scroll_region());
1628 }
1629
1630 #[test]
1631 fn use_hyperlinks_disabled_in_mux() {
1632 let mut caps = TerminalCapabilities::basic();
1633 caps.osc8_hyperlinks = true;
1634 assert!(caps.use_hyperlinks());
1635
1636 caps.in_tmux = true;
1637 assert!(!caps.use_hyperlinks());
1638
1639 caps.in_tmux = false;
1640 caps.in_wezterm_mux = true;
1641 assert!(!caps.use_hyperlinks());
1642 }
1643
1644 #[test]
1645 fn use_clipboard_disabled_in_mux() {
1646 let mut caps = TerminalCapabilities::basic();
1647 caps.osc52_clipboard = true;
1648 assert!(caps.use_clipboard());
1649
1650 caps.in_screen = true;
1651 assert!(!caps.use_clipboard());
1652
1653 caps.in_screen = false;
1654 caps.in_wezterm_mux = true;
1655 assert!(!caps.use_clipboard());
1656 }
1657
1658 #[test]
1659 fn needs_passthrough_wrap_only_for_tmux_screen() {
1660 let mut caps = TerminalCapabilities::basic();
1661 assert!(!caps.needs_passthrough_wrap());
1662
1663 caps.in_tmux = true;
1664 assert!(caps.needs_passthrough_wrap());
1665
1666 caps.in_tmux = false;
1667 caps.in_screen = true;
1668 assert!(caps.needs_passthrough_wrap());
1669
1670 caps.in_screen = false;
1672 caps.in_zellij = true;
1673 assert!(!caps.needs_passthrough_wrap());
1674 }
1675
1676 #[test]
1677 fn policies_return_false_when_capability_absent() {
1678 let caps = TerminalCapabilities::basic();
1680 assert!(!caps.use_sync_output());
1681 assert!(!caps.use_scroll_region());
1682 assert!(!caps.use_hyperlinks());
1683 assert!(!caps.use_clipboard());
1684 }
1685
1686 fn make_env(term: &str, term_program: &str, colorterm: &str) -> DetectInputs {
1689 DetectInputs {
1690 no_color: false,
1691 term: term.to_string(),
1692 term_program: term_program.to_string(),
1693 lc_terminal: String::new(),
1694 colorterm: colorterm.to_string(),
1695 in_tmux: false,
1696 in_screen: false,
1697 in_zellij: false,
1698 wezterm_unix_socket: false,
1699 wezterm_pane: false,
1700 wezterm_executable: false,
1701 kitty_window_id: false,
1702 wt_session: false,
1703 }
1704 }
1705
1706 #[test]
1707 fn detect_dumb_terminal() {
1708 for term in ["dumb", "DUMB", " dumb "] {
1709 let env = make_env(term, " WezTerm ", " TRUECOLOR ");
1710 let caps = TerminalCapabilities::detect_from_inputs(&env);
1711 assert_eq!(caps.color_depth, ColorDepth::Mono, "TERM={term:?}");
1712 assert!(!caps.sync_output, "TERM={term:?}");
1713 assert!(!caps.osc8_hyperlinks, "TERM={term:?}");
1714 assert!(!caps.scroll_region, "TERM={term:?}");
1715 assert!(!caps.focus_events, "TERM={term:?}");
1716 assert!(!caps.bracketed_paste, "TERM={term:?}");
1717 assert!(!caps.mouse_sgr, "TERM={term:?}");
1718 }
1719 }
1720
1721 #[test]
1722 fn detect_dumb_overrides_truecolor_env() {
1723 let env = make_env("dumb", "WezTerm", "truecolor");
1724 let caps = TerminalCapabilities::detect_from_inputs(&env);
1725 assert_eq!(caps.color_depth, ColorDepth::Mono);
1726 assert!(!caps.bracketed_paste);
1727 assert!(!caps.mouse_sgr);
1728 assert!(!caps.osc8_hyperlinks);
1729 }
1730
1731 #[test]
1732 fn detect_dumb_disables_kitty_keyboard() {
1733 for term_program in ["kitty", "ghostty", "iTerm.app", "WezTerm"] {
1737 let env = make_env("dumb", term_program, "truecolor");
1738 let caps = TerminalCapabilities::detect_from_inputs(&env);
1739 assert!(
1740 !caps.kitty_keyboard,
1741 "TERM=dumb + TERM_PROGRAM={term_program} must disable kitty keyboard"
1742 );
1743 }
1744 let env = make_env("xterm-kitty", "kitty", "truecolor");
1746 let caps = TerminalCapabilities::detect_from_inputs(&env);
1747 assert!(caps.kitty_keyboard);
1748 }
1749
1750 #[test]
1751 fn detect_term_identity_counts_as_mux_evidence() {
1752 let env = make_env("tmux-256color", "", "");
1757 let caps = TerminalCapabilities::detect_from_inputs(&env);
1758 assert!(caps.in_tmux, "TERM=tmux-256color implies tmux");
1759 assert!(caps.in_any_mux());
1760 assert!(!caps.use_scroll_region());
1761
1762 let env = make_env("screen-256color", "", "");
1763 let caps = TerminalCapabilities::detect_from_inputs(&env);
1764 assert!(caps.in_screen, "TERM=screen-256color implies screen");
1765 assert!(caps.in_any_mux());
1766 assert!(!caps.use_scroll_region());
1767
1768 let env = make_env("xterm-256color", "", "");
1770 let caps = TerminalCapabilities::detect_from_inputs(&env);
1771 assert!(!caps.in_tmux);
1772 assert!(!caps.in_screen);
1773 }
1774
1775 #[test]
1776 fn from_profile_custom_roundtrips() {
1777 let caps = TerminalCapabilities::from_profile(TerminalProfile::Custom);
1778 assert_eq!(caps.profile(), TerminalProfile::Custom);
1779 let basic = TerminalCapabilities::basic();
1781 assert_eq!(caps.color_depth, basic.color_depth);
1782 assert_eq!(caps.scroll_region, basic.scroll_region);
1783 }
1784
1785 #[test]
1786 fn detect_empty_term_is_dumb() {
1787 let env = make_env("", "", "");
1788 let caps = TerminalCapabilities::detect_from_inputs(&env);
1789 assert_eq!(caps.color_depth, ColorDepth::Mono);
1790 assert!(!caps.bracketed_paste);
1791 }
1792
1793 #[test]
1794 fn detect_xterm_256color() {
1795 let env = make_env("xterm-256color", "", "");
1796 let caps = TerminalCapabilities::detect_from_inputs(&env);
1797 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
1798 assert!(caps.bracketed_paste);
1799 assert!(caps.mouse_sgr);
1800 assert!(caps.scroll_region);
1801 }
1802
1803 #[test]
1804 fn detect_colorterm_truecolor() {
1805 let env = make_env("xterm-256color", "", "truecolor");
1806 let caps = TerminalCapabilities::detect_from_inputs(&env);
1807 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1808 }
1809
1810 #[test]
1811 fn detect_colorterm_24bit() {
1812 let env = make_env("xterm-256color", "", "24bit");
1813 let caps = TerminalCapabilities::detect_from_inputs(&env);
1814 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1815 }
1816
1817 #[test]
1818 fn detect_kitty_by_window_id() {
1819 let mut env = make_env("xterm-kitty", "", "");
1820 env.kitty_window_id = true;
1821 let caps = TerminalCapabilities::detect_from_inputs(&env);
1822 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1823 assert!(
1824 caps.kitty_keyboard,
1825 "Kitty supports kitty keyboard protocol"
1826 );
1827 assert!(caps.sync_output, "Kitty supports sync output");
1828 }
1829
1830 #[test]
1831 fn detect_kitty_by_term() {
1832 let env = make_env("xterm-kitty", "", "");
1833 let caps = TerminalCapabilities::detect_from_inputs(&env);
1834 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1835 assert!(caps.kitty_keyboard);
1836 }
1837
1838 #[test]
1841 fn detect_alacritty_via_term_only() {
1842 let env = make_env("alacritty", "", "truecolor");
1843 let caps = TerminalCapabilities::detect_from_inputs(&env);
1844 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1845 assert!(caps.sync_output, "Alacritty implements DEC 2026");
1846 assert!(caps.use_sync_output());
1847 assert!(caps.osc8_hyperlinks);
1848 assert!(caps.kitty_keyboard);
1849 assert!(!caps.in_any_mux());
1850 }
1851
1852 #[test]
1856 fn detect_iterm2_via_lc_terminal_when_term_program_missing() {
1857 let mut env = make_env("xterm-256color", "", "");
1858 env.lc_terminal = "iTerm2".to_string();
1859 let caps = TerminalCapabilities::detect_from_inputs(&env);
1860 assert_eq!(caps.color_depth, ColorDepth::TrueColor, "modern terminal");
1861 assert!(caps.osc8_hyperlinks);
1862 assert!(caps.kitty_keyboard);
1863 assert!(
1864 !caps.sync_output,
1865 "not allowlisted: left to the DECRPM probe"
1866 );
1867
1868 let mut env = make_env("xterm-256color", "Apple_Terminal", "");
1870 env.lc_terminal = "iTerm2".to_string();
1871 let caps = TerminalCapabilities::detect_from_inputs(&env);
1872 assert!(
1873 !caps.osc8_hyperlinks,
1874 "Apple Terminal is not on the allowlist"
1875 );
1876 }
1877
1878 #[test]
1879 fn detect_wezterm() {
1880 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1881 let caps = TerminalCapabilities::detect_from_inputs(&env);
1882 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1883 assert!(
1884 caps.in_wezterm_mux,
1885 "WezTerm identity is treated as conservative mux evidence"
1886 );
1887 assert!(caps.in_any_mux());
1888 assert!(
1889 !caps.sync_output,
1890 "WezTerm sync output is hard-disabled as a safety fallback"
1891 );
1892 assert!(caps.osc8_hyperlinks, "WezTerm supports hyperlinks");
1893 assert!(caps.kitty_keyboard, "WezTerm supports kitty keyboard");
1894 assert!(caps.focus_events);
1895 assert!(
1896 !caps.osc52_clipboard,
1897 "conservative mux policy should disable raw OSC52 detection"
1898 );
1899 assert!(!caps.use_scroll_region());
1900 assert!(!caps.use_hyperlinks());
1901 assert!(!caps.use_clipboard());
1902 }
1903
1904 #[test]
1905 fn detect_wezterm_mux_socket_disables_sync_policy() {
1906 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1907 env.wezterm_unix_socket = true;
1908 let caps = TerminalCapabilities::detect_from_inputs(&env);
1909 assert!(!caps.sync_output);
1910 assert!(caps.in_wezterm_mux, "wezterm mux marker should be detected");
1911 assert!(
1912 caps.in_any_mux(),
1913 "wezterm mux must participate in in_any_mux()"
1914 );
1915 assert!(
1916 !caps.use_sync_output(),
1917 "policy must suppress sync output in wezterm mux sessions"
1918 );
1919 assert!(
1920 !caps.use_scroll_region(),
1921 "policy must suppress scroll region in wezterm mux sessions"
1922 );
1923 assert!(
1924 !caps.use_hyperlinks(),
1925 "policy must suppress hyperlinks in wezterm mux sessions"
1926 );
1927 assert!(
1928 !caps.use_clipboard(),
1929 "policy must suppress clipboard in wezterm mux sessions"
1930 );
1931 }
1932
1933 #[test]
1934 fn detect_wezterm_mux_socket_without_term_program_disables_sync_policy() {
1935 let mut env = make_env("xterm-256color", "", "truecolor");
1936 env.wezterm_unix_socket = true;
1937 let caps = TerminalCapabilities::detect_from_inputs(&env);
1938 assert!(
1939 caps.in_wezterm_mux,
1940 "socket marker alone must detect wezterm mux"
1941 );
1942 assert!(
1943 caps.in_any_mux(),
1944 "wezterm mux must participate in in_any_mux()"
1945 );
1946 assert!(
1947 !caps.use_sync_output(),
1948 "policy must suppress sync output when wezterm mux socket is present"
1949 );
1950 }
1951
1952 #[test]
1953 fn detect_wezterm_mux_pane_disables_sync_policy() {
1954 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1955 env.wezterm_pane = true;
1956 let caps = TerminalCapabilities::detect_from_inputs(&env);
1957 assert!(!caps.sync_output);
1958 assert!(
1959 caps.in_wezterm_mux,
1960 "wezterm pane marker should be detected"
1961 );
1962 assert!(
1963 caps.in_any_mux(),
1964 "wezterm mux must participate in in_any_mux()"
1965 );
1966 assert!(
1967 !caps.use_sync_output(),
1968 "policy must suppress sync output when wezterm pane marker is present"
1969 );
1970 }
1971
1972 #[test]
1973 fn detect_wezterm_mux_pane_without_term_program_disables_sync_policy() {
1974 let mut env = make_env("xterm-256color", "", "truecolor");
1975 env.wezterm_pane = true;
1976 let caps = TerminalCapabilities::detect_from_inputs(&env);
1977 assert!(
1978 caps.in_wezterm_mux,
1979 "pane marker alone must detect wezterm mux"
1980 );
1981 assert!(
1982 caps.in_any_mux(),
1983 "wezterm mux must participate in in_any_mux()"
1984 );
1985 assert!(
1986 !caps.use_sync_output(),
1987 "policy must suppress sync output when wezterm pane marker is present"
1988 );
1989 }
1990
1991 #[test]
1992 fn detect_wezterm_executable_without_term_program_is_conservative_mux() {
1993 let mut env = make_env("xterm-256color", "", "truecolor");
1994 env.wezterm_executable = true;
1995 let caps = TerminalCapabilities::detect_from_inputs(&env);
1996 assert!(
1997 caps.in_wezterm_mux,
1998 "WEZTERM_EXECUTABLE fallback should conservatively mark mux context"
1999 );
2000 assert!(
2001 !caps.use_sync_output(),
2002 "fallback mux detection must suppress sync output policy"
2003 );
2004 }
2005
2006 #[test]
2007 fn detect_wezterm_executable_overrides_explicit_non_wezterm_program() {
2008 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
2009 env.wezterm_executable = true;
2010 let caps = TerminalCapabilities::detect_from_inputs(&env);
2011 assert!(
2012 caps.in_wezterm_mux,
2013 "WEZTERM_EXECUTABLE should conservatively force wezterm mux policy"
2014 );
2015 assert!(
2016 !caps.sync_output,
2017 "raw sync_output capability should be disabled under conservative wezterm marker handling"
2018 );
2019 assert!(
2020 !caps.use_sync_output(),
2021 "mux policy should disable sync output under WEZTERM_EXECUTABLE marker"
2022 );
2023 }
2024
2025 #[test]
2026 fn detect_wezterm_socket_overrides_explicit_non_wezterm_program() {
2027 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
2028 env.wezterm_unix_socket = true;
2029 let caps = TerminalCapabilities::detect_from_inputs(&env);
2030 assert!(
2031 caps.in_wezterm_mux,
2032 "WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
2033 );
2034 assert!(
2035 !caps.use_sync_output(),
2036 "mux policy should disable sync output with socket marker"
2037 );
2038 }
2039
2040 #[test]
2041 fn detect_wezterm_pane_overrides_explicit_non_wezterm_program() {
2042 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
2043 env.wezterm_pane = true;
2044 let caps = TerminalCapabilities::detect_from_inputs(&env);
2045 assert!(
2046 caps.in_wezterm_mux,
2047 "WEZTERM_PANE should conservatively force wezterm mux policy"
2048 );
2049 assert!(
2050 !caps.use_sync_output(),
2051 "mux policy should disable sync output with pane marker"
2052 );
2053 }
2054
2055 #[test]
2056 fn detect_wezterm_socket_overrides_explicit_non_wezterm_term_identity() {
2057 let mut env = make_env("xterm-ghostty", "", "truecolor");
2058 env.wezterm_unix_socket = true;
2059 let caps = TerminalCapabilities::detect_from_inputs(&env);
2060 assert!(
2061 caps.in_wezterm_mux,
2062 "WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
2063 );
2064 assert!(
2065 !caps.use_sync_output(),
2066 "mux policy should disable sync output with socket marker"
2067 );
2068 }
2069
2070 #[test]
2071 #[cfg(target_os = "macos")]
2072 fn detect_iterm2_from_term_program() {
2073 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
2074 let caps = TerminalCapabilities::detect_from_inputs(&env);
2075 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2076 assert!(caps.osc8_hyperlinks, "iTerm2 supports OSC 8 hyperlinks");
2077 }
2078
2079 #[test]
2080 fn detect_alacritty() {
2081 let env = make_env("alacritty", "Alacritty", "truecolor");
2082 let caps = TerminalCapabilities::detect_from_inputs(&env);
2083 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2084 assert!(caps.sync_output);
2085 assert!(caps.osc8_hyperlinks);
2086 assert!(caps.kitty_keyboard);
2087 assert!(caps.focus_events);
2088 }
2089
2090 #[test]
2091 fn detect_ghostty() {
2092 let env = make_env("xterm-ghostty", "Ghostty", "truecolor");
2093 let caps = TerminalCapabilities::detect_from_inputs(&env);
2094 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2095 assert!(caps.sync_output);
2096 assert!(caps.osc8_hyperlinks);
2097 assert!(caps.kitty_keyboard);
2098 assert!(caps.focus_events);
2099 }
2100
2101 #[test]
2102 fn detect_iterm() {
2103 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
2104 let caps = TerminalCapabilities::detect_from_inputs(&env);
2105 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2106 assert!(caps.osc8_hyperlinks);
2107 assert!(caps.kitty_keyboard);
2108 assert!(caps.focus_events);
2109 }
2110
2111 #[test]
2112 fn detect_vscode_terminal() {
2113 let env = make_env("xterm-256color", "vscode", "truecolor");
2114 let caps = TerminalCapabilities::detect_from_inputs(&env);
2115 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2116 assert!(caps.osc8_hyperlinks);
2117 assert!(caps.focus_events);
2118 }
2119
2120 #[test]
2123 fn detect_in_tmux() {
2124 let mut env = make_env("screen-256color", "", "");
2125 env.in_tmux = true;
2126 let caps = TerminalCapabilities::detect_from_inputs(&env);
2127 assert!(caps.in_tmux);
2128 assert!(caps.in_any_mux());
2129 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2130 assert!(!caps.osc52_clipboard, "clipboard disabled in tmux");
2131 }
2132
2133 #[test]
2134 fn detect_in_screen() {
2135 let mut env = make_env("screen", "", "");
2136 env.in_screen = true;
2137 let caps = TerminalCapabilities::detect_from_inputs(&env);
2138 assert!(caps.in_screen);
2139 assert!(caps.in_any_mux());
2140 assert!(caps.needs_passthrough_wrap());
2141 }
2142
2143 #[test]
2144 fn detect_in_zellij() {
2145 let mut env = make_env("xterm-256color", "", "truecolor");
2146 env.in_zellij = true;
2147 let caps = TerminalCapabilities::detect_from_inputs(&env);
2148 assert!(caps.in_zellij);
2149 assert!(caps.in_any_mux());
2150 assert!(
2151 !caps.needs_passthrough_wrap(),
2152 "Zellij handles passthrough natively"
2153 );
2154 assert!(!caps.osc52_clipboard, "clipboard disabled in mux");
2155 }
2156
2157 #[test]
2158 fn detect_modern_terminal_in_tmux() {
2159 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2160 env.in_tmux = true;
2161 let caps = TerminalCapabilities::detect_from_inputs(&env);
2162 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2164 assert!(!caps.sync_output);
2165 assert!(!caps.use_sync_output());
2167 assert!(!caps.use_hyperlinks());
2168 assert!(!caps.use_scroll_region());
2169 }
2170
2171 #[test]
2174 fn no_color_overrides_everything() {
2175 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2176 env.no_color = true;
2177 let caps = TerminalCapabilities::detect_from_inputs(&env);
2178 assert_eq!(caps.color_depth, ColorDepth::Mono);
2179 assert!(!caps.osc8_hyperlinks);
2180 assert!(!caps.sync_output);
2182 assert!(caps.bracketed_paste);
2183 assert!(caps.mouse_sgr);
2184 }
2185
2186 #[test]
2189 fn unknown_term_program() {
2190 let env = make_env("xterm", "SomeUnknownTerminal", "");
2191 let caps = TerminalCapabilities::detect_from_inputs(&env);
2192 assert!(
2193 !caps.supports_true_color(),
2194 "unknown terminal should not assume truecolor"
2195 );
2196 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2197 assert!(!caps.osc8_hyperlinks);
2198 assert!(caps.bracketed_paste);
2200 assert!(caps.mouse_sgr);
2201 assert!(caps.scroll_region);
2202 }
2203
2204 #[test]
2205 fn all_mux_flags_simultaneous() {
2206 let mut env = make_env("screen", "", "");
2207 env.in_tmux = true;
2208 env.in_screen = true;
2209 env.in_zellij = true;
2210 let caps = TerminalCapabilities::detect_from_inputs(&env);
2211 assert!(caps.in_any_mux());
2212 assert!(caps.needs_passthrough_wrap());
2213 assert!(!caps.use_sync_output());
2214 assert!(!caps.use_hyperlinks());
2215 assert!(!caps.use_clipboard());
2216 }
2217
2218 #[test]
2221 fn detect_rio() {
2222 let env = make_env("xterm-256color", "Rio", "truecolor");
2223 let caps = TerminalCapabilities::detect_from_inputs(&env);
2224 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2225 assert!(caps.osc8_hyperlinks);
2226 assert!(caps.kitty_keyboard);
2227 assert!(caps.focus_events);
2228 }
2229
2230 #[test]
2231 fn detect_contour() {
2232 let env = make_env("xterm-256color", "Contour", "truecolor");
2233 let caps = TerminalCapabilities::detect_from_inputs(&env);
2234 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2235 assert!(caps.sync_output);
2236 assert!(caps.osc8_hyperlinks);
2237 assert!(caps.focus_events);
2238 }
2239
2240 #[test]
2241 fn detect_foot() {
2242 let env = make_env("foot", "foot", "truecolor");
2243 let caps = TerminalCapabilities::detect_from_inputs(&env);
2244 assert!(caps.kitty_keyboard, "foot supports kitty keyboard");
2245 }
2246
2247 #[test]
2248 fn detect_hyper() {
2249 let env = make_env("xterm-256color", "Hyper", "truecolor");
2250 let caps = TerminalCapabilities::detect_from_inputs(&env);
2251 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2252 assert!(caps.osc8_hyperlinks);
2253 assert!(caps.focus_events);
2254 }
2255
2256 #[test]
2257 fn detect_linux_console() {
2258 let env = make_env("linux", "", "");
2259 let detected = TerminalCapabilities::detect_from_inputs(&env);
2260 let mut expected = TerminalCapabilities::linux_console();
2261 expected.profile = TerminalProfile::Detected;
2262
2263 assert_eq!(
2264 detected, expected,
2265 "the predefined Linux console profile must match TERM=linux detection"
2266 );
2267
2268 let policy = crate::glyph_policy::GlyphPolicy::from_env_with(
2269 |key| (key == "TERM").then(|| "linux".to_string()),
2270 &detected,
2271 );
2272 assert_eq!(policy.mode, crate::glyph_policy::GlyphMode::Ascii);
2273 assert!(!policy.double_width);
2274 }
2275
2276 #[test]
2277 fn detect_linux_console_caps_color_despite_conflicting_positive_signals() {
2278 let expected = TerminalCapabilities::linux_console().color_depth;
2279
2280 for (term_program, colorterm) in [
2281 ("WezTerm", "truecolor"),
2282 ("kitty", "24bit"),
2283 ("Ghostty", ""),
2284 ] {
2285 let env = make_env(" linux ", term_program, colorterm);
2286 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2287 let explicit = ColorDepth::detect_from_env(None, Some(colorterm), Some(" linux "));
2288
2289 assert_eq!(detected, expected, "TERM_PROGRAM={term_program:?}");
2290 assert_eq!(explicit, expected, "COLORTERM={colorterm:?}");
2291 }
2292 }
2293
2294 #[test]
2295 fn detect_direct_color_term_variants() {
2296 for term in [
2297 "xterm-direct",
2298 "xterm-direct2",
2299 "xterm-direct16",
2300 "xterm-direct256",
2301 "xterm-truecolor",
2302 "xterm-24bit",
2303 " XTERM-DIRECT ",
2304 ] {
2305 let env = make_env(term, "", "");
2306 let caps = TerminalCapabilities::detect_from_inputs(&env);
2307 assert_eq!(caps.color_depth, ColorDepth::TrueColor, "TERM={term:?}");
2308 assert!(caps.bracketed_paste, "TERM={term:?}");
2309 assert!(caps.mouse_sgr, "TERM={term:?}");
2310 }
2311 }
2312
2313 #[test]
2314 fn direct_color_detection_rejects_negated_and_partial_tokens() {
2315 for term in [
2316 "xterm-indirect",
2317 "xterm-direct-color",
2318 "xterm-no24bit",
2319 "xterm-24bit-disabled",
2320 ] {
2321 let env = make_env(term, "", "");
2322 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2323 let explicit = ColorDepth::detect_from_env(None, None, Some(term));
2324
2325 assert_eq!(detected, ColorDepth::Ansi16, "TERM={term:?}");
2326 assert_eq!(explicit, detected, "TERM={term:?}");
2327 }
2328
2329 for colorterm in [
2330 "nottruecolor",
2331 "truecolor-disabled",
2332 "no24bit",
2333 "24bit-disabled",
2334 ] {
2335 let env = make_env("xterm", "", colorterm);
2336 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2337 let explicit = ColorDepth::detect_from_env(None, Some(colorterm), Some("xterm"));
2338
2339 assert_eq!(detected, ColorDepth::Ansi16, "COLORTERM={colorterm:?}");
2340 assert_eq!(explicit, detected, "COLORTERM={colorterm:?}");
2341 }
2342 }
2343
2344 #[test]
2345 fn detect_plain_xterm_is_ansi16() {
2346 let env = make_env("xterm", "", "");
2347 let caps = TerminalCapabilities::detect_from_inputs(&env);
2348 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2349 assert!(caps.bracketed_paste);
2350 assert!(caps.mouse_sgr);
2351 }
2352
2353 #[test]
2354 fn explicit_and_full_color_detection_agree() {
2355 for (no_color, colorterm, term) in [
2356 (false, "", "xterm"),
2357 (false, "", "xterm-256color"),
2358 (false, "", " XTERM-DIRECT "),
2359 (false, "", "xterm-direct2"),
2360 (false, "", "xterm-direct16"),
2361 (false, "", "xterm-direct256"),
2362 (false, "24BIT", "xterm"),
2363 (false, "no24bit", "xterm"),
2364 (false, "truecolor", "linux"),
2365 (false, "truecolor", " VT100 "),
2366 (false, "truecolor", " DUMB "),
2367 (true, "truecolor", "xterm-direct"),
2368 ] {
2369 let mut env = make_env(term, "", colorterm);
2370 env.no_color = no_color;
2371 let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
2372 let explicit =
2373 ColorDepth::detect_from_env(no_color.then_some(""), Some(colorterm), Some(term));
2374 assert_eq!(
2375 detected, explicit,
2376 "NO_COLOR={no_color} COLORTERM={colorterm:?} TERM={term:?}"
2377 );
2378 }
2379 }
2380
2381 #[test]
2382 fn detect_vt100_is_monochrome() {
2383 let env = make_env("vt100", "", "");
2384 let caps = TerminalCapabilities::detect_from_inputs(&env);
2385 assert_eq!(caps.color_depth, ColorDepth::Mono);
2386 }
2387
2388 #[test]
2389 fn detect_screen_256color() {
2390 let env = make_env("screen-256color", "", "");
2391 let caps = TerminalCapabilities::detect_from_inputs(&env);
2392 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2393 }
2394
2395 #[test]
2398 fn wezterm_without_colorterm() {
2399 let env = make_env("xterm-256color", "WezTerm", "");
2400 let caps = TerminalCapabilities::detect_from_inputs(&env);
2401 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2403 assert!(!caps.sync_output);
2404 assert!(caps.osc8_hyperlinks);
2405 }
2406
2407 #[test]
2408 fn alacritty_via_term_only() {
2409 let env = make_env("alacritty", "", "");
2411 let caps = TerminalCapabilities::detect_from_inputs(&env);
2412 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2414 assert!(caps.osc8_hyperlinks);
2415 }
2416
2417 #[test]
2420 fn kitty_via_term_without_window_id() {
2421 let env = make_env("xterm-kitty", "", "");
2422 let caps = TerminalCapabilities::detect_from_inputs(&env);
2423 assert!(caps.kitty_keyboard);
2424 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2425 assert!(caps.sync_output);
2426 }
2427
2428 #[test]
2429 fn kitty_window_id_with_generic_term() {
2430 let mut env = make_env("xterm-256color", "", "");
2431 env.kitty_window_id = true;
2432 let caps = TerminalCapabilities::detect_from_inputs(&env);
2433 assert!(caps.kitty_keyboard);
2434 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2435 }
2436
2437 #[test]
2440 fn use_clipboard_enabled_when_no_mux_and_modern() {
2441 let env = make_env("xterm-256color", "Alacritty", "truecolor");
2442 let caps = TerminalCapabilities::detect_from_inputs(&env);
2443 assert!(caps.osc52_clipboard);
2444 assert!(caps.use_clipboard());
2445 }
2446
2447 #[test]
2448 fn use_clipboard_disabled_in_tmux_even_if_detected() {
2449 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2450 env.in_tmux = true;
2451 let caps = TerminalCapabilities::detect_from_inputs(&env);
2452 assert!(!caps.osc52_clipboard);
2454 assert!(!caps.use_clipboard());
2455 }
2456
2457 #[test]
2458 fn scroll_region_enabled_for_basic_xterm() {
2459 let env = make_env("xterm", "", "");
2460 let caps = TerminalCapabilities::detect_from_inputs(&env);
2461 assert!(caps.scroll_region);
2462 assert!(caps.use_scroll_region());
2463 }
2464
2465 #[test]
2466 fn no_color_preserves_non_visual_features() {
2467 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2468 env.no_color = true;
2469 let caps = TerminalCapabilities::detect_from_inputs(&env);
2470 assert_eq!(caps.color_depth, ColorDepth::Mono);
2472 assert!(!caps.osc8_hyperlinks);
2473 assert!(!caps.sync_output);
2475 assert!(caps.kitty_keyboard);
2476 assert!(caps.focus_events);
2477 assert!(caps.bracketed_paste);
2478 assert!(caps.mouse_sgr);
2479 }
2480
2481 #[test]
2484 fn colorterm_yes_not_truecolor() {
2485 let env = make_env("xterm-256color", "", "yes");
2486 let caps = TerminalCapabilities::detect_from_inputs(&env);
2487 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2488 }
2489
2490 #[test]
2493 fn profile_enum_as_str() {
2494 assert_eq!(TerminalProfile::Modern.as_str(), "modern");
2495 assert_eq!(TerminalProfile::Xterm256Color.as_str(), "xterm-256color");
2496 assert_eq!(TerminalProfile::Vt100.as_str(), "vt100");
2497 assert_eq!(TerminalProfile::Dumb.as_str(), "dumb");
2498 assert_eq!(TerminalProfile::Tmux.as_str(), "tmux");
2499 assert_eq!(TerminalProfile::Screen.as_str(), "screen");
2500 assert_eq!(TerminalProfile::Kitty.as_str(), "kitty");
2501 }
2502
2503 #[test]
2504 fn profile_enum_from_str() {
2505 use std::str::FromStr;
2506 assert_eq!(
2507 TerminalProfile::from_str("modern"),
2508 Ok(TerminalProfile::Modern)
2509 );
2510 assert_eq!(
2511 TerminalProfile::from_str("xterm-256color"),
2512 Ok(TerminalProfile::Xterm256Color)
2513 );
2514 assert_eq!(
2515 TerminalProfile::from_str("xterm256color"),
2516 Ok(TerminalProfile::Xterm256Color)
2517 );
2518 assert_eq!(TerminalProfile::from_str("DUMB"), Ok(TerminalProfile::Dumb));
2519 assert!(TerminalProfile::from_str("unknown").is_err());
2520 }
2521
2522 #[test]
2523 fn profile_all_predefined() {
2524 let all = TerminalProfile::all_predefined();
2525 assert!(all.len() >= 10);
2526 assert!(all.contains(&TerminalProfile::Modern));
2527 assert!(all.contains(&TerminalProfile::Dumb));
2528 assert!(!all.contains(&TerminalProfile::Custom));
2529 assert!(!all.contains(&TerminalProfile::Detected));
2530 }
2531
2532 #[test]
2533 fn profile_modern_has_all_features() {
2534 let caps = TerminalCapabilities::modern();
2535 assert_eq!(caps.profile(), TerminalProfile::Modern);
2536 assert_eq!(caps.profile_name(), Some("modern"));
2537 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2538 assert!(caps.sync_output);
2539 assert!(caps.osc8_hyperlinks);
2540 assert!(caps.scroll_region);
2541 assert!(caps.kitty_keyboard);
2542 assert!(caps.focus_events);
2543 assert!(caps.bracketed_paste);
2544 assert!(caps.mouse_sgr);
2545 assert!(caps.osc52_clipboard);
2546 assert!(!caps.in_any_mux());
2547 }
2548
2549 #[test]
2550 fn profile_xterm_256color() {
2551 let caps = TerminalCapabilities::xterm_256color();
2552 assert_eq!(caps.profile(), TerminalProfile::Xterm256Color);
2553 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2554 assert!(!caps.sync_output);
2555 assert!(!caps.osc8_hyperlinks);
2556 assert!(caps.scroll_region);
2557 assert!(caps.bracketed_paste);
2558 assert!(caps.mouse_sgr);
2559 }
2560
2561 #[test]
2562 fn profile_xterm_basic() {
2563 let caps = TerminalCapabilities::xterm();
2564 assert_eq!(caps.profile(), TerminalProfile::Xterm);
2565 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2566 assert!(caps.scroll_region);
2567 }
2568
2569 #[test]
2570 fn profile_vt100_minimal() {
2571 let caps = TerminalCapabilities::vt100();
2572 assert_eq!(caps.profile(), TerminalProfile::Vt100);
2573 assert_eq!(caps.color_depth, ColorDepth::Mono);
2574 assert!(caps.scroll_region);
2575 assert!(!caps.bracketed_paste);
2576 assert!(!caps.mouse_sgr);
2577 }
2578
2579 #[test]
2580 fn profile_dumb_no_features() {
2581 let caps = TerminalCapabilities::dumb();
2582 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2583 assert_eq!(caps.color_depth, ColorDepth::Mono);
2584 assert!(!caps.scroll_region);
2585 assert!(!caps.bracketed_paste);
2586 assert!(!caps.mouse_sgr);
2587 assert!(!caps.use_sync_output());
2588 assert!(!caps.use_scroll_region());
2589 }
2590
2591 #[test]
2592 fn profile_tmux_mux_flags() {
2593 let caps = TerminalCapabilities::tmux();
2594 assert_eq!(caps.profile(), TerminalProfile::Tmux);
2595 assert!(caps.in_tmux);
2596 assert!(!caps.in_screen);
2597 assert!(!caps.in_zellij);
2598 assert!(caps.in_any_mux());
2599 assert!(!caps.use_sync_output());
2601 assert!(!caps.use_scroll_region());
2602 assert!(!caps.use_hyperlinks());
2603 }
2604
2605 #[test]
2606 fn profile_screen_mux_flags() {
2607 let caps = TerminalCapabilities::screen();
2608 assert_eq!(caps.profile(), TerminalProfile::Screen);
2609 assert!(!caps.in_tmux);
2610 assert!(caps.in_screen);
2611 assert!(caps.in_any_mux());
2612 assert!(caps.needs_passthrough_wrap());
2613 }
2614
2615 #[test]
2616 fn profile_zellij_mux_flags() {
2617 let caps = TerminalCapabilities::zellij();
2618 assert_eq!(caps.profile(), TerminalProfile::Zellij);
2619 assert!(caps.in_zellij);
2620 assert!(caps.in_any_mux());
2621 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2623 assert!(caps.focus_events);
2624 assert!(!caps.needs_passthrough_wrap());
2626 }
2627
2628 #[test]
2629 fn profile_kitty_full_features() {
2630 let caps = TerminalCapabilities::kitty();
2631 assert_eq!(caps.profile(), TerminalProfile::Kitty);
2632 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2633 assert!(caps.sync_output);
2634 assert!(caps.kitty_keyboard);
2635 assert!(caps.osc8_hyperlinks);
2636 }
2637
2638 #[test]
2639 fn profile_windows_console() {
2640 let caps = TerminalCapabilities::windows_console();
2641 assert_eq!(caps.profile(), TerminalProfile::WindowsConsole);
2642 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2643 assert!(caps.osc8_hyperlinks);
2644 assert!(caps.focus_events);
2645 }
2646
2647 #[test]
2648 fn profile_linux_console() {
2649 let caps = TerminalCapabilities::linux_console();
2650 assert_eq!(caps.profile(), TerminalProfile::LinuxConsole);
2651 assert_eq!(caps.color_depth, ColorDepth::Ansi16);
2652 assert!(!caps.double_width);
2653 assert!(caps.scroll_region);
2654 }
2655
2656 #[test]
2657 fn from_profile_roundtrip() {
2658 for profile in TerminalProfile::all_predefined() {
2659 let caps = TerminalCapabilities::from_profile(*profile);
2660 assert_eq!(caps.profile(), *profile);
2661 }
2662 }
2663
2664 #[test]
2665 fn detected_profile_has_none_name() {
2666 let caps = detect_with_override(None);
2667 assert_eq!(caps.profile(), TerminalProfile::Detected);
2668 assert_eq!(caps.profile_name(), None);
2669 }
2670
2671 #[test]
2672 fn detect_respects_test_profile_env() {
2673 let caps = detect_with_override(Some("dumb"));
2674 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2675 }
2676
2677 #[test]
2678 fn detect_ignores_invalid_test_profile() {
2679 let caps = detect_with_override(Some("not-a-real-profile"));
2680 assert_eq!(caps.profile(), TerminalProfile::Detected);
2681 }
2682
2683 #[test]
2684 fn basic_has_dumb_profile() {
2685 let caps = TerminalCapabilities::basic();
2686 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2687 }
2688
2689 #[test]
2692 fn builder_starts_empty() {
2693 let caps = CapabilityProfileBuilder::new().build();
2694 assert_eq!(caps.profile(), TerminalProfile::Custom);
2695 assert_eq!(caps.color_depth, ColorDepth::Mono);
2696 assert!(!caps.sync_output);
2697 assert!(!caps.scroll_region);
2698 assert!(!caps.mouse_sgr);
2699 }
2700
2701 #[test]
2702 fn builder_sets_one_canonical_color_depth() {
2703 let caps = CapabilityProfileBuilder::new()
2704 .color_depth(ColorDepth::TrueColor)
2705 .build();
2706 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2707 }
2708
2709 #[test]
2710 fn builder_set_advanced() {
2711 let caps = CapabilityProfileBuilder::new()
2712 .sync_output(true)
2713 .osc8_hyperlinks(true)
2714 .scroll_region(true)
2715 .build();
2716 assert!(caps.sync_output);
2717 assert!(caps.osc8_hyperlinks);
2718 assert!(caps.scroll_region);
2719 }
2720
2721 #[test]
2722 fn builder_set_mux() {
2723 let caps = CapabilityProfileBuilder::new()
2724 .in_tmux(true)
2725 .in_screen(false)
2726 .in_zellij(false)
2727 .build();
2728 assert!(caps.in_tmux);
2729 assert!(!caps.in_screen);
2730 assert!(caps.in_any_mux());
2731 }
2732
2733 #[test]
2734 fn builder_set_input() {
2735 let caps = CapabilityProfileBuilder::new()
2736 .kitty_keyboard(true)
2737 .focus_events(true)
2738 .bracketed_paste(true)
2739 .mouse_sgr(true)
2740 .build();
2741 assert!(caps.kitty_keyboard);
2742 assert!(caps.focus_events);
2743 assert!(caps.bracketed_paste);
2744 assert!(caps.mouse_sgr);
2745 }
2746
2747 #[test]
2748 fn builder_set_clipboard() {
2749 let caps = CapabilityProfileBuilder::new()
2750 .osc52_clipboard(true)
2751 .build();
2752 assert!(caps.osc52_clipboard);
2753 }
2754
2755 #[test]
2756 fn builder_from_profile() {
2757 let caps = CapabilityProfileBuilder::from_profile(TerminalProfile::Modern)
2758 .sync_output(false) .build();
2760 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
2762 assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
2764 assert_eq!(caps.profile(), TerminalProfile::Custom);
2766 }
2767
2768 #[test]
2769 fn builder_chain_multiple() {
2770 let caps = TerminalCapabilities::builder()
2771 .color_depth(ColorDepth::Ansi256)
2772 .bracketed_paste(true)
2773 .mouse_sgr(true)
2774 .scroll_region(true)
2775 .build();
2776 assert_eq!(caps.color_depth, ColorDepth::Ansi256);
2777 assert!(caps.bracketed_paste);
2778 assert!(caps.mouse_sgr);
2779 assert!(caps.scroll_region);
2780 assert!(!caps.supports_true_color());
2781 assert!(!caps.sync_output);
2782 }
2783
2784 #[test]
2785 fn builder_default() {
2786 let builder = CapabilityProfileBuilder::default();
2787 let caps = builder.build();
2788 assert_eq!(caps.profile(), TerminalProfile::Custom);
2789 }
2790
2791 #[test]
2800 fn mux_compatibility_matrix() {
2801 {
2806 let caps = TerminalCapabilities::modern();
2807 assert!(
2808 caps.use_sync_output(),
2809 "baseline: sync_output should be enabled"
2810 );
2811 assert!(
2812 caps.use_scroll_region(),
2813 "baseline: scroll_region should be enabled"
2814 );
2815 assert!(
2816 caps.use_hyperlinks(),
2817 "baseline: hyperlinks should be enabled"
2818 );
2819 assert!(!caps.needs_passthrough_wrap(), "baseline: no wrap needed");
2820 }
2821
2822 {
2824 let caps = TerminalCapabilities::tmux();
2825 assert!(!caps.use_sync_output(), "tmux: sync_output disabled");
2826 assert!(!caps.use_scroll_region(), "tmux: scroll_region disabled");
2827 assert!(!caps.use_hyperlinks(), "tmux: hyperlinks disabled");
2828 assert!(caps.needs_passthrough_wrap(), "tmux: needs wrap");
2829 }
2830
2831 {
2833 let caps = TerminalCapabilities::screen();
2834 assert!(!caps.use_sync_output(), "screen: sync_output disabled");
2835 assert!(!caps.use_scroll_region(), "screen: scroll_region disabled");
2836 assert!(!caps.use_hyperlinks(), "screen: hyperlinks disabled");
2837 assert!(caps.needs_passthrough_wrap(), "screen: needs wrap");
2838 }
2839
2840 {
2842 let caps = TerminalCapabilities::zellij();
2843 assert!(!caps.use_sync_output(), "zellij: sync_output disabled");
2844 assert!(!caps.use_scroll_region(), "zellij: scroll_region disabled");
2845 assert!(!caps.use_hyperlinks(), "zellij: hyperlinks disabled");
2846 assert!(
2847 !caps.needs_passthrough_wrap(),
2848 "zellij: no wrap needed (native passthrough)"
2849 );
2850 }
2851
2852 {
2854 let caps = TerminalCapabilities::builder()
2855 .in_wezterm_mux(true)
2856 .sync_output(true)
2857 .scroll_region(true)
2858 .osc8_hyperlinks(true)
2859 .build();
2860 assert!(!caps.use_sync_output(), "wezterm mux: sync_output disabled");
2861 assert!(
2862 !caps.use_scroll_region(),
2863 "wezterm mux: scroll_region disabled"
2864 );
2865 assert!(!caps.use_hyperlinks(), "wezterm mux: hyperlinks disabled");
2866 assert!(
2867 !caps.needs_passthrough_wrap(),
2868 "wezterm mux: no wrap needed"
2869 );
2870 }
2871 }
2872
2873 #[test]
2875 fn modern_terminal_in_mux_matrix() {
2876 for (mux_name, in_tmux, in_screen, in_zellij, in_wezterm_mux) in [
2880 ("tmux", true, false, false, false),
2881 ("screen", false, true, false, false),
2882 ("zellij", false, false, true, false),
2883 ("wezterm-mux", false, false, false, true),
2884 ] {
2885 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2886 env.in_tmux = in_tmux;
2887 env.in_screen = in_screen;
2888 env.in_zellij = in_zellij;
2889 env.wezterm_unix_socket = in_wezterm_mux;
2890 let caps = TerminalCapabilities::detect_from_inputs(&env);
2891
2892 assert!(
2894 caps.supports_true_color(),
2895 "{mux_name}: true_color detection should work"
2896 );
2897 assert!(
2898 !caps.sync_output,
2899 "{mux_name}: sync_output hard-disabled for WezTerm safety"
2900 );
2901
2902 assert!(
2904 !caps.use_sync_output(),
2905 "{mux_name}: use_sync_output() should be false"
2906 );
2907 assert!(
2908 !caps.use_scroll_region(),
2909 "{mux_name}: use_scroll_region() should be false"
2910 );
2911 assert!(
2912 !caps.use_hyperlinks(),
2913 "{mux_name}: use_hyperlinks() should be false"
2914 );
2915 }
2916 }
2917
2918 #[test]
2920 fn profile_mux_invariant_matrix() {
2921 for profile in TerminalProfile::all_predefined() {
2923 let caps = TerminalCapabilities::from_profile(*profile);
2924 let name = profile.as_str();
2925
2926 let expected_mux =
2928 caps.in_tmux || caps.in_screen || caps.in_zellij || caps.in_wezterm_mux;
2929 assert_eq!(
2930 caps.in_any_mux(),
2931 expected_mux,
2932 "{name}: in_any_mux() should match individual flags"
2933 );
2934
2935 if caps.in_any_mux() {
2937 assert!(
2938 !caps.use_sync_output(),
2939 "{name}: mux should disable use_sync_output()"
2940 );
2941 assert!(
2942 !caps.use_scroll_region(),
2943 "{name}: mux should disable use_scroll_region()"
2944 );
2945 assert!(
2946 !caps.use_hyperlinks(),
2947 "{name}: mux should disable use_hyperlinks()"
2948 );
2949 }
2950
2951 if caps.in_tmux || caps.in_screen {
2953 assert!(
2954 caps.needs_passthrough_wrap(),
2955 "{name}: tmux/screen should need passthrough wrap"
2956 );
2957 } else if caps.in_zellij {
2958 assert!(
2959 !caps.needs_passthrough_wrap(),
2960 "{name}: zellij should NOT need passthrough wrap"
2961 );
2962 }
2963 }
2964 }
2965
2966 #[test]
2968 fn fallback_ordering_matrix() {
2969 use crate::inline_mode::InlineStrategy;
2970
2971 let caps_full = TerminalCapabilities::builder()
2973 .sync_output(true)
2974 .scroll_region(true)
2975 .build();
2976 assert_eq!(
2977 InlineStrategy::select(&caps_full),
2978 InlineStrategy::ScrollRegion,
2979 "full capabilities should use ScrollRegion"
2980 );
2981
2982 let caps_hybrid = TerminalCapabilities::builder()
2984 .sync_output(false)
2985 .scroll_region(true)
2986 .build();
2987 assert_eq!(
2988 InlineStrategy::select(&caps_hybrid),
2989 InlineStrategy::Hybrid,
2990 "scroll without sync should use Hybrid"
2991 );
2992
2993 let caps_none = TerminalCapabilities::builder()
2995 .sync_output(false)
2996 .scroll_region(false)
2997 .build();
2998 assert_eq!(
2999 InlineStrategy::select(&caps_none),
3000 InlineStrategy::OverlayRedraw,
3001 "no capabilities should use OverlayRedraw"
3002 );
3003
3004 let caps_tmux = TerminalCapabilities::tmux();
3006 assert_eq!(
3007 InlineStrategy::select(&caps_tmux),
3008 InlineStrategy::OverlayRedraw,
3009 "tmux should force OverlayRedraw"
3010 );
3011 }
3012
3013 #[test]
3015 fn terminal_mux_strategy_matrix() {
3016 use crate::inline_mode::InlineStrategy;
3017
3018 struct TestCase {
3019 name: &'static str,
3020 profile: TerminalProfile,
3021 expected: InlineStrategy,
3022 }
3023
3024 let cases = [
3025 TestCase {
3026 name: "modern (no mux)",
3027 profile: TerminalProfile::Modern,
3028 expected: InlineStrategy::ScrollRegion,
3029 },
3030 TestCase {
3031 name: "kitty (no mux)",
3032 profile: TerminalProfile::Kitty,
3033 expected: InlineStrategy::ScrollRegion,
3034 },
3035 TestCase {
3036 name: "xterm-256color (no mux)",
3037 profile: TerminalProfile::Xterm256Color,
3038 expected: InlineStrategy::Hybrid, },
3040 TestCase {
3041 name: "xterm (no mux)",
3042 profile: TerminalProfile::Xterm,
3043 expected: InlineStrategy::Hybrid,
3044 },
3045 TestCase {
3046 name: "vt100 (no mux)",
3047 profile: TerminalProfile::Vt100,
3048 expected: InlineStrategy::Hybrid,
3049 },
3050 TestCase {
3051 name: "dumb",
3052 profile: TerminalProfile::Dumb,
3053 expected: InlineStrategy::OverlayRedraw, },
3055 TestCase {
3056 name: "tmux",
3057 profile: TerminalProfile::Tmux,
3058 expected: InlineStrategy::OverlayRedraw,
3059 },
3060 TestCase {
3061 name: "screen",
3062 profile: TerminalProfile::Screen,
3063 expected: InlineStrategy::OverlayRedraw,
3064 },
3065 TestCase {
3066 name: "zellij",
3067 profile: TerminalProfile::Zellij,
3068 expected: InlineStrategy::OverlayRedraw,
3069 },
3070 ];
3071
3072 for case in cases {
3073 let caps = TerminalCapabilities::from_profile(case.profile);
3074 let actual = InlineStrategy::select(&caps);
3075 assert_eq!(
3076 actual, case.expected,
3077 "{}: expected {:?}, got {:?}",
3078 case.name, case.expected, actual
3079 );
3080 }
3081 }
3082
3083 #[test]
3086 fn shared_caps_load_returns_initial() {
3087 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
3088 assert_eq!(shared.load().color_depth, ColorDepth::TrueColor);
3089 assert!(shared.load().sync_output);
3090 }
3091
3092 #[test]
3093 fn shared_caps_store_replaces_value() {
3094 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
3095 shared.store(TerminalCapabilities::dumb());
3096 let loaded = shared.load();
3097 assert_eq!(loaded.color_depth, ColorDepth::Mono);
3098 assert!(!loaded.sync_output);
3099 }
3100
3101 #[test]
3102 fn shared_caps_concurrent_read_write() {
3103 use std::sync::{Arc, Barrier};
3104 use std::thread;
3105
3106 let shared = Arc::new(SharedCapabilities::new(TerminalCapabilities::basic()));
3107 let barrier = Arc::new(Barrier::new(5)); let readers: Vec<_> = (0..4)
3110 .map(|_| {
3111 let s = Arc::clone(&shared);
3112 let b = Arc::clone(&barrier);
3113 thread::spawn(move || {
3114 b.wait();
3115 for _ in 0..10_000 {
3116 let caps = s.load();
3117 let _ = caps.use_sync_output();
3119 let _ = caps.color_depth;
3120 }
3121 })
3122 })
3123 .collect();
3124
3125 let writer = {
3126 let s = Arc::clone(&shared);
3127 let b = Arc::clone(&barrier);
3128 thread::spawn(move || {
3129 b.wait();
3130 for i in 0..1_000 {
3131 if i % 2 == 0 {
3132 s.store(TerminalCapabilities::modern());
3133 } else {
3134 s.store(TerminalCapabilities::dumb());
3135 }
3136 }
3137 })
3138 };
3139
3140 writer.join().unwrap();
3141 for h in readers {
3142 h.join().unwrap();
3143 }
3144 }
3145}
3146
3147#[cfg(test)]
3152mod proptests {
3153 use super::*;
3154 use proptest::prelude::*;
3155
3156 proptest! {
3157 #[test]
3159 fn prop_in_any_mux_consistent(
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 ) {
3165 let caps = TerminalCapabilities::builder()
3166 .in_tmux(in_tmux)
3167 .in_screen(in_screen)
3168 .in_zellij(in_zellij)
3169 .in_wezterm_mux(in_wezterm_mux)
3170 .build();
3171
3172 let expected = in_tmux || in_screen || in_zellij || in_wezterm_mux;
3173 prop_assert_eq!(caps.in_any_mux(), expected);
3174 }
3175
3176 #[test]
3178 fn prop_mux_disables_sync_output(
3179 in_tmux in any::<bool>(),
3180 in_screen in any::<bool>(),
3181 in_zellij in any::<bool>(),
3182 in_wezterm_mux in any::<bool>(),
3183 sync_output in any::<bool>(),
3184 ) {
3185 let caps = TerminalCapabilities::builder()
3186 .in_tmux(in_tmux)
3187 .in_screen(in_screen)
3188 .in_zellij(in_zellij)
3189 .in_wezterm_mux(in_wezterm_mux)
3190 .sync_output(sync_output)
3191 .build();
3192
3193 if caps.in_any_mux() {
3194 prop_assert!(!caps.use_sync_output(), "mux should disable sync_output policy");
3195 }
3196 }
3197
3198 #[test]
3200 fn prop_mux_disables_scroll_region(
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 scroll_region in any::<bool>(),
3206 ) {
3207 let caps = TerminalCapabilities::builder()
3208 .in_tmux(in_tmux)
3209 .in_screen(in_screen)
3210 .in_zellij(in_zellij)
3211 .in_wezterm_mux(in_wezterm_mux)
3212 .scroll_region(scroll_region)
3213 .build();
3214
3215 if caps.in_any_mux() {
3216 prop_assert!(!caps.use_scroll_region(), "mux should disable scroll_region policy");
3217 }
3218 }
3219
3220 #[test]
3222 fn prop_mux_disables_hyperlinks(
3223 in_tmux in any::<bool>(),
3224 in_screen in any::<bool>(),
3225 in_zellij in any::<bool>(),
3226 in_wezterm_mux in any::<bool>(),
3227 osc8_hyperlinks in any::<bool>(),
3228 ) {
3229 let caps = TerminalCapabilities::builder()
3230 .in_tmux(in_tmux)
3231 .in_screen(in_screen)
3232 .in_zellij(in_zellij)
3233 .in_wezterm_mux(in_wezterm_mux)
3234 .osc8_hyperlinks(osc8_hyperlinks)
3235 .build();
3236
3237 if caps.in_any_mux() {
3238 prop_assert!(!caps.use_hyperlinks(), "mux should disable hyperlinks policy");
3239 }
3240 }
3241
3242 #[test]
3244 fn prop_passthrough_wrap_logic(
3245 in_tmux in any::<bool>(),
3246 in_screen in any::<bool>(),
3247 in_zellij in any::<bool>(),
3248 in_wezterm_mux in any::<bool>(),
3249 ) {
3250 let caps = TerminalCapabilities::builder()
3251 .in_tmux(in_tmux)
3252 .in_screen(in_screen)
3253 .in_zellij(in_zellij)
3254 .in_wezterm_mux(in_wezterm_mux)
3255 .build();
3256
3257 let expected = in_tmux || in_screen; prop_assert_eq!(caps.needs_passthrough_wrap(), expected);
3259 }
3260
3261 #[test]
3263 fn prop_policy_false_when_capability_off(
3264 in_tmux in any::<bool>(),
3265 in_screen in any::<bool>(),
3266 in_zellij in any::<bool>(),
3267 in_wezterm_mux in any::<bool>(),
3268 ) {
3269 let caps = TerminalCapabilities::builder()
3270 .in_tmux(in_tmux)
3271 .in_screen(in_screen)
3272 .in_zellij(in_zellij)
3273 .in_wezterm_mux(in_wezterm_mux)
3274 .sync_output(false)
3275 .scroll_region(false)
3276 .osc8_hyperlinks(false)
3277 .osc52_clipboard(false)
3278 .build();
3279
3280 prop_assert!(!caps.use_sync_output(), "sync_output=false implies use_sync_output()=false");
3281 prop_assert!(!caps.use_scroll_region(), "scroll_region=false implies use_scroll_region()=false");
3282 prop_assert!(!caps.use_hyperlinks(), "osc8_hyperlinks=false implies use_hyperlinks()=false");
3283 prop_assert!(!caps.use_clipboard(), "osc52_clipboard=false implies use_clipboard()=false");
3284 }
3285
3286 #[test]
3288 fn prop_no_color_preserves_non_visual(no_color in any::<bool>()) {
3289 let env = DetectInputs {
3290 no_color,
3291 term: "xterm-256color".to_string(),
3292 term_program: "WezTerm".to_string(),
3293 lc_terminal: String::new(),
3294 colorterm: "truecolor".to_string(),
3295 in_tmux: false,
3296 in_screen: false,
3297 in_zellij: false,
3298 wezterm_unix_socket: false,
3299 wezterm_pane: false,
3300 wezterm_executable: false,
3301 kitty_window_id: false,
3302 wt_session: false,
3303 };
3304 let caps = TerminalCapabilities::detect_from_inputs(&env);
3305
3306 if no_color {
3307 prop_assert_eq!(caps.color_depth, ColorDepth::Mono);
3308 prop_assert!(!caps.osc8_hyperlinks, "NO_COLOR disables hyperlinks");
3309 }
3310
3311 prop_assert!(
3313 !caps.sync_output,
3314 "WezTerm sync_output stays disabled despite NO_COLOR"
3315 );
3316 prop_assert!(caps.bracketed_paste, "bracketed_paste preserved despite NO_COLOR");
3317 }
3318 }
3319}