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