1#![forbid(unsafe_code)]
2
3use crate::terminal_capabilities::{ColorDepth, TerminalCapabilities};
55use std::cell::RefCell;
56
57#[derive(Debug, Clone, Default)]
71pub struct CapabilityOverride {
72 pub color_depth: Option<ColorDepth>,
74
75 pub unicode_box_drawing: Option<bool>,
77 pub unicode_emoji: Option<bool>,
78 pub double_width: Option<bool>,
79
80 pub sync_output: Option<bool>,
82 pub osc8_hyperlinks: Option<bool>,
83 pub scroll_region: Option<bool>,
84
85 pub in_tmux: Option<bool>,
87 pub in_screen: Option<bool>,
88 pub in_zellij: Option<bool>,
89 pub in_wezterm_mux: Option<bool>,
90
91 pub kitty_keyboard: Option<bool>,
93 pub focus_events: Option<bool>,
94 pub bracketed_paste: Option<bool>,
95 pub mouse_sgr: Option<bool>,
96
97 pub osc52_clipboard: Option<bool>,
99}
100
101impl CapabilityOverride {
102 #[must_use]
104 pub const fn new() -> Self {
105 Self {
106 color_depth: None,
107 unicode_box_drawing: None,
108 unicode_emoji: None,
109 double_width: None,
110 sync_output: None,
111 osc8_hyperlinks: None,
112 scroll_region: None,
113 in_tmux: None,
114 in_screen: None,
115 in_zellij: None,
116 in_wezterm_mux: None,
117 kitty_keyboard: None,
118 focus_events: None,
119 bracketed_paste: None,
120 mouse_sgr: None,
121 osc52_clipboard: None,
122 }
123 }
124
125 #[must_use]
127 pub const fn dumb() -> Self {
128 Self {
129 color_depth: Some(ColorDepth::Mono),
130 unicode_box_drawing: Some(false),
131 unicode_emoji: Some(false),
132 double_width: Some(false),
133 sync_output: Some(false),
134 osc8_hyperlinks: Some(false),
135 scroll_region: Some(false),
136 in_tmux: Some(false),
137 in_screen: Some(false),
138 in_zellij: Some(false),
139 in_wezterm_mux: Some(false),
140 kitty_keyboard: Some(false),
141 focus_events: Some(false),
142 bracketed_paste: Some(false),
143 mouse_sgr: Some(false),
144 osc52_clipboard: Some(false),
145 }
146 }
147
148 #[must_use]
150 pub const fn modern() -> Self {
151 Self {
152 color_depth: Some(ColorDepth::TrueColor),
153 unicode_box_drawing: Some(true),
154 unicode_emoji: Some(true),
155 double_width: Some(true),
156 sync_output: Some(true),
157 osc8_hyperlinks: Some(true),
158 scroll_region: Some(true),
159 in_tmux: Some(false),
160 in_screen: Some(false),
161 in_zellij: Some(false),
162 in_wezterm_mux: Some(false),
163 kitty_keyboard: Some(true),
164 focus_events: Some(true),
165 bracketed_paste: Some(true),
166 mouse_sgr: Some(true),
167 osc52_clipboard: Some(true),
168 }
169 }
170
171 #[must_use]
177 pub const fn tmux() -> Self {
178 Self {
179 color_depth: None,
180 unicode_box_drawing: None,
181 unicode_emoji: None,
182 double_width: None,
183 sync_output: Some(false),
184 osc8_hyperlinks: Some(false),
185 scroll_region: Some(true),
186 in_tmux: Some(true),
187 in_screen: Some(false),
188 in_zellij: Some(false),
189 in_wezterm_mux: Some(false),
190 kitty_keyboard: Some(false),
191 focus_events: Some(false),
192 bracketed_paste: Some(true),
193 mouse_sgr: Some(true),
194 osc52_clipboard: Some(false),
195 }
196 }
197
198 #[must_use]
202 pub const fn color_depth(mut self, value: Option<ColorDepth>) -> Self {
203 self.color_depth = value;
204 self
205 }
206
207 #[must_use]
209 pub const fn unicode_box_drawing(mut self, value: Option<bool>) -> Self {
210 self.unicode_box_drawing = value;
211 self
212 }
213
214 #[must_use]
216 pub const fn unicode_emoji(mut self, value: Option<bool>) -> Self {
217 self.unicode_emoji = value;
218 self
219 }
220
221 #[must_use]
223 pub const fn double_width(mut self, value: Option<bool>) -> Self {
224 self.double_width = value;
225 self
226 }
227
228 #[must_use]
230 pub const fn sync_output(mut self, value: Option<bool>) -> Self {
231 self.sync_output = value;
232 self
233 }
234
235 #[must_use]
237 pub const fn osc8_hyperlinks(mut self, value: Option<bool>) -> Self {
238 self.osc8_hyperlinks = value;
239 self
240 }
241
242 #[must_use]
244 pub const fn scroll_region(mut self, value: Option<bool>) -> Self {
245 self.scroll_region = value;
246 self
247 }
248
249 #[must_use]
251 pub const fn in_tmux(mut self, value: Option<bool>) -> Self {
252 self.in_tmux = value;
253 self
254 }
255
256 #[must_use]
258 pub const fn in_screen(mut self, value: Option<bool>) -> Self {
259 self.in_screen = value;
260 self
261 }
262
263 #[must_use]
265 pub const fn in_zellij(mut self, value: Option<bool>) -> Self {
266 self.in_zellij = value;
267 self
268 }
269
270 #[must_use]
272 pub const fn in_wezterm_mux(mut self, value: Option<bool>) -> Self {
273 self.in_wezterm_mux = value;
274 self
275 }
276
277 #[must_use]
279 pub const fn kitty_keyboard(mut self, value: Option<bool>) -> Self {
280 self.kitty_keyboard = value;
281 self
282 }
283
284 #[must_use]
286 pub const fn focus_events(mut self, value: Option<bool>) -> Self {
287 self.focus_events = value;
288 self
289 }
290
291 #[must_use]
293 pub const fn bracketed_paste(mut self, value: Option<bool>) -> Self {
294 self.bracketed_paste = value;
295 self
296 }
297
298 #[must_use]
300 pub const fn mouse_sgr(mut self, value: Option<bool>) -> Self {
301 self.mouse_sgr = value;
302 self
303 }
304
305 #[must_use]
307 pub const fn osc52_clipboard(mut self, value: Option<bool>) -> Self {
308 self.osc52_clipboard = value;
309 self
310 }
311
312 #[must_use]
314 pub const fn is_empty(&self) -> bool {
315 self.color_depth.is_none()
316 && self.unicode_box_drawing.is_none()
317 && self.unicode_emoji.is_none()
318 && self.double_width.is_none()
319 && self.sync_output.is_none()
320 && self.osc8_hyperlinks.is_none()
321 && self.scroll_region.is_none()
322 && self.in_tmux.is_none()
323 && self.in_screen.is_none()
324 && self.in_zellij.is_none()
325 && self.in_wezterm_mux.is_none()
326 && self.kitty_keyboard.is_none()
327 && self.focus_events.is_none()
328 && self.bracketed_paste.is_none()
329 && self.mouse_sgr.is_none()
330 && self.osc52_clipboard.is_none()
331 }
332
333 #[must_use]
335 pub fn apply_to(&self, mut caps: TerminalCapabilities) -> TerminalCapabilities {
336 if let Some(v) = self.color_depth {
337 caps.color_depth = v;
338 }
339 if let Some(v) = self.unicode_box_drawing {
340 caps.unicode_box_drawing = v;
341 }
342 if let Some(v) = self.unicode_emoji {
343 caps.unicode_emoji = v;
344 }
345 if let Some(v) = self.double_width {
346 caps.double_width = v;
347 }
348 if let Some(v) = self.sync_output {
349 caps.sync_output = v;
350 }
351 if let Some(v) = self.osc8_hyperlinks {
352 caps.osc8_hyperlinks = v;
353 }
354 if let Some(v) = self.scroll_region {
355 caps.scroll_region = v;
356 }
357 if let Some(v) = self.in_tmux {
358 caps.in_tmux = v;
359 }
360 if let Some(v) = self.in_screen {
361 caps.in_screen = v;
362 }
363 if let Some(v) = self.in_zellij {
364 caps.in_zellij = v;
365 }
366 if let Some(v) = self.in_wezterm_mux {
367 caps.in_wezterm_mux = v;
368 }
369 if let Some(v) = self.kitty_keyboard {
370 caps.kitty_keyboard = v;
371 }
372 if let Some(v) = self.focus_events {
373 caps.focus_events = v;
374 }
375 if let Some(v) = self.bracketed_paste {
376 caps.bracketed_paste = v;
377 }
378 if let Some(v) = self.mouse_sgr {
379 caps.mouse_sgr = v;
380 }
381 if let Some(v) = self.osc52_clipboard {
382 caps.osc52_clipboard = v;
383 }
384 caps
385 }
386}
387
388thread_local! {
393 static OVERRIDE_STACK: RefCell<Vec<CapabilityOverride>> = const { RefCell::new(Vec::new()) };
395}
396
397#[must_use]
401pub struct OverrideGuard {
402 _marker: std::marker::PhantomData<*const ()>,
404}
405
406impl Drop for OverrideGuard {
407 fn drop(&mut self) {
408 OVERRIDE_STACK.with(|stack| {
411 stack.borrow_mut().pop();
412 });
413 }
414}
415
416#[must_use = "the override is removed when the guard is dropped"]
430pub fn push_override(over: CapabilityOverride) -> OverrideGuard {
431 OVERRIDE_STACK.with(|stack| {
432 stack.borrow_mut().push(over);
433 });
434 OverrideGuard {
435 _marker: std::marker::PhantomData,
436 }
437}
438
439pub fn with_capability_override<F, R>(over: CapabilityOverride, f: F) -> R
456where
457 F: FnOnce() -> R,
458{
459 let _guard = push_override(over);
460 f()
461}
462
463#[must_use]
468pub fn current_capabilities() -> TerminalCapabilities {
469 let base = TerminalCapabilities::detect();
470 current_capabilities_with_base(base)
471}
472
473#[must_use]
475pub fn current_capabilities_with_base(base: TerminalCapabilities) -> TerminalCapabilities {
476 OVERRIDE_STACK.with(|stack| {
477 let stack = stack.borrow();
478 stack.iter().fold(base, |caps, over| over.apply_to(caps))
479 })
480}
481
482#[must_use]
484pub fn has_active_overrides() -> bool {
485 OVERRIDE_STACK.with(|stack| !stack.borrow().is_empty())
486}
487
488#[must_use]
490pub fn override_depth() -> usize {
491 OVERRIDE_STACK.with(|stack| stack.borrow().len())
492}
493
494pub fn clear_all_overrides() {
499 OVERRIDE_STACK.with(|stack| {
500 stack.borrow_mut().clear();
501 });
502}
503
504fn policy_switch(value: Option<String>) -> Option<bool> {
511 let value = value?;
512 match value.trim().to_ascii_lowercase().as_str() {
513 "1" | "true" | "on" | "yes" => Some(true),
514 "0" | "false" | "off" | "no" => Some(false),
515 _ => None,
516 }
517}
518
519#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
525pub struct PolicyOverrides {
526 pub sync_output: Option<bool>,
529 pub scroll_region: Option<bool>,
531 pub osc52_clipboard: Option<bool>,
533}
534
535impl PolicyOverrides {
536 #[must_use]
538 pub const fn is_empty(&self) -> bool {
539 self.sync_output.is_none() && self.scroll_region.is_none() && self.osc52_clipboard.is_none()
540 }
541}
542
543pub fn apply_env_policy_overrides_with<F>(
561 caps: &mut TerminalCapabilities,
562 get_env: F,
563) -> PolicyOverrides
564where
565 F: Fn(&str) -> Option<String>,
566{
567 let overrides = PolicyOverrides {
568 sync_output: policy_switch(get_env("FTUI_SYNC_OUTPUT")),
569 scroll_region: policy_switch(get_env("FTUI_SCROLL_REGION")),
570 osc52_clipboard: policy_switch(get_env("FTUI_OSC52_CLIPBOARD")),
571 };
572 if let Some(enabled) = overrides.sync_output {
573 caps.sync_output = enabled;
574 if enabled {
575 caps.in_wezterm_mux = false;
576 }
577 }
578 if let Some(enabled) = overrides.scroll_region {
579 caps.scroll_region = enabled;
580 if enabled {
581 caps.in_wezterm_mux = false;
582 }
583 }
584 if let Some(enabled) = overrides.osc52_clipboard {
585 caps.osc52_clipboard = enabled;
586 }
587 overrides
588}
589
590pub fn apply_env_policy_overrides(caps: &mut TerminalCapabilities) -> PolicyOverrides {
594 apply_env_policy_overrides_with(caps, |key| std::env::var(key).ok())
595}
596
597impl TerminalCapabilities {
598 #[must_use]
605 pub fn with_overrides() -> Self {
606 let mut caps = current_capabilities();
607 let _applied = apply_env_policy_overrides(&mut caps);
608 caps
609 }
610
611 #[must_use]
613 pub fn with_overrides_from(self, base: Self) -> Self {
614 current_capabilities_with_base(base)
615 }
616}
617
618#[cfg(test)]
623mod tests {
624 use super::*;
625
626 fn env_from<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
627 move |key| {
628 pairs
629 .iter()
630 .find(|(k, _)| *k == key)
631 .map(|(_, v)| (*v).to_string())
632 }
633 }
634
635 #[test]
636 fn env_policy_switch_parses_common_spellings() {
637 for on in ["1", "true", "ON", " yes "] {
638 assert_eq!(policy_switch(Some(on.to_string())), Some(true), "{on:?}");
639 }
640 for off in ["0", "false", "Off", "no"] {
641 assert_eq!(policy_switch(Some(off.to_string())), Some(false), "{off:?}");
642 }
643 assert_eq!(policy_switch(Some("maybe".to_string())), None);
644 assert_eq!(policy_switch(None), None);
645 }
646
647 #[test]
648 fn env_policy_override_forces_sync_output_on_and_lifts_wezterm_gate() {
649 let mut caps = TerminalCapabilities::xterm_256color();
650 caps.in_wezterm_mux = true;
651 assert!(!caps.use_sync_output());
652 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
653 assert!(caps.sync_output);
654 assert!(
655 !caps.in_wezterm_mux,
656 "explicit opt-in outranks the identity gate"
657 );
658 assert!(caps.use_sync_output());
659 }
660
661 #[test]
662 fn env_policy_override_never_lifts_real_multiplexer_evidence() {
663 let mut caps = TerminalCapabilities::modern();
664 caps.in_tmux = true;
665 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
666 assert!(caps.sync_output);
667 assert!(caps.in_tmux);
668 assert!(!caps.use_sync_output(), "tmux policy still wins");
669 }
670
671 #[test]
672 fn env_policy_override_forces_off_and_ignores_unset_or_junk() {
673 let mut caps = TerminalCapabilities::modern();
674 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "0")]));
675 assert!(!caps.sync_output);
676 assert!(caps.scroll_region, "unrelated switch untouched");
677
678 let mut caps = TerminalCapabilities::modern();
679 apply_env_policy_overrides_with(
680 &mut caps,
681 env_from(&[("FTUI_SYNC_OUTPUT", "banana"), ("FTUI_SCROLL_REGION", "0")]),
682 );
683 assert!(caps.sync_output, "junk value ignored");
684 assert!(!caps.scroll_region);
685 assert!(!caps.use_scroll_region());
686 }
687
688 #[test]
689 fn env_policy_override_reports_which_switches_applied() {
690 let mut caps = TerminalCapabilities::modern();
691 let none = apply_env_policy_overrides_with(&mut caps, env_from(&[]));
692 assert!(none.is_empty());
693 assert_eq!(none, PolicyOverrides::default());
694
695 let mut caps = TerminalCapabilities::modern();
696 let applied = apply_env_policy_overrides_with(
697 &mut caps,
698 env_from(&[("FTUI_SYNC_OUTPUT", "off"), ("FTUI_SCROLL_REGION", "yes")]),
699 );
700 assert_eq!(applied.sync_output, Some(false));
701 assert_eq!(applied.scroll_region, Some(true));
702 assert!(!applied.is_empty());
703 assert!(!caps.sync_output);
704 assert!(caps.scroll_region);
705
706 let mut caps = TerminalCapabilities::modern();
707 let junk =
708 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "maybe")]));
709 assert!(junk.is_empty(), "unparsable switch reports nothing applied");
710 }
711
712 #[test]
713 fn clipboard_policy_requires_explicit_opt_in_and_preserves_mux_identity() {
714 for value in [None, Some("banana"), Some("0"), Some("1")] {
715 let mut caps = TerminalCapabilities::tmux();
716 let applied = apply_env_policy_overrides_with(&mut caps, |key| {
717 (key == "FTUI_OSC52_CLIPBOARD")
718 .then_some(value)
719 .flatten()
720 .map(str::to_owned)
721 });
722 assert_eq!(caps.osc52_clipboard, value == Some("1"));
723 assert!(caps.in_tmux);
724 assert!(!caps.use_sync_output());
725 assert_eq!(
726 applied.is_empty(),
727 value.is_none() || value == Some("banana")
728 );
729 }
730 let mut caps = TerminalCapabilities::modern();
731 let applied = apply_env_policy_overrides_with(
732 &mut caps,
733 env_from(&[("FTUI_OSC52_CLIPBOARD", "off")]),
734 );
735 assert_eq!(applied.osc52_clipboard, Some(false));
736 assert!(!caps.osc52_clipboard);
737 assert!(caps.sync_output);
738 }
739
740 #[test]
741 fn override_new_is_empty() {
742 let over = CapabilityOverride::new();
743 assert!(over.is_empty());
744 }
745
746 #[test]
747 fn override_dumb_disables_all() {
748 let over = CapabilityOverride::dumb();
749 assert!(!over.is_empty());
750 assert_eq!(over.color_depth, Some(ColorDepth::Mono));
751 assert_eq!(over.sync_output, Some(false));
752 assert_eq!(over.mouse_sgr, Some(false));
753 }
754
755 #[test]
756 fn override_modern_enables_all() {
757 let over = CapabilityOverride::modern();
758 assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
759 assert_eq!(over.sync_output, Some(true));
760 assert_eq!(over.kitty_keyboard, Some(true));
761 assert_eq!(over.in_tmux, Some(false));
763 }
764
765 #[test]
766 fn override_tmux_sets_mux() {
767 let over = CapabilityOverride::tmux();
768 assert_eq!(over.in_tmux, Some(true));
769 assert_eq!(over.sync_output, Some(false));
770 assert_eq!(over.osc52_clipboard, Some(false));
771 }
772
773 #[test]
774 fn override_builder_chain() {
775 let over = CapabilityOverride::new()
776 .color_depth(Some(ColorDepth::TrueColor))
777 .unicode_box_drawing(Some(false))
778 .mouse_sgr(Some(false));
779
780 assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
781 assert_eq!(over.unicode_box_drawing, Some(false));
782 assert_eq!(over.mouse_sgr, Some(false));
783 assert!(over.sync_output.is_none());
784 }
785
786 #[test]
787 fn apply_to_overrides_caps() {
788 let base = TerminalCapabilities::dumb();
789 let over = CapabilityOverride::new()
790 .color_depth(Some(ColorDepth::TrueColor))
791 .unicode_box_drawing(Some(true));
792
793 let result = over.apply_to(base);
794 assert_eq!(result.color_depth, ColorDepth::TrueColor);
795 assert!(result.unicode_box_drawing);
796 assert!(!result.mouse_sgr);
798 }
799
800 #[test]
801 fn apply_to_none_keeps_original() {
802 let base = TerminalCapabilities::modern();
803 let over = CapabilityOverride::new(); let result = over.apply_to(base);
806 assert_eq!(result.color_depth, base.color_depth);
807 assert_eq!(result.mouse_sgr, base.mouse_sgr);
808 }
809
810 #[test]
811 fn push_pop_override() {
812 clear_all_overrides();
813 assert!(!has_active_overrides());
814 assert_eq!(override_depth(), 0);
815
816 {
817 let _guard = push_override(CapabilityOverride::dumb());
818 assert!(has_active_overrides());
819 assert_eq!(override_depth(), 1);
820 }
821
822 assert!(!has_active_overrides());
823 assert_eq!(override_depth(), 0);
824 }
825
826 #[test]
827 fn nested_overrides() {
828 clear_all_overrides();
829
830 {
831 let _outer = push_override(
832 CapabilityOverride::new()
833 .color_depth(Some(ColorDepth::TrueColor))
834 .mouse_sgr(Some(true)),
835 );
836 assert_eq!(override_depth(), 1);
837
838 {
839 let _inner =
840 push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
841 assert_eq!(override_depth(), 2);
842
843 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
845 assert_eq!(caps.color_depth, ColorDepth::Mono);
846 assert!(caps.mouse_sgr); }
848
849 assert_eq!(override_depth(), 1);
851 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
852 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
853 }
854
855 assert_eq!(override_depth(), 0);
856 }
857
858 #[test]
859 fn with_capability_override_scope() {
860 clear_all_overrides();
861
862 let result = with_capability_override(CapabilityOverride::modern(), || {
863 assert!(has_active_overrides());
864 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
865 caps.supports_true_color()
866 });
867
868 assert!(result);
869 assert!(!has_active_overrides());
870 }
871
872 #[test]
873 fn with_capability_override_nested() {
874 clear_all_overrides();
875
876 with_capability_override(
877 CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)),
878 || {
879 with_capability_override(CapabilityOverride::new().mouse_sgr(Some(false)), || {
880 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
881 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
882 assert!(!caps.mouse_sgr);
883 });
884 },
885 );
886 }
887
888 #[test]
889 fn with_overrides_method() {
890 clear_all_overrides();
891
892 with_capability_override(CapabilityOverride::dumb(), || {
893 let caps = TerminalCapabilities::with_overrides();
894 assert_eq!(caps.color_depth, ColorDepth::Mono);
895 assert!(!caps.unicode_box_drawing);
896 assert!(!caps.unicode_emoji);
897 assert!(!caps.double_width);
898 });
899 }
900
901 #[test]
902 fn clear_all_overrides_works() {
903 let _g1 = push_override(CapabilityOverride::dumb());
904 let _g2 = push_override(CapabilityOverride::modern());
905 assert_eq!(override_depth(), 2);
906
907 clear_all_overrides();
908 assert_eq!(override_depth(), 0);
909 }
910
911 #[test]
912 fn default_override_is_empty() {
913 let over = CapabilityOverride::default();
914 assert!(over.is_empty());
915 }
916
917 #[test]
918 fn is_empty_false_for_single_override() {
919 let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
920 assert!(!over.is_empty());
921 }
922
923 #[test]
924 fn dumb_disables_all_fields() {
925 let over = CapabilityOverride::dumb();
926 assert_eq!(over.unicode_box_drawing, Some(false));
927 assert_eq!(over.unicode_emoji, Some(false));
928 assert_eq!(over.double_width, Some(false));
929 assert_eq!(over.osc8_hyperlinks, Some(false));
930 assert_eq!(over.scroll_region, Some(false));
931 assert_eq!(over.kitty_keyboard, Some(false));
932 assert_eq!(over.focus_events, Some(false));
933 assert_eq!(over.bracketed_paste, Some(false));
934 assert_eq!(over.osc52_clipboard, Some(false));
935 assert_eq!(over.in_tmux, Some(false));
936 assert_eq!(over.in_screen, Some(false));
937 assert_eq!(over.in_zellij, Some(false));
938 }
939
940 #[test]
941 fn modern_enables_features_disables_mux() {
942 let over = CapabilityOverride::modern();
943 assert_eq!(over.unicode_box_drawing, Some(true));
944 assert_eq!(over.unicode_emoji, Some(true));
945 assert_eq!(over.double_width, Some(true));
946 assert_eq!(over.osc8_hyperlinks, Some(true));
947 assert_eq!(over.scroll_region, Some(true));
948 assert_eq!(over.focus_events, Some(true));
949 assert_eq!(over.bracketed_paste, Some(true));
950 assert_eq!(over.osc52_clipboard, Some(true));
951 assert_eq!(over.in_screen, Some(false));
952 assert_eq!(over.in_zellij, Some(false));
953 }
954
955 #[test]
956 fn tmux_preserves_color_depth_and_sets_bracketed_paste() {
957 let over = CapabilityOverride::tmux();
958 assert_eq!(over.color_depth, None);
959 assert_eq!(over.bracketed_paste, Some(true));
960 assert_eq!(over.mouse_sgr, Some(true));
961 assert_eq!(over.scroll_region, Some(true));
962 assert_eq!(over.kitty_keyboard, Some(false));
963 }
964
965 #[test]
966 fn builder_all_optional_features() {
967 let over = CapabilityOverride::new()
968 .unicode_emoji(Some(true))
969 .double_width(Some(false))
970 .in_screen(Some(true))
971 .in_zellij(Some(true))
972 .osc8_hyperlinks(Some(true))
973 .osc52_clipboard(Some(false))
974 .scroll_region(Some(true))
975 .focus_events(Some(true))
976 .bracketed_paste(Some(false))
977 .kitty_keyboard(Some(true));
978
979 assert_eq!(over.unicode_emoji, Some(true));
980 assert_eq!(over.double_width, Some(false));
981 assert_eq!(over.in_screen, Some(true));
982 assert_eq!(over.in_zellij, Some(true));
983 assert_eq!(over.osc8_hyperlinks, Some(true));
984 assert_eq!(over.osc52_clipboard, Some(false));
985 assert_eq!(over.scroll_region, Some(true));
986 assert_eq!(over.focus_events, Some(true));
987 assert_eq!(over.bracketed_paste, Some(false));
988 assert_eq!(over.kitty_keyboard, Some(true));
989 }
990
991 #[test]
992 fn apply_to_covers_all_mux_flags() {
993 let base = TerminalCapabilities::dumb();
994 let over = CapabilityOverride::new()
995 .in_tmux(Some(true))
996 .in_screen(Some(true))
997 .in_zellij(Some(true))
998 .in_wezterm_mux(Some(true));
999 let result = over.apply_to(base);
1000 assert!(result.in_tmux);
1001 assert!(result.in_screen);
1002 assert!(result.in_zellij);
1003 assert!(result.in_wezterm_mux);
1004 }
1005
1006 #[test]
1007 fn modern_override_simulates_non_mux_even_under_wezterm() {
1008 let mut base = TerminalCapabilities::modern();
1013 base.in_wezterm_mux = true; let result = CapabilityOverride::modern().apply_to(base);
1016 assert!(!result.in_wezterm_mux);
1017 assert!(!result.in_any_mux());
1018 assert!(result.use_sync_output());
1019 assert!(result.use_scroll_region());
1020 assert!(result.use_hyperlinks());
1021 assert!(result.use_clipboard());
1022 }
1023
1024 #[test]
1025 fn dumb_override_clears_wezterm_mux() {
1026 let mut base = TerminalCapabilities::modern();
1027 base.in_wezterm_mux = true;
1028 let result = CapabilityOverride::dumb().apply_to(base);
1029 assert!(!result.in_wezterm_mux);
1030 }
1031
1032 #[test]
1033 fn is_empty_false_for_in_wezterm_mux() {
1034 assert!(
1035 !CapabilityOverride::new()
1036 .in_wezterm_mux(Some(true))
1037 .is_empty()
1038 );
1039 }
1040
1041 #[test]
1042 fn apply_to_covers_input_features() {
1043 let base = TerminalCapabilities::dumb();
1044 let over = CapabilityOverride::new()
1045 .kitty_keyboard(Some(true))
1046 .focus_events(Some(true))
1047 .bracketed_paste(Some(true))
1048 .osc52_clipboard(Some(true));
1049 let result = over.apply_to(base);
1050 assert!(result.kitty_keyboard);
1051 assert!(result.focus_events);
1052 assert!(result.bracketed_paste);
1053 assert!(result.osc52_clipboard);
1054 }
1055
1056 #[test]
1057 fn current_capabilities_with_base_composes_stack() {
1058 clear_all_overrides();
1059 let base = TerminalCapabilities::dumb();
1060
1061 let _g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1062 let _g2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1063
1064 let caps = current_capabilities_with_base(base);
1065 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1066 assert!(caps.mouse_sgr);
1067
1068 clear_all_overrides();
1069 }
1070
1071 #[test]
1072 fn override_clone() {
1073 let over = CapabilityOverride::new()
1074 .color_depth(Some(ColorDepth::TrueColor))
1075 .in_tmux(Some(false));
1076 let cloned = over.clone();
1077 assert_eq!(over.color_depth, cloned.color_depth);
1078 assert_eq!(over.in_tmux, cloned.in_tmux);
1079 }
1080
1081 #[test]
1084 fn is_empty_false_for_color_depth() {
1085 assert!(
1086 !CapabilityOverride::new()
1087 .color_depth(Some(ColorDepth::Ansi256))
1088 .is_empty()
1089 );
1090 }
1091
1092 #[test]
1093 fn is_empty_false_for_unicode_box_drawing() {
1094 assert!(
1095 !CapabilityOverride::new()
1096 .unicode_box_drawing(Some(false))
1097 .is_empty()
1098 );
1099 }
1100
1101 #[test]
1102 fn is_empty_false_for_unicode_emoji() {
1103 assert!(
1104 !CapabilityOverride::new()
1105 .unicode_emoji(Some(true))
1106 .is_empty()
1107 );
1108 }
1109
1110 #[test]
1111 fn is_empty_false_for_double_width() {
1112 assert!(
1113 !CapabilityOverride::new()
1114 .double_width(Some(true))
1115 .is_empty()
1116 );
1117 }
1118
1119 #[test]
1120 fn is_empty_false_for_sync_output() {
1121 assert!(
1122 !CapabilityOverride::new()
1123 .sync_output(Some(false))
1124 .is_empty()
1125 );
1126 }
1127
1128 #[test]
1129 fn is_empty_false_for_osc8_hyperlinks() {
1130 assert!(
1131 !CapabilityOverride::new()
1132 .osc8_hyperlinks(Some(true))
1133 .is_empty()
1134 );
1135 }
1136
1137 #[test]
1138 fn is_empty_false_for_scroll_region() {
1139 assert!(
1140 !CapabilityOverride::new()
1141 .scroll_region(Some(true))
1142 .is_empty()
1143 );
1144 }
1145
1146 #[test]
1147 fn is_empty_false_for_in_tmux() {
1148 assert!(!CapabilityOverride::new().in_tmux(Some(true)).is_empty());
1149 }
1150
1151 #[test]
1152 fn is_empty_false_for_in_screen() {
1153 assert!(!CapabilityOverride::new().in_screen(Some(true)).is_empty());
1154 }
1155
1156 #[test]
1157 fn is_empty_false_for_in_zellij() {
1158 assert!(!CapabilityOverride::new().in_zellij(Some(true)).is_empty());
1159 }
1160
1161 #[test]
1162 fn is_empty_false_for_kitty_keyboard() {
1163 assert!(
1164 !CapabilityOverride::new()
1165 .kitty_keyboard(Some(true))
1166 .is_empty()
1167 );
1168 }
1169
1170 #[test]
1171 fn is_empty_false_for_focus_events() {
1172 assert!(
1173 !CapabilityOverride::new()
1174 .focus_events(Some(false))
1175 .is_empty()
1176 );
1177 }
1178
1179 #[test]
1180 fn is_empty_false_for_bracketed_paste() {
1181 assert!(
1182 !CapabilityOverride::new()
1183 .bracketed_paste(Some(true))
1184 .is_empty()
1185 );
1186 }
1187
1188 #[test]
1189 fn is_empty_false_for_mouse_sgr() {
1190 assert!(!CapabilityOverride::new().mouse_sgr(Some(true)).is_empty());
1191 }
1192
1193 #[test]
1194 fn is_empty_false_for_osc52_clipboard() {
1195 assert!(
1196 !CapabilityOverride::new()
1197 .osc52_clipboard(Some(false))
1198 .is_empty()
1199 );
1200 }
1201
1202 #[test]
1205 fn apply_to_covers_unicode_emoji() {
1206 let base = TerminalCapabilities::dumb();
1207 let result = CapabilityOverride::new()
1208 .unicode_emoji(Some(true))
1209 .apply_to(base);
1210 assert!(result.unicode_emoji);
1211 }
1212
1213 #[test]
1214 fn apply_to_covers_double_width() {
1215 let base = TerminalCapabilities::dumb();
1216 let result = CapabilityOverride::new()
1217 .double_width(Some(true))
1218 .apply_to(base);
1219 assert!(result.double_width);
1220 }
1221
1222 #[test]
1223 fn apply_to_covers_sync_output() {
1224 let base = TerminalCapabilities::dumb();
1225 let result = CapabilityOverride::new()
1226 .sync_output(Some(true))
1227 .apply_to(base);
1228 assert!(result.sync_output);
1229 }
1230
1231 #[test]
1232 fn apply_to_covers_osc8_hyperlinks() {
1233 let base = TerminalCapabilities::dumb();
1234 let result = CapabilityOverride::new()
1235 .osc8_hyperlinks(Some(true))
1236 .apply_to(base);
1237 assert!(result.osc8_hyperlinks);
1238 }
1239
1240 #[test]
1241 fn apply_to_covers_scroll_region() {
1242 let base = TerminalCapabilities::dumb();
1243 let result = CapabilityOverride::new()
1244 .scroll_region(Some(true))
1245 .apply_to(base);
1246 assert!(result.scroll_region);
1247 }
1248
1249 #[test]
1250 fn apply_to_covers_mouse_sgr() {
1251 let base = TerminalCapabilities::dumb();
1252 let result = CapabilityOverride::new()
1253 .mouse_sgr(Some(true))
1254 .apply_to(base);
1255 assert!(result.mouse_sgr);
1256 }
1257
1258 #[test]
1261 fn dumb_override_disables_all_on_modern_base() {
1262 let base = TerminalCapabilities::modern();
1263 let result = CapabilityOverride::dumb().apply_to(base);
1264 assert_eq!(result.color_depth, ColorDepth::Mono);
1265 assert!(!result.unicode_box_drawing);
1266 assert!(!result.unicode_emoji);
1267 assert!(!result.double_width);
1268 assert!(!result.sync_output);
1269 assert!(!result.osc8_hyperlinks);
1270 assert!(!result.scroll_region);
1271 assert!(!result.in_tmux);
1272 assert!(!result.in_screen);
1273 assert!(!result.in_zellij);
1274 assert!(!result.kitty_keyboard);
1275 assert!(!result.focus_events);
1276 assert!(!result.bracketed_paste);
1277 assert!(!result.mouse_sgr);
1278 assert!(!result.osc52_clipboard);
1279 }
1280
1281 #[test]
1282 fn modern_override_enables_features_on_dumb_base() {
1283 let base = TerminalCapabilities::dumb();
1284 let result = CapabilityOverride::modern().apply_to(base);
1285 assert_eq!(result.color_depth, ColorDepth::TrueColor);
1286 assert!(result.unicode_box_drawing);
1287 assert!(result.unicode_emoji);
1288 assert!(result.double_width);
1289 assert!(result.sync_output);
1290 assert!(result.osc8_hyperlinks);
1291 assert!(result.scroll_region);
1292 assert!(!result.in_tmux);
1294 assert!(!result.in_screen);
1295 assert!(!result.in_zellij);
1296 assert!(result.kitty_keyboard);
1297 assert!(result.focus_events);
1298 assert!(result.bracketed_paste);
1299 assert!(result.mouse_sgr);
1300 assert!(result.osc52_clipboard);
1301 }
1302
1303 #[test]
1306 fn tmux_leaves_depth_and_unrelated_fields_unset() {
1307 let over = CapabilityOverride::tmux();
1308 assert_eq!(over.color_depth, None);
1309 assert!(over.unicode_box_drawing.is_none());
1310 assert!(over.unicode_emoji.is_none());
1311 assert!(over.double_width.is_none());
1312 }
1313
1314 #[test]
1317 fn builder_in_tmux_individually() {
1318 let over = CapabilityOverride::new().in_tmux(Some(true));
1319 assert_eq!(over.in_tmux, Some(true));
1320 assert!(over.color_depth.is_none()); }
1322
1323 #[test]
1324 fn builder_sync_output_individually() {
1325 let over = CapabilityOverride::new().sync_output(Some(false));
1326 assert_eq!(over.sync_output, Some(false));
1327 assert!(over.color_depth.is_none());
1328 }
1329
1330 #[test]
1333 fn builder_overwrite_field_to_none() {
1334 let over = CapabilityOverride::new()
1335 .color_depth(Some(ColorDepth::TrueColor))
1336 .color_depth(None);
1337 assert!(over.color_depth.is_none());
1338 assert!(over.is_empty());
1339 }
1340
1341 #[test]
1342 fn builder_overwrite_dumb_field_to_none() {
1343 let over = CapabilityOverride::dumb().color_depth(None);
1344 assert!(over.color_depth.is_none());
1345 assert!(!over.is_empty()); }
1347
1348 #[test]
1351 fn guard_drop_after_clear_all_is_noop() {
1352 clear_all_overrides();
1353
1354 let guard = push_override(CapabilityOverride::dumb());
1355 assert_eq!(override_depth(), 1);
1356
1357 clear_all_overrides();
1358 assert_eq!(override_depth(), 0);
1359
1360 drop(guard);
1362 assert_eq!(override_depth(), 0);
1363 }
1364
1365 #[test]
1366 fn multiple_guards_drop_after_clear_all() {
1367 clear_all_overrides();
1368
1369 let g1 = push_override(CapabilityOverride::dumb());
1370 let g2 = push_override(CapabilityOverride::modern());
1371 assert_eq!(override_depth(), 2);
1372
1373 clear_all_overrides();
1374 assert_eq!(override_depth(), 0);
1375
1376 drop(g2);
1378 drop(g1);
1379 assert_eq!(override_depth(), 0);
1380 }
1381
1382 #[test]
1385 fn three_level_nesting_innermost_wins() {
1386 clear_all_overrides();
1387
1388 let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1389 let _l2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1390 let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1391
1392 assert_eq!(override_depth(), 3);
1393 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1394 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1395
1396 clear_all_overrides();
1397 }
1398
1399 #[test]
1400 fn three_level_nesting_partial_overrides() {
1401 clear_all_overrides();
1402
1403 let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1404 let _l2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1405 let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Ansi256)));
1406
1407 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1408 assert_eq!(caps.color_depth, ColorDepth::Ansi256); assert!(caps.mouse_sgr); assert!(!caps.sync_output); clear_all_overrides();
1413 }
1414
1415 #[test]
1418 fn with_overrides_from_applies_stack() {
1419 clear_all_overrides();
1420
1421 let base = TerminalCapabilities::dumb();
1422 let _g = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1423
1424 let caps = base.with_overrides_from(base);
1426 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1427
1428 clear_all_overrides();
1429 }
1430
1431 #[test]
1432 fn with_overrides_from_without_active_overrides() {
1433 clear_all_overrides();
1434
1435 let base = TerminalCapabilities::modern();
1436 let caps = base.with_overrides_from(base);
1437 assert_eq!(caps.color_depth, base.color_depth);
1439 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1440 }
1441
1442 #[test]
1445 fn with_capability_override_cleans_up_on_panic() {
1446 clear_all_overrides();
1447
1448 let result = std::panic::catch_unwind(|| {
1449 with_capability_override(CapabilityOverride::dumb(), || {
1450 assert!(has_active_overrides());
1451 panic!("deliberate panic");
1452 });
1453 });
1454
1455 assert!(result.is_err());
1456 assert!(!has_active_overrides());
1458 assert_eq!(override_depth(), 0);
1459 }
1460
1461 #[test]
1464 fn debug_format_contains_field_names() {
1465 let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
1466 let dbg = format!("{over:?}");
1467 assert!(dbg.contains("color_depth"));
1468 assert!(dbg.contains("TrueColor"));
1469 }
1470
1471 #[test]
1472 fn debug_format_empty_override() {
1473 let over = CapabilityOverride::new();
1474 let dbg = format!("{over:?}");
1475 assert!(dbg.contains("CapabilityOverride"));
1476 assert!(dbg.contains("None"));
1477 }
1478
1479 #[test]
1482 fn clear_all_then_push_resumes_normally() {
1483 clear_all_overrides();
1484
1485 let _g1 = push_override(CapabilityOverride::dumb());
1486 clear_all_overrides();
1487 assert_eq!(override_depth(), 0);
1488
1489 let _g2 = push_override(CapabilityOverride::modern());
1491 assert_eq!(override_depth(), 1);
1492 assert!(has_active_overrides());
1493
1494 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1495 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1496
1497 clear_all_overrides();
1498 }
1499
1500 #[test]
1503 fn current_capabilities_uses_detect_as_base() {
1504 clear_all_overrides();
1505
1506 let _g = push_override(CapabilityOverride::dumb());
1508 let caps = current_capabilities();
1509 assert_eq!(caps.color_depth, ColorDepth::Mono);
1510 assert!(!caps.mouse_sgr);
1511
1512 clear_all_overrides();
1513 }
1514
1515 #[test]
1518 fn with_overrides_integrates_full_stack() {
1519 clear_all_overrides();
1520
1521 let _g = push_override(CapabilityOverride::modern());
1522 let caps = TerminalCapabilities::with_overrides();
1523 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1524 assert!(caps.kitty_keyboard);
1525 assert!(!caps.in_tmux); clear_all_overrides();
1528 }
1529
1530 #[test]
1533 fn second_guard_dropped_first_still_active() {
1534 clear_all_overrides();
1535
1536 let g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1537 let g2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1538
1539 drop(g2);
1541 assert_eq!(override_depth(), 1);
1542 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1543 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1544
1545 drop(g1);
1546 assert_eq!(override_depth(), 0);
1547 }
1548
1549 #[test]
1552 fn with_capability_override_returns_string() {
1553 clear_all_overrides();
1554
1555 let val = with_capability_override(CapabilityOverride::dumb(), || {
1556 String::from("computed value")
1557 });
1558 assert_eq!(val, "computed value");
1559 }
1560
1561 #[test]
1562 fn with_capability_override_returns_tuple() {
1563 clear_all_overrides();
1564
1565 let (a, b) = with_capability_override(CapabilityOverride::modern(), || {
1566 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1567 (caps.supports_true_color(), caps.mouse_sgr)
1568 });
1569 assert!(a);
1570 assert!(b);
1571 }
1572
1573 #[test]
1576 fn apply_to_disables_on_modern_base() {
1577 let base = TerminalCapabilities::modern();
1578 let result = CapabilityOverride::new()
1579 .color_depth(Some(ColorDepth::Ansi256))
1580 .kitty_keyboard(Some(false))
1581 .apply_to(base);
1582 assert_eq!(result.color_depth, ColorDepth::Ansi256);
1583 assert!(!result.kitty_keyboard);
1584 assert!(result.supports_256_colors());
1586 assert!(result.unicode_box_drawing);
1587 }
1588
1589 #[test]
1592 fn current_capabilities_with_base_no_overrides_returns_base() {
1593 clear_all_overrides();
1594
1595 let base = TerminalCapabilities::modern();
1596 let caps = current_capabilities_with_base(base);
1597 assert_eq!(caps.color_depth, base.color_depth);
1598 assert_eq!(caps.unicode_box_drawing, base.unicode_box_drawing);
1599 assert_eq!(caps.unicode_emoji, base.unicode_emoji);
1600 assert_eq!(caps.double_width, base.double_width);
1601 assert_eq!(caps.sync_output, base.sync_output);
1602 assert_eq!(caps.osc8_hyperlinks, base.osc8_hyperlinks);
1603 assert_eq!(caps.scroll_region, base.scroll_region);
1604 assert_eq!(caps.in_tmux, base.in_tmux);
1605 assert_eq!(caps.in_screen, base.in_screen);
1606 assert_eq!(caps.in_zellij, base.in_zellij);
1607 assert_eq!(caps.kitty_keyboard, base.kitty_keyboard);
1608 assert_eq!(caps.focus_events, base.focus_events);
1609 assert_eq!(caps.bracketed_paste, base.bracketed_paste);
1610 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1611 assert_eq!(caps.osc52_clipboard, base.osc52_clipboard);
1612 }
1613}