1#![forbid(unsafe_code)]
2
3use std::env;
118use std::str::FromStr;
119
120#[derive(Debug, Clone)]
121struct DetectInputs {
122 no_color: bool,
123 term: String,
124 term_program: String,
125 colorterm: String,
126 in_tmux: bool,
127 in_screen: bool,
128 in_zellij: bool,
129 wezterm_unix_socket: bool,
130 wezterm_pane: bool,
131 wezterm_executable: bool,
132 kitty_window_id: bool,
133 wt_session: bool,
134}
135
136impl DetectInputs {
137 fn from_env() -> Self {
138 Self {
139 no_color: env::var("NO_COLOR").is_ok(),
140 term: env::var("TERM").unwrap_or_default(),
141 term_program: env::var("TERM_PROGRAM").unwrap_or_default(),
142 colorterm: env::var("COLORTERM").unwrap_or_default(),
143 in_tmux: env::var("TMUX").is_ok(),
144 in_screen: env::var("STY").is_ok(),
145 in_zellij: env::var("ZELLIJ").is_ok(),
146 wezterm_unix_socket: env::var("WEZTERM_UNIX_SOCKET").is_ok(),
147 wezterm_pane: env::var("WEZTERM_PANE").is_ok(),
148 wezterm_executable: env::var("WEZTERM_EXECUTABLE").is_ok(),
149 kitty_window_id: env::var("KITTY_WINDOW_ID").is_ok(),
150 wt_session: env::var("WT_SESSION").is_ok(),
151 }
152 }
153}
154
155const MODERN_TERMINALS: &[&str] = &[
157 "iTerm.app",
158 "WezTerm",
159 "Alacritty",
160 "Ghostty",
161 "kitty",
162 "Rio",
163 "Hyper",
164 "Contour",
165 "vscode",
166 "Black Box",
167];
168
169const KITTY_KEYBOARD_TERMINALS: &[&str] = &[
171 "iTerm.app",
172 "WezTerm",
173 "Alacritty",
174 "Ghostty",
175 "Rio",
176 "kitty",
177 "foot",
178 "Black Box",
179];
180
181const SYNC_OUTPUT_TERMINALS: &[&str] = &["Alacritty", "Ghostty", "kitty", "Contour"];
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
191pub enum TerminalProfile {
192 Modern,
194 Xterm256Color,
196 Xterm,
198 Vt100,
200 Dumb,
202 Screen,
204 Tmux,
206 Zellij,
208 WindowsConsole,
210 Kitty,
212 LinuxConsole,
214 Custom,
216 Detected,
218}
219
220impl TerminalProfile {
221 #[must_use]
223 pub const fn as_str(&self) -> &'static str {
224 match self {
225 Self::Modern => "modern",
226 Self::Xterm256Color => "xterm-256color",
227 Self::Xterm => "xterm",
228 Self::Vt100 => "vt100",
229 Self::Dumb => "dumb",
230 Self::Screen => "screen",
231 Self::Tmux => "tmux",
232 Self::Zellij => "zellij",
233 Self::WindowsConsole => "windows-console",
234 Self::Kitty => "kitty",
235 Self::LinuxConsole => "linux",
236 Self::Custom => "custom",
237 Self::Detected => "detected",
238 }
239 }
240
241 #[must_use]
243 pub const fn all_predefined() -> &'static [Self] {
244 &[
245 Self::Modern,
246 Self::Xterm256Color,
247 Self::Xterm,
248 Self::Vt100,
249 Self::Dumb,
250 Self::Screen,
251 Self::Tmux,
252 Self::Zellij,
253 Self::WindowsConsole,
254 Self::Kitty,
255 Self::LinuxConsole,
256 ]
257 }
258}
259
260impl std::str::FromStr for TerminalProfile {
261 type Err = ();
262
263 fn from_str(s: &str) -> Result<Self, Self::Err> {
264 match s.to_lowercase().as_str() {
265 "modern" => Ok(Self::Modern),
266 "xterm-256color" | "xterm256color" | "xterm-256" => Ok(Self::Xterm256Color),
267 "xterm" => Ok(Self::Xterm),
268 "vt100" => Ok(Self::Vt100),
269 "dumb" => Ok(Self::Dumb),
270 "screen" | "screen-256color" => Ok(Self::Screen),
271 "tmux" | "tmux-256color" => Ok(Self::Tmux),
272 "zellij" => Ok(Self::Zellij),
273 "windows-console" | "windows" | "conhost" => Ok(Self::WindowsConsole),
274 "kitty" | "xterm-kitty" => Ok(Self::Kitty),
275 "linux" | "linux-console" => Ok(Self::LinuxConsole),
276 "custom" => Ok(Self::Custom),
277 "detected" | "auto" => Ok(Self::Detected),
278 _ => Err(()),
279 }
280 }
281}
282
283impl std::fmt::Display for TerminalProfile {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 write!(f, "{}", self.as_str())
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub struct TerminalCapabilities {
307 profile: TerminalProfile,
309
310 pub true_color: bool,
313 pub colors_256: bool,
315
316 pub unicode_box_drawing: bool,
319 pub unicode_emoji: bool,
321 pub double_width: bool,
323
324 pub sync_output: bool,
327 pub osc8_hyperlinks: bool,
329 pub scroll_region: bool,
331
332 pub in_tmux: bool,
335 pub in_screen: bool,
337 pub in_zellij: bool,
339 pub in_wezterm_mux: bool,
344
345 pub kitty_keyboard: bool,
348 pub focus_events: bool,
350 pub bracketed_paste: bool,
352 pub mouse_sgr: bool,
354
355 pub osc52_clipboard: bool,
358}
359
360impl Default for TerminalCapabilities {
361 fn default() -> Self {
362 Self::basic()
363 }
364}
365
366impl TerminalCapabilities {
371 #[must_use]
375 pub const fn profile(&self) -> TerminalProfile {
376 self.profile
377 }
378
379 #[must_use]
384 pub fn profile_name(&self) -> Option<&'static str> {
385 match self.profile {
386 TerminalProfile::Detected => None,
387 p => Some(p.as_str()),
388 }
389 }
390
391 #[must_use]
393 pub fn from_profile(profile: TerminalProfile) -> Self {
394 match profile {
395 TerminalProfile::Modern => Self::modern(),
396 TerminalProfile::Xterm256Color => Self::xterm_256color(),
397 TerminalProfile::Xterm => Self::xterm(),
398 TerminalProfile::Vt100 => Self::vt100(),
399 TerminalProfile::Dumb => Self::dumb(),
400 TerminalProfile::Screen => Self::screen(),
401 TerminalProfile::Tmux => Self::tmux(),
402 TerminalProfile::Zellij => Self::zellij(),
403 TerminalProfile::WindowsConsole => Self::windows_console(),
404 TerminalProfile::Kitty => Self::kitty(),
405 TerminalProfile::LinuxConsole => Self::linux_console(),
406 TerminalProfile::Custom => Self::basic(),
407 TerminalProfile::Detected => Self::detect(),
408 }
409 }
410
411 #[must_use]
418 pub const fn modern() -> Self {
419 Self {
420 profile: TerminalProfile::Modern,
421 true_color: true,
422 colors_256: true,
423 unicode_box_drawing: true,
424 unicode_emoji: true,
425 double_width: true,
426 sync_output: true,
427 osc8_hyperlinks: true,
428 scroll_region: true,
429 in_tmux: false,
430 in_screen: false,
431 in_zellij: false,
432 in_wezterm_mux: false,
433 kitty_keyboard: true,
434 focus_events: true,
435 bracketed_paste: true,
436 mouse_sgr: true,
437 osc52_clipboard: true,
438 }
439 }
440
441 #[must_use]
446 pub const fn xterm_256color() -> Self {
447 Self {
448 profile: TerminalProfile::Xterm256Color,
449 true_color: false,
450 colors_256: true,
451 unicode_box_drawing: true,
452 unicode_emoji: true,
453 double_width: true,
454 sync_output: false,
455 osc8_hyperlinks: false,
456 scroll_region: true,
457 in_tmux: false,
458 in_screen: false,
459 in_zellij: false,
460 in_wezterm_mux: false,
461 kitty_keyboard: false,
462 focus_events: false,
463 bracketed_paste: true,
464 mouse_sgr: true,
465 osc52_clipboard: false,
466 }
467 }
468
469 #[must_use]
473 pub const fn xterm() -> Self {
474 Self {
475 profile: TerminalProfile::Xterm,
476 true_color: false,
477 colors_256: false,
478 unicode_box_drawing: true,
479 unicode_emoji: false,
480 double_width: true,
481 sync_output: false,
482 osc8_hyperlinks: false,
483 scroll_region: true,
484 in_tmux: false,
485 in_screen: false,
486 in_zellij: false,
487 in_wezterm_mux: false,
488 kitty_keyboard: false,
489 focus_events: false,
490 bracketed_paste: true,
491 mouse_sgr: true,
492 osc52_clipboard: false,
493 }
494 }
495
496 #[must_use]
500 pub const fn vt100() -> Self {
501 Self {
502 profile: TerminalProfile::Vt100,
503 true_color: false,
504 colors_256: false,
505 unicode_box_drawing: false,
506 unicode_emoji: false,
507 double_width: false,
508 sync_output: false,
509 osc8_hyperlinks: false,
510 scroll_region: true,
511 in_tmux: false,
512 in_screen: false,
513 in_zellij: false,
514 in_wezterm_mux: false,
515 kitty_keyboard: false,
516 focus_events: false,
517 bracketed_paste: false,
518 mouse_sgr: false,
519 osc52_clipboard: false,
520 }
521 }
522
523 #[must_use]
527 pub const fn dumb() -> Self {
528 Self {
529 profile: TerminalProfile::Dumb,
530 true_color: false,
531 colors_256: false,
532 unicode_box_drawing: false,
533 unicode_emoji: false,
534 double_width: false,
535 sync_output: false,
536 osc8_hyperlinks: false,
537 scroll_region: false,
538 in_tmux: false,
539 in_screen: false,
540 in_zellij: false,
541 in_wezterm_mux: false,
542 kitty_keyboard: false,
543 focus_events: false,
544 bracketed_paste: false,
545 mouse_sgr: false,
546 osc52_clipboard: false,
547 }
548 }
549
550 #[must_use]
555 pub const fn screen() -> Self {
556 Self {
557 profile: TerminalProfile::Screen,
558 true_color: false,
559 colors_256: true,
560 unicode_box_drawing: true,
561 unicode_emoji: true,
562 double_width: true,
563 sync_output: false,
564 osc8_hyperlinks: false,
565 scroll_region: true,
566 in_tmux: false,
567 in_screen: true,
568 in_zellij: false,
569 in_wezterm_mux: false,
570 kitty_keyboard: false,
571 focus_events: false,
572 bracketed_paste: true,
573 mouse_sgr: true,
574 osc52_clipboard: false,
575 }
576 }
577
578 #[must_use]
583 pub const fn tmux() -> Self {
584 Self {
585 profile: TerminalProfile::Tmux,
586 true_color: false,
587 colors_256: true,
588 unicode_box_drawing: true,
589 unicode_emoji: true,
590 double_width: true,
591 sync_output: false,
592 osc8_hyperlinks: false,
593 scroll_region: true,
594 in_tmux: true,
595 in_screen: false,
596 in_zellij: false,
597 in_wezterm_mux: false,
598 kitty_keyboard: false,
599 focus_events: false,
600 bracketed_paste: true,
601 mouse_sgr: true,
602 osc52_clipboard: false,
603 }
604 }
605
606 #[must_use]
610 pub const fn zellij() -> Self {
611 Self {
612 profile: TerminalProfile::Zellij,
613 true_color: true,
614 colors_256: true,
615 unicode_box_drawing: true,
616 unicode_emoji: true,
617 double_width: true,
618 sync_output: false,
619 osc8_hyperlinks: false,
620 scroll_region: true,
621 in_tmux: false,
622 in_screen: false,
623 in_zellij: true,
624 in_wezterm_mux: false,
625 kitty_keyboard: false,
626 focus_events: true,
627 bracketed_paste: true,
628 mouse_sgr: true,
629 osc52_clipboard: false,
630 }
631 }
632
633 #[must_use]
637 pub const fn windows_console() -> Self {
638 Self {
639 profile: TerminalProfile::WindowsConsole,
640 true_color: true,
641 colors_256: true,
642 unicode_box_drawing: true,
643 unicode_emoji: true,
644 double_width: true,
645 sync_output: false,
646 osc8_hyperlinks: true,
647 scroll_region: true,
648 in_tmux: false,
649 in_screen: false,
650 in_zellij: false,
651 in_wezterm_mux: false,
652 kitty_keyboard: false,
653 focus_events: true,
654 bracketed_paste: true,
655 mouse_sgr: true,
656 osc52_clipboard: true,
657 }
658 }
659
660 #[must_use]
664 pub const fn kitty() -> Self {
665 Self {
666 profile: TerminalProfile::Kitty,
667 true_color: true,
668 colors_256: true,
669 unicode_box_drawing: true,
670 unicode_emoji: true,
671 double_width: true,
672 sync_output: true,
673 osc8_hyperlinks: true,
674 scroll_region: true,
675 in_tmux: false,
676 in_screen: false,
677 in_zellij: false,
678 in_wezterm_mux: false,
679 kitty_keyboard: true,
680 focus_events: true,
681 bracketed_paste: true,
682 mouse_sgr: true,
683 osc52_clipboard: true,
684 }
685 }
686
687 #[must_use]
691 pub const fn linux_console() -> Self {
692 Self {
693 profile: TerminalProfile::LinuxConsole,
694 true_color: false,
695 colors_256: false,
696 unicode_box_drawing: true,
697 unicode_emoji: false,
698 double_width: false,
699 sync_output: false,
700 osc8_hyperlinks: false,
701 scroll_region: true,
702 in_tmux: false,
703 in_screen: false,
704 in_zellij: false,
705 in_wezterm_mux: false,
706 kitty_keyboard: false,
707 focus_events: false,
708 bracketed_paste: true,
709 mouse_sgr: true,
710 osc52_clipboard: false,
711 }
712 }
713
714 pub fn builder() -> CapabilityProfileBuilder {
718 CapabilityProfileBuilder::new()
719 }
720}
721
722#[derive(Debug, Clone)]
747#[must_use]
748pub struct CapabilityProfileBuilder {
749 caps: TerminalCapabilities,
750}
751
752impl Default for CapabilityProfileBuilder {
753 fn default() -> Self {
754 Self::new()
755 }
756}
757
758impl CapabilityProfileBuilder {
759 pub fn new() -> Self {
761 Self {
762 caps: TerminalCapabilities {
763 profile: TerminalProfile::Custom,
764 true_color: false,
765 colors_256: false,
766 unicode_box_drawing: false,
767 unicode_emoji: false,
768 double_width: false,
769 sync_output: false,
770 osc8_hyperlinks: false,
771 scroll_region: false,
772 in_tmux: false,
773 in_screen: false,
774 in_zellij: false,
775 in_wezterm_mux: false,
776 kitty_keyboard: false,
777 focus_events: false,
778 bracketed_paste: false,
779 mouse_sgr: false,
780 osc52_clipboard: false,
781 },
782 }
783 }
784
785 pub fn from_profile(profile: TerminalProfile) -> Self {
787 let mut caps = TerminalCapabilities::from_profile(profile);
788 caps.profile = TerminalProfile::Custom;
789 Self { caps }
790 }
791
792 #[must_use]
794 pub fn build(self) -> TerminalCapabilities {
795 self.caps
796 }
797
798 pub const fn true_color(mut self, enabled: bool) -> Self {
802 self.caps.true_color = enabled;
803 self
804 }
805
806 pub const fn colors_256(mut self, enabled: bool) -> Self {
808 self.caps.colors_256 = enabled;
809 self
810 }
811
812 pub const fn sync_output(mut self, enabled: bool) -> Self {
816 self.caps.sync_output = enabled;
817 self
818 }
819
820 pub const fn osc8_hyperlinks(mut self, enabled: bool) -> Self {
822 self.caps.osc8_hyperlinks = enabled;
823 self
824 }
825
826 pub const fn scroll_region(mut self, enabled: bool) -> Self {
828 self.caps.scroll_region = enabled;
829 self
830 }
831
832 pub const fn in_tmux(mut self, enabled: bool) -> Self {
836 self.caps.in_tmux = enabled;
837 self
838 }
839
840 pub const fn in_screen(mut self, enabled: bool) -> Self {
842 self.caps.in_screen = enabled;
843 self
844 }
845
846 pub const fn in_zellij(mut self, enabled: bool) -> Self {
848 self.caps.in_zellij = enabled;
849 self
850 }
851
852 pub const fn in_wezterm_mux(mut self, enabled: bool) -> Self {
854 self.caps.in_wezterm_mux = enabled;
855 self
856 }
857
858 pub const fn kitty_keyboard(mut self, enabled: bool) -> Self {
862 self.caps.kitty_keyboard = enabled;
863 self
864 }
865
866 pub const fn focus_events(mut self, enabled: bool) -> Self {
868 self.caps.focus_events = enabled;
869 self
870 }
871
872 pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
874 self.caps.bracketed_paste = enabled;
875 self
876 }
877
878 pub const fn mouse_sgr(mut self, enabled: bool) -> Self {
880 self.caps.mouse_sgr = enabled;
881 self
882 }
883
884 pub const fn osc52_clipboard(mut self, enabled: bool) -> Self {
888 self.caps.osc52_clipboard = enabled;
889 self
890 }
891}
892
893impl TerminalCapabilities {
894 #[must_use]
900 pub fn detect() -> Self {
901 let value = env::var("FTUI_TEST_PROFILE").ok();
902 Self::detect_with_test_profile_override(value.as_deref())
903 }
904
905 fn detect_with_test_profile_override(value: Option<&str>) -> Self {
906 if let Some(value) = value
907 && let Ok(profile) = TerminalProfile::from_str(value.trim())
908 && profile != TerminalProfile::Detected
909 {
910 return Self::from_profile(profile);
911 }
912 let env = DetectInputs::from_env();
913 Self::detect_from_inputs(&env)
914 }
915
916 fn detect_from_inputs(env: &DetectInputs) -> Self {
917 let in_tmux = env.in_tmux;
919 let in_screen = env.in_screen;
920 let in_zellij = env.in_zellij;
921 let term = env.term.as_str();
922 let term_program = env.term_program.as_str();
923 let colorterm = env.colorterm.as_str();
924 let term_lower = term.to_ascii_lowercase();
925 let term_program_lower = term_program.to_ascii_lowercase();
926 let colorterm_lower = colorterm.to_ascii_lowercase();
927
928 let term_program_is_wezterm = term_program_lower.contains("wezterm");
932 let term_is_wezterm = term_lower.contains("wezterm");
933 let in_wezterm_mux = term_program_is_wezterm
934 || term_is_wezterm
935 || env.wezterm_unix_socket
936 || env.wezterm_pane
937 || env.wezterm_executable;
938 let in_any_mux = in_tmux || in_screen || in_zellij || in_wezterm_mux;
939
940 let is_windows_terminal = env.wt_session;
942
943 let is_dumb = term == "dumb" || (term.is_empty() && !is_windows_terminal);
948
949 let is_kitty = env.kitty_window_id || term_lower.contains("kitty");
951
952 let is_modern_terminal = MODERN_TERMINALS.iter().any(|t| {
954 let t_lower = t.to_ascii_lowercase();
955 term_program_lower.contains(&t_lower) || term_lower.contains(&t_lower)
956 }) || is_windows_terminal;
957
958 let true_color = !env.no_color
960 && !is_dumb
961 && (colorterm_lower.contains("truecolor")
962 || colorterm_lower.contains("24bit")
963 || is_modern_terminal
964 || is_kitty);
965
966 let colors_256 = !env.no_color
968 && !is_dumb
969 && (true_color || term_lower.contains("256color") || term_lower.contains("256"));
970
971 let is_wezterm = term_program_is_wezterm || term_is_wezterm || env.wezterm_executable;
974
975 let sync_output = !is_dumb
977 && !is_wezterm
978 && (is_kitty
979 || SYNC_OUTPUT_TERMINALS.iter().any(|t| {
980 let t_lower = t.to_ascii_lowercase();
981 term_program_lower.contains(&t_lower)
982 }));
983
984 let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
986
987 let scroll_region = !is_dumb;
989
990 let kitty_keyboard = is_kitty
992 || KITTY_KEYBOARD_TERMINALS.iter().any(|t| {
993 let t_lower = t.to_ascii_lowercase();
994 term_program_lower.contains(&t_lower) || term_lower.contains(&t_lower)
995 });
996
997 let focus_events = !is_dumb && (is_modern_terminal || is_kitty);
999
1000 let bracketed_paste = !is_dumb;
1002
1003 let mouse_sgr = !is_dumb;
1005
1006 let osc52_clipboard = !is_dumb && !in_any_mux && (is_modern_terminal || is_kitty);
1008
1009 let unicode_box_drawing = !is_dumb;
1011 let unicode_emoji = !is_dumb && (is_modern_terminal || is_kitty);
1012 let double_width = !is_dumb;
1013
1014 Self {
1015 profile: TerminalProfile::Detected,
1016 true_color,
1017 colors_256,
1018 unicode_box_drawing,
1019 unicode_emoji,
1020 double_width,
1021 sync_output,
1022 osc8_hyperlinks,
1023 scroll_region,
1024 in_tmux,
1025 in_screen,
1026 in_zellij,
1027 in_wezterm_mux,
1028 kitty_keyboard,
1029 focus_events,
1030 bracketed_paste,
1031 mouse_sgr,
1032 osc52_clipboard,
1033 }
1034 }
1035
1036 #[must_use]
1041 pub const fn basic() -> Self {
1042 Self {
1043 profile: TerminalProfile::Dumb,
1044 true_color: false,
1045 colors_256: false,
1046 unicode_box_drawing: false,
1047 unicode_emoji: false,
1048 double_width: false,
1049 sync_output: false,
1050 osc8_hyperlinks: false,
1051 scroll_region: false,
1052 in_tmux: false,
1053 in_screen: false,
1054 in_zellij: false,
1055 in_wezterm_mux: false,
1056 kitty_keyboard: false,
1057 focus_events: false,
1058 bracketed_paste: false,
1059 mouse_sgr: false,
1060 osc52_clipboard: false,
1061 }
1062 }
1063
1064 #[must_use]
1068 #[inline]
1069 pub const fn in_any_mux(&self) -> bool {
1070 self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux
1071 }
1072
1073 #[must_use]
1075 #[inline]
1076 pub const fn has_color(&self) -> bool {
1077 self.true_color || self.colors_256
1078 }
1079
1080 #[must_use]
1082 pub const fn color_depth(&self) -> &'static str {
1083 if self.true_color {
1084 "truecolor"
1085 } else if self.colors_256 {
1086 "256"
1087 } else {
1088 "mono"
1089 }
1090 }
1091
1092 #[must_use]
1103 #[inline]
1104 pub const fn use_sync_output(&self) -> bool {
1105 if self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux {
1106 return false;
1107 }
1108 self.sync_output
1109 }
1110
1111 #[must_use]
1116 #[inline]
1117 pub const fn use_scroll_region(&self) -> bool {
1118 if self.in_any_mux() {
1119 return false;
1120 }
1121 self.scroll_region
1122 }
1123
1124 #[must_use]
1129 #[inline]
1130 pub const fn use_hyperlinks(&self) -> bool {
1131 if self.in_any_mux() {
1132 return false;
1133 }
1134 self.osc8_hyperlinks
1135 }
1136
1137 #[must_use]
1142 #[inline]
1143 pub const fn use_clipboard(&self) -> bool {
1144 if self.in_any_mux() {
1145 return false;
1146 }
1147 self.osc52_clipboard
1148 }
1149
1150 #[must_use]
1156 #[inline]
1157 pub const fn needs_passthrough_wrap(&self) -> bool {
1158 self.in_tmux || self.in_screen
1159 }
1160}
1161
1162pub struct SharedCapabilities {
1185 inner: crate::read_optimized::ArcSwapStore<TerminalCapabilities>,
1186}
1187
1188impl SharedCapabilities {
1189 pub fn new(caps: TerminalCapabilities) -> Self {
1191 Self {
1192 inner: crate::read_optimized::ArcSwapStore::new(caps),
1193 }
1194 }
1195
1196 pub fn detect() -> Self {
1198 Self::new(TerminalCapabilities::detect())
1199 }
1200
1201 #[inline]
1203 pub fn load(&self) -> TerminalCapabilities {
1204 crate::read_optimized::ReadOptimized::load(&self.inner)
1205 }
1206
1207 #[inline]
1209 pub fn store(&self, caps: TerminalCapabilities) {
1210 crate::read_optimized::ReadOptimized::store(&self.inner, caps);
1211 }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 use super::*;
1217
1218 fn detect_with_override(value: Option<&str>) -> TerminalCapabilities {
1219 TerminalCapabilities::detect_with_test_profile_override(value)
1220 }
1221
1222 #[test]
1223 fn basic_is_minimal() {
1224 let caps = TerminalCapabilities::basic();
1225 assert!(!caps.true_color);
1226 assert!(!caps.colors_256);
1227 assert!(!caps.sync_output);
1228 assert!(!caps.osc8_hyperlinks);
1229 assert!(!caps.scroll_region);
1230 assert!(!caps.in_tmux);
1231 assert!(!caps.in_screen);
1232 assert!(!caps.in_zellij);
1233 assert!(!caps.kitty_keyboard);
1234 assert!(!caps.focus_events);
1235 assert!(!caps.bracketed_paste);
1236 assert!(!caps.mouse_sgr);
1237 assert!(!caps.osc52_clipboard);
1238 }
1239
1240 #[test]
1241 fn basic_is_default() {
1242 let basic = TerminalCapabilities::basic();
1243 let default = TerminalCapabilities::default();
1244 assert_eq!(basic, default);
1245 }
1246
1247 #[test]
1248 fn in_any_mux_logic() {
1249 let mut caps = TerminalCapabilities::basic();
1250 assert!(!caps.in_any_mux());
1251
1252 caps.in_tmux = true;
1253 assert!(caps.in_any_mux());
1254
1255 caps.in_tmux = false;
1256 caps.in_screen = true;
1257 assert!(caps.in_any_mux());
1258
1259 caps.in_screen = false;
1260 caps.in_zellij = true;
1261 assert!(caps.in_any_mux());
1262
1263 caps.in_zellij = false;
1264 caps.in_wezterm_mux = true;
1265 assert!(caps.in_any_mux());
1266 }
1267
1268 #[test]
1269 fn has_color_logic() {
1270 let mut caps = TerminalCapabilities::basic();
1271 assert!(!caps.has_color());
1272
1273 caps.colors_256 = true;
1274 assert!(caps.has_color());
1275
1276 caps.colors_256 = false;
1277 caps.true_color = true;
1278 assert!(caps.has_color());
1279 }
1280
1281 #[test]
1282 fn color_depth_strings() {
1283 let mut caps = TerminalCapabilities::basic();
1284 assert_eq!(caps.color_depth(), "mono");
1285
1286 caps.colors_256 = true;
1287 assert_eq!(caps.color_depth(), "256");
1288
1289 caps.true_color = true;
1290 assert_eq!(caps.color_depth(), "truecolor");
1291 }
1292
1293 #[test]
1294 fn detect_does_not_panic() {
1295 let _caps = TerminalCapabilities::detect();
1297 }
1298
1299 #[test]
1300 fn windows_terminal_not_dumb_when_term_missing() {
1301 let env = DetectInputs {
1302 no_color: false,
1303 term: String::new(),
1304 term_program: String::new(),
1305 colorterm: String::new(),
1306 in_tmux: false,
1307 in_screen: false,
1308 in_zellij: false,
1309 wezterm_unix_socket: false,
1310 wezterm_pane: false,
1311 wezterm_executable: false,
1312 kitty_window_id: false,
1313 wt_session: true,
1314 };
1315
1316 let caps = TerminalCapabilities::detect_from_inputs(&env);
1317 assert!(caps.true_color, "WT_SESSION implies true color by default");
1318 assert!(caps.colors_256, "truecolor implies 256-color");
1319 assert!(
1320 caps.osc8_hyperlinks,
1321 "WT_SESSION implies OSC 8 hyperlink support by default"
1322 );
1323 assert!(
1324 caps.bracketed_paste,
1325 "WT_SESSION should not be treated as dumb"
1326 );
1327 assert!(caps.mouse_sgr, "WT_SESSION should not be treated as dumb");
1328 }
1329
1330 #[test]
1331 #[cfg(target_os = "windows")]
1332 fn detect_windows_terminal_from_wt_session() {
1333 let mut env = make_env("", "", "");
1334 env.wt_session = true;
1335 let caps = TerminalCapabilities::detect_from_inputs(&env);
1336 assert!(caps.true_color, "WT_SESSION implies true color");
1337 assert!(caps.colors_256, "WT_SESSION implies 256-color");
1338 assert!(caps.osc8_hyperlinks, "WT_SESSION implies OSC 8 support");
1339 }
1340
1341 #[test]
1342 fn no_color_disables_color_and_links() {
1343 let env = DetectInputs {
1344 no_color: true,
1345 term: "xterm-256color".to_string(),
1346 term_program: "WezTerm".to_string(),
1347 colorterm: "truecolor".to_string(),
1348 in_tmux: false,
1349 in_screen: false,
1350 in_zellij: false,
1351 wezterm_unix_socket: false,
1352 wezterm_pane: false,
1353 wezterm_executable: false,
1354 kitty_window_id: false,
1355 wt_session: false,
1356 };
1357
1358 let caps = TerminalCapabilities::detect_from_inputs(&env);
1359 assert!(!caps.true_color, "NO_COLOR must disable true color");
1360 assert!(!caps.colors_256, "NO_COLOR must disable 256-color");
1361 assert!(
1362 !caps.osc8_hyperlinks,
1363 "NO_COLOR must disable OSC 8 hyperlinks"
1364 );
1365 }
1366
1367 #[test]
1370 fn use_sync_output_disabled_in_tmux() {
1371 let mut caps = TerminalCapabilities::basic();
1372 caps.sync_output = true;
1373 assert!(caps.use_sync_output());
1374
1375 caps.in_tmux = true;
1376 assert!(!caps.use_sync_output());
1377 }
1378
1379 #[test]
1380 fn use_sync_output_disabled_in_screen() {
1381 let mut caps = TerminalCapabilities::basic();
1382 caps.sync_output = true;
1383 caps.in_screen = true;
1384 assert!(!caps.use_sync_output());
1385 }
1386
1387 #[test]
1388 fn use_sync_output_disabled_in_zellij() {
1389 let mut caps = TerminalCapabilities::basic();
1390 caps.sync_output = true;
1391 caps.in_zellij = true;
1392 assert!(!caps.use_sync_output());
1393 }
1394
1395 #[test]
1396 fn use_sync_output_disabled_in_wezterm_mux() {
1397 let mut caps = TerminalCapabilities::basic();
1398 caps.sync_output = true;
1399 caps.in_wezterm_mux = true;
1400 assert!(!caps.use_sync_output());
1401 }
1402
1403 #[test]
1404 fn use_scroll_region_disabled_in_mux() {
1405 let mut caps = TerminalCapabilities::basic();
1406 caps.scroll_region = true;
1407 assert!(caps.use_scroll_region());
1408
1409 caps.in_tmux = true;
1410 assert!(!caps.use_scroll_region());
1411
1412 caps.in_tmux = false;
1413 caps.in_screen = true;
1414 assert!(!caps.use_scroll_region());
1415
1416 caps.in_screen = false;
1417 caps.in_zellij = true;
1418 assert!(!caps.use_scroll_region());
1419
1420 caps.in_zellij = false;
1421 caps.in_wezterm_mux = true;
1422 assert!(!caps.use_scroll_region());
1423 }
1424
1425 #[test]
1426 fn use_hyperlinks_disabled_in_mux() {
1427 let mut caps = TerminalCapabilities::basic();
1428 caps.osc8_hyperlinks = true;
1429 assert!(caps.use_hyperlinks());
1430
1431 caps.in_tmux = true;
1432 assert!(!caps.use_hyperlinks());
1433
1434 caps.in_tmux = false;
1435 caps.in_wezterm_mux = true;
1436 assert!(!caps.use_hyperlinks());
1437 }
1438
1439 #[test]
1440 fn use_clipboard_disabled_in_mux() {
1441 let mut caps = TerminalCapabilities::basic();
1442 caps.osc52_clipboard = true;
1443 assert!(caps.use_clipboard());
1444
1445 caps.in_screen = true;
1446 assert!(!caps.use_clipboard());
1447
1448 caps.in_screen = false;
1449 caps.in_wezterm_mux = true;
1450 assert!(!caps.use_clipboard());
1451 }
1452
1453 #[test]
1454 fn needs_passthrough_wrap_only_for_tmux_screen() {
1455 let mut caps = TerminalCapabilities::basic();
1456 assert!(!caps.needs_passthrough_wrap());
1457
1458 caps.in_tmux = true;
1459 assert!(caps.needs_passthrough_wrap());
1460
1461 caps.in_tmux = false;
1462 caps.in_screen = true;
1463 assert!(caps.needs_passthrough_wrap());
1464
1465 caps.in_screen = false;
1467 caps.in_zellij = true;
1468 assert!(!caps.needs_passthrough_wrap());
1469 }
1470
1471 #[test]
1472 fn policies_return_false_when_capability_absent() {
1473 let caps = TerminalCapabilities::basic();
1475 assert!(!caps.use_sync_output());
1476 assert!(!caps.use_scroll_region());
1477 assert!(!caps.use_hyperlinks());
1478 assert!(!caps.use_clipboard());
1479 }
1480
1481 fn make_env(term: &str, term_program: &str, colorterm: &str) -> DetectInputs {
1484 DetectInputs {
1485 no_color: false,
1486 term: term.to_string(),
1487 term_program: term_program.to_string(),
1488 colorterm: colorterm.to_string(),
1489 in_tmux: false,
1490 in_screen: false,
1491 in_zellij: false,
1492 wezterm_unix_socket: false,
1493 wezterm_pane: false,
1494 wezterm_executable: false,
1495 kitty_window_id: false,
1496 wt_session: false,
1497 }
1498 }
1499
1500 #[test]
1501 fn detect_dumb_terminal() {
1502 let env = make_env("dumb", "", "");
1503 let caps = TerminalCapabilities::detect_from_inputs(&env);
1504 assert!(!caps.true_color);
1505 assert!(!caps.colors_256);
1506 assert!(!caps.sync_output);
1507 assert!(!caps.osc8_hyperlinks);
1508 assert!(!caps.scroll_region);
1509 assert!(!caps.focus_events);
1510 assert!(!caps.bracketed_paste);
1511 assert!(!caps.mouse_sgr);
1512 }
1513
1514 #[test]
1515 fn detect_dumb_overrides_truecolor_env() {
1516 let env = make_env("dumb", "WezTerm", "truecolor");
1517 let caps = TerminalCapabilities::detect_from_inputs(&env);
1518 assert!(!caps.true_color, "dumb should override COLORTERM");
1519 assert!(!caps.colors_256);
1520 assert!(!caps.bracketed_paste);
1521 assert!(!caps.mouse_sgr);
1522 assert!(!caps.osc8_hyperlinks);
1523 }
1524
1525 #[test]
1526 fn detect_empty_term_is_dumb() {
1527 let env = make_env("", "", "");
1528 let caps = TerminalCapabilities::detect_from_inputs(&env);
1529 assert!(!caps.true_color);
1530 assert!(!caps.bracketed_paste);
1531 }
1532
1533 #[test]
1534 fn detect_xterm_256color() {
1535 let env = make_env("xterm-256color", "", "");
1536 let caps = TerminalCapabilities::detect_from_inputs(&env);
1537 assert!(caps.colors_256, "xterm-256color implies 256 color");
1538 assert!(!caps.true_color, "256color alone does not imply truecolor");
1539 assert!(caps.bracketed_paste);
1540 assert!(caps.mouse_sgr);
1541 assert!(caps.scroll_region);
1542 }
1543
1544 #[test]
1545 fn detect_colorterm_truecolor() {
1546 let env = make_env("xterm-256color", "", "truecolor");
1547 let caps = TerminalCapabilities::detect_from_inputs(&env);
1548 assert!(caps.true_color, "COLORTERM=truecolor enables truecolor");
1549 assert!(caps.colors_256, "truecolor implies 256-color");
1550 }
1551
1552 #[test]
1553 fn detect_colorterm_24bit() {
1554 let env = make_env("xterm-256color", "", "24bit");
1555 let caps = TerminalCapabilities::detect_from_inputs(&env);
1556 assert!(caps.true_color, "COLORTERM=24bit enables truecolor");
1557 }
1558
1559 #[test]
1560 fn detect_kitty_by_window_id() {
1561 let mut env = make_env("xterm-kitty", "", "");
1562 env.kitty_window_id = true;
1563 let caps = TerminalCapabilities::detect_from_inputs(&env);
1564 assert!(caps.true_color, "Kitty supports truecolor");
1565 assert!(
1566 caps.kitty_keyboard,
1567 "Kitty supports kitty keyboard protocol"
1568 );
1569 assert!(caps.sync_output, "Kitty supports sync output");
1570 }
1571
1572 #[test]
1573 fn detect_kitty_by_term() {
1574 let env = make_env("xterm-kitty", "", "");
1575 let caps = TerminalCapabilities::detect_from_inputs(&env);
1576 assert!(caps.true_color, "kitty TERM implies truecolor");
1577 assert!(caps.kitty_keyboard);
1578 }
1579
1580 #[test]
1581 fn detect_wezterm() {
1582 let env = make_env("xterm-256color", "WezTerm", "truecolor");
1583 let caps = TerminalCapabilities::detect_from_inputs(&env);
1584 assert!(caps.true_color);
1585 assert!(
1586 caps.in_wezterm_mux,
1587 "WezTerm identity is treated as conservative mux evidence"
1588 );
1589 assert!(caps.in_any_mux());
1590 assert!(
1591 !caps.sync_output,
1592 "WezTerm sync output is hard-disabled as a safety fallback"
1593 );
1594 assert!(caps.osc8_hyperlinks, "WezTerm supports hyperlinks");
1595 assert!(caps.kitty_keyboard, "WezTerm supports kitty keyboard");
1596 assert!(caps.focus_events);
1597 assert!(
1598 !caps.osc52_clipboard,
1599 "conservative mux policy should disable raw OSC52 detection"
1600 );
1601 assert!(!caps.use_scroll_region());
1602 assert!(!caps.use_hyperlinks());
1603 assert!(!caps.use_clipboard());
1604 }
1605
1606 #[test]
1607 fn detect_wezterm_mux_socket_disables_sync_policy() {
1608 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1609 env.wezterm_unix_socket = true;
1610 let caps = TerminalCapabilities::detect_from_inputs(&env);
1611 assert!(!caps.sync_output);
1612 assert!(caps.in_wezterm_mux, "wezterm mux marker should be detected");
1613 assert!(
1614 caps.in_any_mux(),
1615 "wezterm mux must participate in in_any_mux()"
1616 );
1617 assert!(
1618 !caps.use_sync_output(),
1619 "policy must suppress sync output in wezterm mux sessions"
1620 );
1621 assert!(
1622 !caps.use_scroll_region(),
1623 "policy must suppress scroll region in wezterm mux sessions"
1624 );
1625 assert!(
1626 !caps.use_hyperlinks(),
1627 "policy must suppress hyperlinks in wezterm mux sessions"
1628 );
1629 assert!(
1630 !caps.use_clipboard(),
1631 "policy must suppress clipboard in wezterm mux sessions"
1632 );
1633 }
1634
1635 #[test]
1636 fn detect_wezterm_mux_socket_without_term_program_disables_sync_policy() {
1637 let mut env = make_env("xterm-256color", "", "truecolor");
1638 env.wezterm_unix_socket = true;
1639 let caps = TerminalCapabilities::detect_from_inputs(&env);
1640 assert!(
1641 caps.in_wezterm_mux,
1642 "socket marker alone must detect wezterm mux"
1643 );
1644 assert!(
1645 caps.in_any_mux(),
1646 "wezterm mux must participate in in_any_mux()"
1647 );
1648 assert!(
1649 !caps.use_sync_output(),
1650 "policy must suppress sync output when wezterm mux socket is present"
1651 );
1652 }
1653
1654 #[test]
1655 fn detect_wezterm_mux_pane_disables_sync_policy() {
1656 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1657 env.wezterm_pane = true;
1658 let caps = TerminalCapabilities::detect_from_inputs(&env);
1659 assert!(!caps.sync_output);
1660 assert!(
1661 caps.in_wezterm_mux,
1662 "wezterm pane marker should be detected"
1663 );
1664 assert!(
1665 caps.in_any_mux(),
1666 "wezterm mux must participate in in_any_mux()"
1667 );
1668 assert!(
1669 !caps.use_sync_output(),
1670 "policy must suppress sync output when wezterm pane marker is present"
1671 );
1672 }
1673
1674 #[test]
1675 fn detect_wezterm_mux_pane_without_term_program_disables_sync_policy() {
1676 let mut env = make_env("xterm-256color", "", "truecolor");
1677 env.wezterm_pane = true;
1678 let caps = TerminalCapabilities::detect_from_inputs(&env);
1679 assert!(
1680 caps.in_wezterm_mux,
1681 "pane marker alone must detect wezterm mux"
1682 );
1683 assert!(
1684 caps.in_any_mux(),
1685 "wezterm mux must participate in in_any_mux()"
1686 );
1687 assert!(
1688 !caps.use_sync_output(),
1689 "policy must suppress sync output when wezterm pane marker is present"
1690 );
1691 }
1692
1693 #[test]
1694 fn detect_wezterm_executable_without_term_program_is_conservative_mux() {
1695 let mut env = make_env("xterm-256color", "", "truecolor");
1696 env.wezterm_executable = true;
1697 let caps = TerminalCapabilities::detect_from_inputs(&env);
1698 assert!(
1699 caps.in_wezterm_mux,
1700 "WEZTERM_EXECUTABLE fallback should conservatively mark mux context"
1701 );
1702 assert!(
1703 !caps.use_sync_output(),
1704 "fallback mux detection must suppress sync output policy"
1705 );
1706 }
1707
1708 #[test]
1709 fn detect_wezterm_executable_overrides_explicit_non_wezterm_program() {
1710 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1711 env.wezterm_executable = true;
1712 let caps = TerminalCapabilities::detect_from_inputs(&env);
1713 assert!(
1714 caps.in_wezterm_mux,
1715 "WEZTERM_EXECUTABLE should conservatively force wezterm mux policy"
1716 );
1717 assert!(
1718 !caps.sync_output,
1719 "raw sync_output capability should be disabled under conservative wezterm marker handling"
1720 );
1721 assert!(
1722 !caps.use_sync_output(),
1723 "mux policy should disable sync output under WEZTERM_EXECUTABLE marker"
1724 );
1725 }
1726
1727 #[test]
1728 fn detect_wezterm_socket_overrides_explicit_non_wezterm_program() {
1729 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1730 env.wezterm_unix_socket = true;
1731 let caps = TerminalCapabilities::detect_from_inputs(&env);
1732 assert!(
1733 caps.in_wezterm_mux,
1734 "WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
1735 );
1736 assert!(
1737 !caps.use_sync_output(),
1738 "mux policy should disable sync output with socket marker"
1739 );
1740 }
1741
1742 #[test]
1743 fn detect_wezterm_pane_overrides_explicit_non_wezterm_program() {
1744 let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1745 env.wezterm_pane = true;
1746 let caps = TerminalCapabilities::detect_from_inputs(&env);
1747 assert!(
1748 caps.in_wezterm_mux,
1749 "WEZTERM_PANE should conservatively force wezterm mux policy"
1750 );
1751 assert!(
1752 !caps.use_sync_output(),
1753 "mux policy should disable sync output with pane marker"
1754 );
1755 }
1756
1757 #[test]
1758 fn detect_wezterm_socket_overrides_explicit_non_wezterm_term_identity() {
1759 let mut env = make_env("xterm-ghostty", "", "truecolor");
1760 env.wezterm_unix_socket = true;
1761 let caps = TerminalCapabilities::detect_from_inputs(&env);
1762 assert!(
1763 caps.in_wezterm_mux,
1764 "WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
1765 );
1766 assert!(
1767 !caps.use_sync_output(),
1768 "mux policy should disable sync output with socket marker"
1769 );
1770 }
1771
1772 #[test]
1773 #[cfg(target_os = "macos")]
1774 fn detect_iterm2_from_term_program() {
1775 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1776 let caps = TerminalCapabilities::detect_from_inputs(&env);
1777 assert!(caps.true_color, "iTerm2 implies truecolor");
1778 assert!(caps.osc8_hyperlinks, "iTerm2 supports OSC 8 hyperlinks");
1779 }
1780
1781 #[test]
1782 fn detect_alacritty() {
1783 let env = make_env("alacritty", "Alacritty", "truecolor");
1784 let caps = TerminalCapabilities::detect_from_inputs(&env);
1785 assert!(caps.true_color);
1786 assert!(caps.sync_output);
1787 assert!(caps.osc8_hyperlinks);
1788 assert!(caps.kitty_keyboard);
1789 assert!(caps.focus_events);
1790 }
1791
1792 #[test]
1793 fn detect_ghostty() {
1794 let env = make_env("xterm-ghostty", "Ghostty", "truecolor");
1795 let caps = TerminalCapabilities::detect_from_inputs(&env);
1796 assert!(caps.true_color);
1797 assert!(caps.sync_output);
1798 assert!(caps.osc8_hyperlinks);
1799 assert!(caps.kitty_keyboard);
1800 assert!(caps.focus_events);
1801 }
1802
1803 #[test]
1804 fn detect_iterm() {
1805 let env = make_env("xterm-256color", "iTerm.app", "truecolor");
1806 let caps = TerminalCapabilities::detect_from_inputs(&env);
1807 assert!(caps.true_color);
1808 assert!(caps.osc8_hyperlinks);
1809 assert!(caps.kitty_keyboard);
1810 assert!(caps.focus_events);
1811 }
1812
1813 #[test]
1814 fn detect_vscode_terminal() {
1815 let env = make_env("xterm-256color", "vscode", "truecolor");
1816 let caps = TerminalCapabilities::detect_from_inputs(&env);
1817 assert!(caps.true_color);
1818 assert!(caps.osc8_hyperlinks);
1819 assert!(caps.focus_events);
1820 }
1821
1822 #[test]
1825 fn detect_in_tmux() {
1826 let mut env = make_env("screen-256color", "", "");
1827 env.in_tmux = true;
1828 let caps = TerminalCapabilities::detect_from_inputs(&env);
1829 assert!(caps.in_tmux);
1830 assert!(caps.in_any_mux());
1831 assert!(caps.colors_256);
1832 assert!(!caps.osc52_clipboard, "clipboard disabled in tmux");
1833 }
1834
1835 #[test]
1836 fn detect_in_screen() {
1837 let mut env = make_env("screen", "", "");
1838 env.in_screen = true;
1839 let caps = TerminalCapabilities::detect_from_inputs(&env);
1840 assert!(caps.in_screen);
1841 assert!(caps.in_any_mux());
1842 assert!(caps.needs_passthrough_wrap());
1843 }
1844
1845 #[test]
1846 fn detect_in_zellij() {
1847 let mut env = make_env("xterm-256color", "", "truecolor");
1848 env.in_zellij = true;
1849 let caps = TerminalCapabilities::detect_from_inputs(&env);
1850 assert!(caps.in_zellij);
1851 assert!(caps.in_any_mux());
1852 assert!(
1853 !caps.needs_passthrough_wrap(),
1854 "Zellij handles passthrough natively"
1855 );
1856 assert!(!caps.osc52_clipboard, "clipboard disabled in mux");
1857 }
1858
1859 #[test]
1860 fn detect_modern_terminal_in_tmux() {
1861 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
1862 env.in_tmux = true;
1863 let caps = TerminalCapabilities::detect_from_inputs(&env);
1864 assert!(caps.true_color);
1866 assert!(!caps.sync_output);
1867 assert!(!caps.use_sync_output());
1869 assert!(!caps.use_hyperlinks());
1870 assert!(!caps.use_scroll_region());
1871 }
1872
1873 #[test]
1876 fn no_color_overrides_everything() {
1877 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
1878 env.no_color = true;
1879 let caps = TerminalCapabilities::detect_from_inputs(&env);
1880 assert!(!caps.true_color);
1881 assert!(!caps.colors_256);
1882 assert!(!caps.osc8_hyperlinks);
1883 assert!(!caps.sync_output);
1885 assert!(caps.bracketed_paste);
1886 assert!(caps.mouse_sgr);
1887 }
1888
1889 #[test]
1892 fn unknown_term_program() {
1893 let env = make_env("xterm", "SomeUnknownTerminal", "");
1894 let caps = TerminalCapabilities::detect_from_inputs(&env);
1895 assert!(
1896 !caps.true_color,
1897 "unknown terminal should not assume truecolor"
1898 );
1899 assert!(!caps.osc8_hyperlinks);
1900 assert!(caps.bracketed_paste);
1902 assert!(caps.mouse_sgr);
1903 assert!(caps.scroll_region);
1904 }
1905
1906 #[test]
1907 fn all_mux_flags_simultaneous() {
1908 let mut env = make_env("screen", "", "");
1909 env.in_tmux = true;
1910 env.in_screen = true;
1911 env.in_zellij = true;
1912 let caps = TerminalCapabilities::detect_from_inputs(&env);
1913 assert!(caps.in_any_mux());
1914 assert!(caps.needs_passthrough_wrap());
1915 assert!(!caps.use_sync_output());
1916 assert!(!caps.use_hyperlinks());
1917 assert!(!caps.use_clipboard());
1918 }
1919
1920 #[test]
1923 fn detect_rio() {
1924 let env = make_env("xterm-256color", "Rio", "truecolor");
1925 let caps = TerminalCapabilities::detect_from_inputs(&env);
1926 assert!(caps.true_color);
1927 assert!(caps.osc8_hyperlinks);
1928 assert!(caps.kitty_keyboard);
1929 assert!(caps.focus_events);
1930 }
1931
1932 #[test]
1933 fn detect_contour() {
1934 let env = make_env("xterm-256color", "Contour", "truecolor");
1935 let caps = TerminalCapabilities::detect_from_inputs(&env);
1936 assert!(caps.true_color);
1937 assert!(caps.sync_output);
1938 assert!(caps.osc8_hyperlinks);
1939 assert!(caps.focus_events);
1940 }
1941
1942 #[test]
1943 fn detect_foot() {
1944 let env = make_env("foot", "foot", "truecolor");
1945 let caps = TerminalCapabilities::detect_from_inputs(&env);
1946 assert!(caps.kitty_keyboard, "foot supports kitty keyboard");
1947 }
1948
1949 #[test]
1950 fn detect_hyper() {
1951 let env = make_env("xterm-256color", "Hyper", "truecolor");
1952 let caps = TerminalCapabilities::detect_from_inputs(&env);
1953 assert!(caps.true_color);
1954 assert!(caps.osc8_hyperlinks);
1955 assert!(caps.focus_events);
1956 }
1957
1958 #[test]
1959 fn detect_linux_console() {
1960 let env = make_env("linux", "", "");
1961 let caps = TerminalCapabilities::detect_from_inputs(&env);
1962 assert!(!caps.true_color, "linux console doesn't support truecolor");
1963 assert!(!caps.colors_256, "linux console doesn't support 256 colors");
1964 assert!(caps.bracketed_paste);
1966 assert!(caps.mouse_sgr);
1967 assert!(caps.scroll_region);
1968 }
1969
1970 #[test]
1971 fn detect_xterm_direct() {
1972 let env = make_env("xterm", "", "");
1973 let caps = TerminalCapabilities::detect_from_inputs(&env);
1974 assert!(!caps.true_color, "plain xterm has no truecolor");
1975 assert!(!caps.colors_256, "plain xterm has no 256color");
1976 assert!(caps.bracketed_paste);
1977 assert!(caps.mouse_sgr);
1978 }
1979
1980 #[test]
1981 fn detect_screen_256color() {
1982 let env = make_env("screen-256color", "", "");
1983 let caps = TerminalCapabilities::detect_from_inputs(&env);
1984 assert!(caps.colors_256, "screen-256color has 256 colors");
1985 assert!(!caps.true_color);
1986 }
1987
1988 #[test]
1991 fn wezterm_without_colorterm() {
1992 let env = make_env("xterm-256color", "WezTerm", "");
1993 let caps = TerminalCapabilities::detect_from_inputs(&env);
1994 assert!(caps.true_color, "WezTerm is modern, implies truecolor");
1996 assert!(!caps.sync_output);
1997 assert!(caps.osc8_hyperlinks);
1998 }
1999
2000 #[test]
2001 fn alacritty_via_term_only() {
2002 let env = make_env("alacritty", "", "");
2004 let caps = TerminalCapabilities::detect_from_inputs(&env);
2005 assert!(caps.true_color);
2007 assert!(caps.osc8_hyperlinks);
2008 }
2009
2010 #[test]
2013 fn kitty_via_term_without_window_id() {
2014 let env = make_env("xterm-kitty", "", "");
2015 let caps = TerminalCapabilities::detect_from_inputs(&env);
2016 assert!(caps.kitty_keyboard);
2017 assert!(caps.true_color);
2018 assert!(caps.sync_output);
2019 }
2020
2021 #[test]
2022 fn kitty_window_id_with_generic_term() {
2023 let mut env = make_env("xterm-256color", "", "");
2024 env.kitty_window_id = true;
2025 let caps = TerminalCapabilities::detect_from_inputs(&env);
2026 assert!(caps.kitty_keyboard);
2027 assert!(caps.true_color);
2028 }
2029
2030 #[test]
2033 fn use_clipboard_enabled_when_no_mux_and_modern() {
2034 let env = make_env("xterm-256color", "Alacritty", "truecolor");
2035 let caps = TerminalCapabilities::detect_from_inputs(&env);
2036 assert!(caps.osc52_clipboard);
2037 assert!(caps.use_clipboard());
2038 }
2039
2040 #[test]
2041 fn use_clipboard_disabled_in_tmux_even_if_detected() {
2042 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2043 env.in_tmux = true;
2044 let caps = TerminalCapabilities::detect_from_inputs(&env);
2045 assert!(!caps.osc52_clipboard);
2047 assert!(!caps.use_clipboard());
2048 }
2049
2050 #[test]
2051 fn scroll_region_enabled_for_basic_xterm() {
2052 let env = make_env("xterm", "", "");
2053 let caps = TerminalCapabilities::detect_from_inputs(&env);
2054 assert!(caps.scroll_region);
2055 assert!(caps.use_scroll_region());
2056 }
2057
2058 #[test]
2059 fn no_color_preserves_non_visual_features() {
2060 let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
2061 env.no_color = true;
2062 let caps = TerminalCapabilities::detect_from_inputs(&env);
2063 assert!(!caps.true_color);
2065 assert!(!caps.colors_256);
2066 assert!(!caps.osc8_hyperlinks);
2067 assert!(!caps.sync_output);
2069 assert!(caps.kitty_keyboard);
2070 assert!(caps.focus_events);
2071 assert!(caps.bracketed_paste);
2072 assert!(caps.mouse_sgr);
2073 }
2074
2075 #[test]
2078 fn colorterm_yes_not_truecolor() {
2079 let env = make_env("xterm-256color", "", "yes");
2080 let caps = TerminalCapabilities::detect_from_inputs(&env);
2081 assert!(!caps.true_color, "COLORTERM=yes is not truecolor");
2082 assert!(caps.colors_256, "TERM=xterm-256color implies 256");
2083 }
2084
2085 #[test]
2088 fn profile_enum_as_str() {
2089 assert_eq!(TerminalProfile::Modern.as_str(), "modern");
2090 assert_eq!(TerminalProfile::Xterm256Color.as_str(), "xterm-256color");
2091 assert_eq!(TerminalProfile::Vt100.as_str(), "vt100");
2092 assert_eq!(TerminalProfile::Dumb.as_str(), "dumb");
2093 assert_eq!(TerminalProfile::Tmux.as_str(), "tmux");
2094 assert_eq!(TerminalProfile::Screen.as_str(), "screen");
2095 assert_eq!(TerminalProfile::Kitty.as_str(), "kitty");
2096 }
2097
2098 #[test]
2099 fn profile_enum_from_str() {
2100 use std::str::FromStr;
2101 assert_eq!(
2102 TerminalProfile::from_str("modern"),
2103 Ok(TerminalProfile::Modern)
2104 );
2105 assert_eq!(
2106 TerminalProfile::from_str("xterm-256color"),
2107 Ok(TerminalProfile::Xterm256Color)
2108 );
2109 assert_eq!(
2110 TerminalProfile::from_str("xterm256color"),
2111 Ok(TerminalProfile::Xterm256Color)
2112 );
2113 assert_eq!(TerminalProfile::from_str("DUMB"), Ok(TerminalProfile::Dumb));
2114 assert!(TerminalProfile::from_str("unknown").is_err());
2115 }
2116
2117 #[test]
2118 fn profile_all_predefined() {
2119 let all = TerminalProfile::all_predefined();
2120 assert!(all.len() >= 10);
2121 assert!(all.contains(&TerminalProfile::Modern));
2122 assert!(all.contains(&TerminalProfile::Dumb));
2123 assert!(!all.contains(&TerminalProfile::Custom));
2124 assert!(!all.contains(&TerminalProfile::Detected));
2125 }
2126
2127 #[test]
2128 fn profile_modern_has_all_features() {
2129 let caps = TerminalCapabilities::modern();
2130 assert_eq!(caps.profile(), TerminalProfile::Modern);
2131 assert_eq!(caps.profile_name(), Some("modern"));
2132 assert!(caps.true_color);
2133 assert!(caps.colors_256);
2134 assert!(caps.sync_output);
2135 assert!(caps.osc8_hyperlinks);
2136 assert!(caps.scroll_region);
2137 assert!(caps.kitty_keyboard);
2138 assert!(caps.focus_events);
2139 assert!(caps.bracketed_paste);
2140 assert!(caps.mouse_sgr);
2141 assert!(caps.osc52_clipboard);
2142 assert!(!caps.in_any_mux());
2143 }
2144
2145 #[test]
2146 fn profile_xterm_256color() {
2147 let caps = TerminalCapabilities::xterm_256color();
2148 assert_eq!(caps.profile(), TerminalProfile::Xterm256Color);
2149 assert!(!caps.true_color);
2150 assert!(caps.colors_256);
2151 assert!(!caps.sync_output);
2152 assert!(!caps.osc8_hyperlinks);
2153 assert!(caps.scroll_region);
2154 assert!(caps.bracketed_paste);
2155 assert!(caps.mouse_sgr);
2156 }
2157
2158 #[test]
2159 fn profile_xterm_basic() {
2160 let caps = TerminalCapabilities::xterm();
2161 assert_eq!(caps.profile(), TerminalProfile::Xterm);
2162 assert!(!caps.true_color);
2163 assert!(!caps.colors_256);
2164 assert!(caps.scroll_region);
2165 }
2166
2167 #[test]
2168 fn profile_vt100_minimal() {
2169 let caps = TerminalCapabilities::vt100();
2170 assert_eq!(caps.profile(), TerminalProfile::Vt100);
2171 assert!(!caps.true_color);
2172 assert!(!caps.colors_256);
2173 assert!(caps.scroll_region);
2174 assert!(!caps.bracketed_paste);
2175 assert!(!caps.mouse_sgr);
2176 }
2177
2178 #[test]
2179 fn profile_dumb_no_features() {
2180 let caps = TerminalCapabilities::dumb();
2181 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2182 assert!(!caps.true_color);
2183 assert!(!caps.colors_256);
2184 assert!(!caps.scroll_region);
2185 assert!(!caps.bracketed_paste);
2186 assert!(!caps.mouse_sgr);
2187 assert!(!caps.use_sync_output());
2188 assert!(!caps.use_scroll_region());
2189 }
2190
2191 #[test]
2192 fn profile_tmux_mux_flags() {
2193 let caps = TerminalCapabilities::tmux();
2194 assert_eq!(caps.profile(), TerminalProfile::Tmux);
2195 assert!(caps.in_tmux);
2196 assert!(!caps.in_screen);
2197 assert!(!caps.in_zellij);
2198 assert!(caps.in_any_mux());
2199 assert!(!caps.use_sync_output());
2201 assert!(!caps.use_scroll_region());
2202 assert!(!caps.use_hyperlinks());
2203 }
2204
2205 #[test]
2206 fn profile_screen_mux_flags() {
2207 let caps = TerminalCapabilities::screen();
2208 assert_eq!(caps.profile(), TerminalProfile::Screen);
2209 assert!(!caps.in_tmux);
2210 assert!(caps.in_screen);
2211 assert!(caps.in_any_mux());
2212 assert!(caps.needs_passthrough_wrap());
2213 }
2214
2215 #[test]
2216 fn profile_zellij_mux_flags() {
2217 let caps = TerminalCapabilities::zellij();
2218 assert_eq!(caps.profile(), TerminalProfile::Zellij);
2219 assert!(caps.in_zellij);
2220 assert!(caps.in_any_mux());
2221 assert!(caps.true_color);
2223 assert!(caps.focus_events);
2224 assert!(!caps.needs_passthrough_wrap());
2226 }
2227
2228 #[test]
2229 fn profile_kitty_full_features() {
2230 let caps = TerminalCapabilities::kitty();
2231 assert_eq!(caps.profile(), TerminalProfile::Kitty);
2232 assert!(caps.true_color);
2233 assert!(caps.sync_output);
2234 assert!(caps.kitty_keyboard);
2235 assert!(caps.osc8_hyperlinks);
2236 }
2237
2238 #[test]
2239 fn profile_windows_console() {
2240 let caps = TerminalCapabilities::windows_console();
2241 assert_eq!(caps.profile(), TerminalProfile::WindowsConsole);
2242 assert!(caps.true_color);
2243 assert!(caps.osc8_hyperlinks);
2244 assert!(caps.focus_events);
2245 }
2246
2247 #[test]
2248 fn profile_linux_console() {
2249 let caps = TerminalCapabilities::linux_console();
2250 assert_eq!(caps.profile(), TerminalProfile::LinuxConsole);
2251 assert!(!caps.true_color);
2252 assert!(!caps.colors_256);
2253 assert!(caps.scroll_region);
2254 }
2255
2256 #[test]
2257 fn from_profile_roundtrip() {
2258 for profile in TerminalProfile::all_predefined() {
2259 let caps = TerminalCapabilities::from_profile(*profile);
2260 assert_eq!(caps.profile(), *profile);
2261 }
2262 }
2263
2264 #[test]
2265 fn detected_profile_has_none_name() {
2266 let caps = detect_with_override(None);
2267 assert_eq!(caps.profile(), TerminalProfile::Detected);
2268 assert_eq!(caps.profile_name(), None);
2269 }
2270
2271 #[test]
2272 fn detect_respects_test_profile_env() {
2273 let caps = detect_with_override(Some("dumb"));
2274 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2275 }
2276
2277 #[test]
2278 fn detect_ignores_invalid_test_profile() {
2279 let caps = detect_with_override(Some("not-a-real-profile"));
2280 assert_eq!(caps.profile(), TerminalProfile::Detected);
2281 }
2282
2283 #[test]
2284 fn basic_has_dumb_profile() {
2285 let caps = TerminalCapabilities::basic();
2286 assert_eq!(caps.profile(), TerminalProfile::Dumb);
2287 }
2288
2289 #[test]
2292 fn builder_starts_empty() {
2293 let caps = CapabilityProfileBuilder::new().build();
2294 assert_eq!(caps.profile(), TerminalProfile::Custom);
2295 assert!(!caps.true_color);
2296 assert!(!caps.colors_256);
2297 assert!(!caps.sync_output);
2298 assert!(!caps.scroll_region);
2299 assert!(!caps.mouse_sgr);
2300 }
2301
2302 #[test]
2303 fn builder_set_colors() {
2304 let caps = CapabilityProfileBuilder::new()
2305 .true_color(true)
2306 .colors_256(true)
2307 .build();
2308 assert!(caps.true_color);
2309 assert!(caps.colors_256);
2310 }
2311
2312 #[test]
2313 fn builder_set_advanced() {
2314 let caps = CapabilityProfileBuilder::new()
2315 .sync_output(true)
2316 .osc8_hyperlinks(true)
2317 .scroll_region(true)
2318 .build();
2319 assert!(caps.sync_output);
2320 assert!(caps.osc8_hyperlinks);
2321 assert!(caps.scroll_region);
2322 }
2323
2324 #[test]
2325 fn builder_set_mux() {
2326 let caps = CapabilityProfileBuilder::new()
2327 .in_tmux(true)
2328 .in_screen(false)
2329 .in_zellij(false)
2330 .build();
2331 assert!(caps.in_tmux);
2332 assert!(!caps.in_screen);
2333 assert!(caps.in_any_mux());
2334 }
2335
2336 #[test]
2337 fn builder_set_input() {
2338 let caps = CapabilityProfileBuilder::new()
2339 .kitty_keyboard(true)
2340 .focus_events(true)
2341 .bracketed_paste(true)
2342 .mouse_sgr(true)
2343 .build();
2344 assert!(caps.kitty_keyboard);
2345 assert!(caps.focus_events);
2346 assert!(caps.bracketed_paste);
2347 assert!(caps.mouse_sgr);
2348 }
2349
2350 #[test]
2351 fn builder_set_clipboard() {
2352 let caps = CapabilityProfileBuilder::new()
2353 .osc52_clipboard(true)
2354 .build();
2355 assert!(caps.osc52_clipboard);
2356 }
2357
2358 #[test]
2359 fn builder_from_profile() {
2360 let caps = CapabilityProfileBuilder::from_profile(TerminalProfile::Modern)
2361 .sync_output(false) .build();
2363 assert!(caps.true_color);
2365 assert!(caps.colors_256);
2366 assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
2368 assert_eq!(caps.profile(), TerminalProfile::Custom);
2370 }
2371
2372 #[test]
2373 fn builder_chain_multiple() {
2374 let caps = TerminalCapabilities::builder()
2375 .colors_256(true)
2376 .bracketed_paste(true)
2377 .mouse_sgr(true)
2378 .scroll_region(true)
2379 .build();
2380 assert!(caps.colors_256);
2381 assert!(caps.bracketed_paste);
2382 assert!(caps.mouse_sgr);
2383 assert!(caps.scroll_region);
2384 assert!(!caps.true_color);
2385 assert!(!caps.sync_output);
2386 }
2387
2388 #[test]
2389 fn builder_default() {
2390 let builder = CapabilityProfileBuilder::default();
2391 let caps = builder.build();
2392 assert_eq!(caps.profile(), TerminalProfile::Custom);
2393 }
2394
2395 #[test]
2404 fn mux_compatibility_matrix() {
2405 {
2410 let caps = TerminalCapabilities::modern();
2411 assert!(
2412 caps.use_sync_output(),
2413 "baseline: sync_output should be enabled"
2414 );
2415 assert!(
2416 caps.use_scroll_region(),
2417 "baseline: scroll_region should be enabled"
2418 );
2419 assert!(
2420 caps.use_hyperlinks(),
2421 "baseline: hyperlinks should be enabled"
2422 );
2423 assert!(!caps.needs_passthrough_wrap(), "baseline: no wrap needed");
2424 }
2425
2426 {
2428 let caps = TerminalCapabilities::tmux();
2429 assert!(!caps.use_sync_output(), "tmux: sync_output disabled");
2430 assert!(!caps.use_scroll_region(), "tmux: scroll_region disabled");
2431 assert!(!caps.use_hyperlinks(), "tmux: hyperlinks disabled");
2432 assert!(caps.needs_passthrough_wrap(), "tmux: needs wrap");
2433 }
2434
2435 {
2437 let caps = TerminalCapabilities::screen();
2438 assert!(!caps.use_sync_output(), "screen: sync_output disabled");
2439 assert!(!caps.use_scroll_region(), "screen: scroll_region disabled");
2440 assert!(!caps.use_hyperlinks(), "screen: hyperlinks disabled");
2441 assert!(caps.needs_passthrough_wrap(), "screen: needs wrap");
2442 }
2443
2444 {
2446 let caps = TerminalCapabilities::zellij();
2447 assert!(!caps.use_sync_output(), "zellij: sync_output disabled");
2448 assert!(!caps.use_scroll_region(), "zellij: scroll_region disabled");
2449 assert!(!caps.use_hyperlinks(), "zellij: hyperlinks disabled");
2450 assert!(
2451 !caps.needs_passthrough_wrap(),
2452 "zellij: no wrap needed (native passthrough)"
2453 );
2454 }
2455
2456 {
2458 let caps = TerminalCapabilities::builder()
2459 .in_wezterm_mux(true)
2460 .sync_output(true)
2461 .scroll_region(true)
2462 .osc8_hyperlinks(true)
2463 .build();
2464 assert!(!caps.use_sync_output(), "wezterm mux: sync_output disabled");
2465 assert!(
2466 !caps.use_scroll_region(),
2467 "wezterm mux: scroll_region disabled"
2468 );
2469 assert!(!caps.use_hyperlinks(), "wezterm mux: hyperlinks disabled");
2470 assert!(
2471 !caps.needs_passthrough_wrap(),
2472 "wezterm mux: no wrap needed"
2473 );
2474 }
2475 }
2476
2477 #[test]
2479 fn modern_terminal_in_mux_matrix() {
2480 for (mux_name, in_tmux, in_screen, in_zellij, in_wezterm_mux) in [
2484 ("tmux", true, false, false, false),
2485 ("screen", false, true, false, false),
2486 ("zellij", false, false, true, false),
2487 ("wezterm-mux", false, false, false, true),
2488 ] {
2489 let mut env = make_env("screen-256color", "WezTerm", "truecolor");
2490 env.in_tmux = in_tmux;
2491 env.in_screen = in_screen;
2492 env.in_zellij = in_zellij;
2493 env.wezterm_unix_socket = in_wezterm_mux;
2494 let caps = TerminalCapabilities::detect_from_inputs(&env);
2495
2496 assert!(
2498 caps.true_color,
2499 "{mux_name}: true_color detection should work"
2500 );
2501 assert!(
2502 !caps.sync_output,
2503 "{mux_name}: sync_output hard-disabled for WezTerm safety"
2504 );
2505
2506 assert!(
2508 !caps.use_sync_output(),
2509 "{mux_name}: use_sync_output() should be false"
2510 );
2511 assert!(
2512 !caps.use_scroll_region(),
2513 "{mux_name}: use_scroll_region() should be false"
2514 );
2515 assert!(
2516 !caps.use_hyperlinks(),
2517 "{mux_name}: use_hyperlinks() should be false"
2518 );
2519 }
2520 }
2521
2522 #[test]
2524 fn profile_mux_invariant_matrix() {
2525 for profile in TerminalProfile::all_predefined() {
2527 let caps = TerminalCapabilities::from_profile(*profile);
2528 let name = profile.as_str();
2529
2530 let expected_mux =
2532 caps.in_tmux || caps.in_screen || caps.in_zellij || caps.in_wezterm_mux;
2533 assert_eq!(
2534 caps.in_any_mux(),
2535 expected_mux,
2536 "{name}: in_any_mux() should match individual flags"
2537 );
2538
2539 if caps.in_any_mux() {
2541 assert!(
2542 !caps.use_sync_output(),
2543 "{name}: mux should disable use_sync_output()"
2544 );
2545 assert!(
2546 !caps.use_scroll_region(),
2547 "{name}: mux should disable use_scroll_region()"
2548 );
2549 assert!(
2550 !caps.use_hyperlinks(),
2551 "{name}: mux should disable use_hyperlinks()"
2552 );
2553 }
2554
2555 if caps.in_tmux || caps.in_screen {
2557 assert!(
2558 caps.needs_passthrough_wrap(),
2559 "{name}: tmux/screen should need passthrough wrap"
2560 );
2561 } else if caps.in_zellij {
2562 assert!(
2563 !caps.needs_passthrough_wrap(),
2564 "{name}: zellij should NOT need passthrough wrap"
2565 );
2566 }
2567 }
2568 }
2569
2570 #[test]
2572 fn fallback_ordering_matrix() {
2573 use crate::inline_mode::InlineStrategy;
2574
2575 let caps_full = TerminalCapabilities::builder()
2577 .sync_output(true)
2578 .scroll_region(true)
2579 .build();
2580 assert_eq!(
2581 InlineStrategy::select(&caps_full),
2582 InlineStrategy::ScrollRegion,
2583 "full capabilities should use ScrollRegion"
2584 );
2585
2586 let caps_hybrid = TerminalCapabilities::builder()
2588 .sync_output(false)
2589 .scroll_region(true)
2590 .build();
2591 assert_eq!(
2592 InlineStrategy::select(&caps_hybrid),
2593 InlineStrategy::Hybrid,
2594 "scroll without sync should use Hybrid"
2595 );
2596
2597 let caps_none = TerminalCapabilities::builder()
2599 .sync_output(false)
2600 .scroll_region(false)
2601 .build();
2602 assert_eq!(
2603 InlineStrategy::select(&caps_none),
2604 InlineStrategy::OverlayRedraw,
2605 "no capabilities should use OverlayRedraw"
2606 );
2607
2608 let caps_tmux = TerminalCapabilities::tmux();
2610 assert_eq!(
2611 InlineStrategy::select(&caps_tmux),
2612 InlineStrategy::OverlayRedraw,
2613 "tmux should force OverlayRedraw"
2614 );
2615 }
2616
2617 #[test]
2619 fn terminal_mux_strategy_matrix() {
2620 use crate::inline_mode::InlineStrategy;
2621
2622 struct TestCase {
2623 name: &'static str,
2624 profile: TerminalProfile,
2625 expected: InlineStrategy,
2626 }
2627
2628 let cases = [
2629 TestCase {
2630 name: "modern (no mux)",
2631 profile: TerminalProfile::Modern,
2632 expected: InlineStrategy::ScrollRegion,
2633 },
2634 TestCase {
2635 name: "kitty (no mux)",
2636 profile: TerminalProfile::Kitty,
2637 expected: InlineStrategy::ScrollRegion,
2638 },
2639 TestCase {
2640 name: "xterm-256color (no mux)",
2641 profile: TerminalProfile::Xterm256Color,
2642 expected: InlineStrategy::Hybrid, },
2644 TestCase {
2645 name: "xterm (no mux)",
2646 profile: TerminalProfile::Xterm,
2647 expected: InlineStrategy::Hybrid,
2648 },
2649 TestCase {
2650 name: "vt100 (no mux)",
2651 profile: TerminalProfile::Vt100,
2652 expected: InlineStrategy::Hybrid,
2653 },
2654 TestCase {
2655 name: "dumb",
2656 profile: TerminalProfile::Dumb,
2657 expected: InlineStrategy::OverlayRedraw, },
2659 TestCase {
2660 name: "tmux",
2661 profile: TerminalProfile::Tmux,
2662 expected: InlineStrategy::OverlayRedraw,
2663 },
2664 TestCase {
2665 name: "screen",
2666 profile: TerminalProfile::Screen,
2667 expected: InlineStrategy::OverlayRedraw,
2668 },
2669 TestCase {
2670 name: "zellij",
2671 profile: TerminalProfile::Zellij,
2672 expected: InlineStrategy::OverlayRedraw,
2673 },
2674 ];
2675
2676 for case in cases {
2677 let caps = TerminalCapabilities::from_profile(case.profile);
2678 let actual = InlineStrategy::select(&caps);
2679 assert_eq!(
2680 actual, case.expected,
2681 "{}: expected {:?}, got {:?}",
2682 case.name, case.expected, actual
2683 );
2684 }
2685 }
2686
2687 #[test]
2690 fn shared_caps_load_returns_initial() {
2691 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
2692 assert!(shared.load().true_color);
2693 assert!(shared.load().sync_output);
2694 }
2695
2696 #[test]
2697 fn shared_caps_store_replaces_value() {
2698 let shared = SharedCapabilities::new(TerminalCapabilities::modern());
2699 shared.store(TerminalCapabilities::dumb());
2700 let loaded = shared.load();
2701 assert!(!loaded.true_color);
2702 assert!(!loaded.sync_output);
2703 }
2704
2705 #[test]
2706 fn shared_caps_concurrent_read_write() {
2707 use std::sync::{Arc, Barrier};
2708 use std::thread;
2709
2710 let shared = Arc::new(SharedCapabilities::new(TerminalCapabilities::basic()));
2711 let barrier = Arc::new(Barrier::new(5)); let readers: Vec<_> = (0..4)
2714 .map(|_| {
2715 let s = Arc::clone(&shared);
2716 let b = Arc::clone(&barrier);
2717 thread::spawn(move || {
2718 b.wait();
2719 for _ in 0..10_000 {
2720 let caps = s.load();
2721 let _ = caps.use_sync_output();
2723 let _ = caps.true_color;
2724 }
2725 })
2726 })
2727 .collect();
2728
2729 let writer = {
2730 let s = Arc::clone(&shared);
2731 let b = Arc::clone(&barrier);
2732 thread::spawn(move || {
2733 b.wait();
2734 for i in 0..1_000 {
2735 if i % 2 == 0 {
2736 s.store(TerminalCapabilities::modern());
2737 } else {
2738 s.store(TerminalCapabilities::dumb());
2739 }
2740 }
2741 })
2742 };
2743
2744 writer.join().unwrap();
2745 for h in readers {
2746 h.join().unwrap();
2747 }
2748 }
2749}
2750
2751#[cfg(test)]
2756mod proptests {
2757 use super::*;
2758 use proptest::prelude::*;
2759
2760 proptest! {
2761 #[test]
2763 fn prop_in_any_mux_consistent(
2764 in_tmux in any::<bool>(),
2765 in_screen in any::<bool>(),
2766 in_zellij in any::<bool>(),
2767 in_wezterm_mux in any::<bool>(),
2768 ) {
2769 let caps = TerminalCapabilities::builder()
2770 .in_tmux(in_tmux)
2771 .in_screen(in_screen)
2772 .in_zellij(in_zellij)
2773 .in_wezterm_mux(in_wezterm_mux)
2774 .build();
2775
2776 let expected = in_tmux || in_screen || in_zellij || in_wezterm_mux;
2777 prop_assert_eq!(caps.in_any_mux(), expected);
2778 }
2779
2780 #[test]
2782 fn prop_mux_disables_sync_output(
2783 in_tmux in any::<bool>(),
2784 in_screen in any::<bool>(),
2785 in_zellij in any::<bool>(),
2786 in_wezterm_mux in any::<bool>(),
2787 sync_output in any::<bool>(),
2788 ) {
2789 let caps = TerminalCapabilities::builder()
2790 .in_tmux(in_tmux)
2791 .in_screen(in_screen)
2792 .in_zellij(in_zellij)
2793 .in_wezterm_mux(in_wezterm_mux)
2794 .sync_output(sync_output)
2795 .build();
2796
2797 if caps.in_any_mux() {
2798 prop_assert!(!caps.use_sync_output(), "mux should disable sync_output policy");
2799 }
2800 }
2801
2802 #[test]
2804 fn prop_mux_disables_scroll_region(
2805 in_tmux in any::<bool>(),
2806 in_screen in any::<bool>(),
2807 in_zellij in any::<bool>(),
2808 in_wezterm_mux in any::<bool>(),
2809 scroll_region in any::<bool>(),
2810 ) {
2811 let caps = TerminalCapabilities::builder()
2812 .in_tmux(in_tmux)
2813 .in_screen(in_screen)
2814 .in_zellij(in_zellij)
2815 .in_wezterm_mux(in_wezterm_mux)
2816 .scroll_region(scroll_region)
2817 .build();
2818
2819 if caps.in_any_mux() {
2820 prop_assert!(!caps.use_scroll_region(), "mux should disable scroll_region policy");
2821 }
2822 }
2823
2824 #[test]
2826 fn prop_mux_disables_hyperlinks(
2827 in_tmux in any::<bool>(),
2828 in_screen in any::<bool>(),
2829 in_zellij in any::<bool>(),
2830 in_wezterm_mux in any::<bool>(),
2831 osc8_hyperlinks in any::<bool>(),
2832 ) {
2833 let caps = TerminalCapabilities::builder()
2834 .in_tmux(in_tmux)
2835 .in_screen(in_screen)
2836 .in_zellij(in_zellij)
2837 .in_wezterm_mux(in_wezterm_mux)
2838 .osc8_hyperlinks(osc8_hyperlinks)
2839 .build();
2840
2841 if caps.in_any_mux() {
2842 prop_assert!(!caps.use_hyperlinks(), "mux should disable hyperlinks policy");
2843 }
2844 }
2845
2846 #[test]
2848 fn prop_passthrough_wrap_logic(
2849 in_tmux in any::<bool>(),
2850 in_screen in any::<bool>(),
2851 in_zellij in any::<bool>(),
2852 in_wezterm_mux in any::<bool>(),
2853 ) {
2854 let caps = TerminalCapabilities::builder()
2855 .in_tmux(in_tmux)
2856 .in_screen(in_screen)
2857 .in_zellij(in_zellij)
2858 .in_wezterm_mux(in_wezterm_mux)
2859 .build();
2860
2861 let expected = in_tmux || in_screen; prop_assert_eq!(caps.needs_passthrough_wrap(), expected);
2863 }
2864
2865 #[test]
2867 fn prop_policy_false_when_capability_off(
2868 in_tmux in any::<bool>(),
2869 in_screen in any::<bool>(),
2870 in_zellij in any::<bool>(),
2871 in_wezterm_mux in any::<bool>(),
2872 ) {
2873 let caps = TerminalCapabilities::builder()
2874 .in_tmux(in_tmux)
2875 .in_screen(in_screen)
2876 .in_zellij(in_zellij)
2877 .in_wezterm_mux(in_wezterm_mux)
2878 .sync_output(false)
2879 .scroll_region(false)
2880 .osc8_hyperlinks(false)
2881 .osc52_clipboard(false)
2882 .build();
2883
2884 prop_assert!(!caps.use_sync_output(), "sync_output=false implies use_sync_output()=false");
2885 prop_assert!(!caps.use_scroll_region(), "scroll_region=false implies use_scroll_region()=false");
2886 prop_assert!(!caps.use_hyperlinks(), "osc8_hyperlinks=false implies use_hyperlinks()=false");
2887 prop_assert!(!caps.use_clipboard(), "osc52_clipboard=false implies use_clipboard()=false");
2888 }
2889
2890 #[test]
2892 fn prop_no_color_preserves_non_visual(no_color in any::<bool>()) {
2893 let env = DetectInputs {
2894 no_color,
2895 term: "xterm-256color".to_string(),
2896 term_program: "WezTerm".to_string(),
2897 colorterm: "truecolor".to_string(),
2898 in_tmux: false,
2899 in_screen: false,
2900 in_zellij: false,
2901 wezterm_unix_socket: false,
2902 wezterm_pane: false,
2903 wezterm_executable: false,
2904 kitty_window_id: false,
2905 wt_session: false,
2906 };
2907 let caps = TerminalCapabilities::detect_from_inputs(&env);
2908
2909 if no_color {
2910 prop_assert!(!caps.true_color, "NO_COLOR disables true_color");
2911 prop_assert!(!caps.colors_256, "NO_COLOR disables colors_256");
2912 prop_assert!(!caps.osc8_hyperlinks, "NO_COLOR disables hyperlinks");
2913 }
2914
2915 prop_assert!(
2917 !caps.sync_output,
2918 "WezTerm sync_output stays disabled despite NO_COLOR"
2919 );
2920 prop_assert!(caps.bracketed_paste, "bracketed_paste preserved despite NO_COLOR");
2921 }
2922 }
2923}