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}
532
533impl PolicyOverrides {
534 #[must_use]
536 pub const fn is_empty(&self) -> bool {
537 self.sync_output.is_none() && self.scroll_region.is_none()
538 }
539}
540
541pub fn apply_env_policy_overrides_with<F>(
556 caps: &mut TerminalCapabilities,
557 get_env: F,
558) -> PolicyOverrides
559where
560 F: Fn(&str) -> Option<String>,
561{
562 let overrides = PolicyOverrides {
563 sync_output: policy_switch(get_env("FTUI_SYNC_OUTPUT")),
564 scroll_region: policy_switch(get_env("FTUI_SCROLL_REGION")),
565 };
566 if let Some(enabled) = overrides.sync_output {
567 caps.sync_output = enabled;
568 if enabled {
569 caps.in_wezterm_mux = false;
570 }
571 }
572 if let Some(enabled) = overrides.scroll_region {
573 caps.scroll_region = enabled;
574 if enabled {
575 caps.in_wezterm_mux = false;
576 }
577 }
578 overrides
579}
580
581pub fn apply_env_policy_overrides(caps: &mut TerminalCapabilities) -> PolicyOverrides {
585 apply_env_policy_overrides_with(caps, |key| std::env::var(key).ok())
586}
587
588impl TerminalCapabilities {
589 #[must_use]
596 pub fn with_overrides() -> Self {
597 let mut caps = current_capabilities();
598 let _applied = apply_env_policy_overrides(&mut caps);
599 caps
600 }
601
602 #[must_use]
604 pub fn with_overrides_from(self, base: Self) -> Self {
605 current_capabilities_with_base(base)
606 }
607}
608
609#[cfg(test)]
614mod tests {
615 use super::*;
616
617 fn env_from<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
618 move |key| {
619 pairs
620 .iter()
621 .find(|(k, _)| *k == key)
622 .map(|(_, v)| (*v).to_string())
623 }
624 }
625
626 #[test]
627 fn env_policy_switch_parses_common_spellings() {
628 for on in ["1", "true", "ON", " yes "] {
629 assert_eq!(policy_switch(Some(on.to_string())), Some(true), "{on:?}");
630 }
631 for off in ["0", "false", "Off", "no"] {
632 assert_eq!(policy_switch(Some(off.to_string())), Some(false), "{off:?}");
633 }
634 assert_eq!(policy_switch(Some("maybe".to_string())), None);
635 assert_eq!(policy_switch(None), None);
636 }
637
638 #[test]
639 fn env_policy_override_forces_sync_output_on_and_lifts_wezterm_gate() {
640 let mut caps = TerminalCapabilities::xterm_256color();
641 caps.in_wezterm_mux = true;
642 assert!(!caps.use_sync_output());
643 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
644 assert!(caps.sync_output);
645 assert!(
646 !caps.in_wezterm_mux,
647 "explicit opt-in outranks the identity gate"
648 );
649 assert!(caps.use_sync_output());
650 }
651
652 #[test]
653 fn env_policy_override_never_lifts_real_multiplexer_evidence() {
654 let mut caps = TerminalCapabilities::modern();
655 caps.in_tmux = true;
656 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
657 assert!(caps.sync_output);
658 assert!(caps.in_tmux);
659 assert!(!caps.use_sync_output(), "tmux policy still wins");
660 }
661
662 #[test]
663 fn env_policy_override_forces_off_and_ignores_unset_or_junk() {
664 let mut caps = TerminalCapabilities::modern();
665 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "0")]));
666 assert!(!caps.sync_output);
667 assert!(caps.scroll_region, "unrelated switch untouched");
668
669 let mut caps = TerminalCapabilities::modern();
670 apply_env_policy_overrides_with(
671 &mut caps,
672 env_from(&[("FTUI_SYNC_OUTPUT", "banana"), ("FTUI_SCROLL_REGION", "0")]),
673 );
674 assert!(caps.sync_output, "junk value ignored");
675 assert!(!caps.scroll_region);
676 assert!(!caps.use_scroll_region());
677 }
678
679 #[test]
680 fn env_policy_override_reports_which_switches_applied() {
681 let mut caps = TerminalCapabilities::modern();
682 let none = apply_env_policy_overrides_with(&mut caps, env_from(&[]));
683 assert!(none.is_empty());
684 assert_eq!(none, PolicyOverrides::default());
685
686 let mut caps = TerminalCapabilities::modern();
687 let applied = apply_env_policy_overrides_with(
688 &mut caps,
689 env_from(&[("FTUI_SYNC_OUTPUT", "off"), ("FTUI_SCROLL_REGION", "yes")]),
690 );
691 assert_eq!(applied.sync_output, Some(false));
692 assert_eq!(applied.scroll_region, Some(true));
693 assert!(!applied.is_empty());
694 assert!(!caps.sync_output);
695 assert!(caps.scroll_region);
696
697 let mut caps = TerminalCapabilities::modern();
698 let junk =
699 apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "maybe")]));
700 assert!(junk.is_empty(), "unparsable switch reports nothing applied");
701 }
702
703 #[test]
704 fn override_new_is_empty() {
705 let over = CapabilityOverride::new();
706 assert!(over.is_empty());
707 }
708
709 #[test]
710 fn override_dumb_disables_all() {
711 let over = CapabilityOverride::dumb();
712 assert!(!over.is_empty());
713 assert_eq!(over.color_depth, Some(ColorDepth::Mono));
714 assert_eq!(over.sync_output, Some(false));
715 assert_eq!(over.mouse_sgr, Some(false));
716 }
717
718 #[test]
719 fn override_modern_enables_all() {
720 let over = CapabilityOverride::modern();
721 assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
722 assert_eq!(over.sync_output, Some(true));
723 assert_eq!(over.kitty_keyboard, Some(true));
724 assert_eq!(over.in_tmux, Some(false));
726 }
727
728 #[test]
729 fn override_tmux_sets_mux() {
730 let over = CapabilityOverride::tmux();
731 assert_eq!(over.in_tmux, Some(true));
732 assert_eq!(over.sync_output, Some(false));
733 assert_eq!(over.osc52_clipboard, Some(false));
734 }
735
736 #[test]
737 fn override_builder_chain() {
738 let over = CapabilityOverride::new()
739 .color_depth(Some(ColorDepth::TrueColor))
740 .unicode_box_drawing(Some(false))
741 .mouse_sgr(Some(false));
742
743 assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
744 assert_eq!(over.unicode_box_drawing, Some(false));
745 assert_eq!(over.mouse_sgr, Some(false));
746 assert!(over.sync_output.is_none());
747 }
748
749 #[test]
750 fn apply_to_overrides_caps() {
751 let base = TerminalCapabilities::dumb();
752 let over = CapabilityOverride::new()
753 .color_depth(Some(ColorDepth::TrueColor))
754 .unicode_box_drawing(Some(true));
755
756 let result = over.apply_to(base);
757 assert_eq!(result.color_depth, ColorDepth::TrueColor);
758 assert!(result.unicode_box_drawing);
759 assert!(!result.mouse_sgr);
761 }
762
763 #[test]
764 fn apply_to_none_keeps_original() {
765 let base = TerminalCapabilities::modern();
766 let over = CapabilityOverride::new(); let result = over.apply_to(base);
769 assert_eq!(result.color_depth, base.color_depth);
770 assert_eq!(result.mouse_sgr, base.mouse_sgr);
771 }
772
773 #[test]
774 fn push_pop_override() {
775 clear_all_overrides();
776 assert!(!has_active_overrides());
777 assert_eq!(override_depth(), 0);
778
779 {
780 let _guard = push_override(CapabilityOverride::dumb());
781 assert!(has_active_overrides());
782 assert_eq!(override_depth(), 1);
783 }
784
785 assert!(!has_active_overrides());
786 assert_eq!(override_depth(), 0);
787 }
788
789 #[test]
790 fn nested_overrides() {
791 clear_all_overrides();
792
793 {
794 let _outer = push_override(
795 CapabilityOverride::new()
796 .color_depth(Some(ColorDepth::TrueColor))
797 .mouse_sgr(Some(true)),
798 );
799 assert_eq!(override_depth(), 1);
800
801 {
802 let _inner =
803 push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
804 assert_eq!(override_depth(), 2);
805
806 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
808 assert_eq!(caps.color_depth, ColorDepth::Mono);
809 assert!(caps.mouse_sgr); }
811
812 assert_eq!(override_depth(), 1);
814 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
815 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
816 }
817
818 assert_eq!(override_depth(), 0);
819 }
820
821 #[test]
822 fn with_capability_override_scope() {
823 clear_all_overrides();
824
825 let result = with_capability_override(CapabilityOverride::modern(), || {
826 assert!(has_active_overrides());
827 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
828 caps.supports_true_color()
829 });
830
831 assert!(result);
832 assert!(!has_active_overrides());
833 }
834
835 #[test]
836 fn with_capability_override_nested() {
837 clear_all_overrides();
838
839 with_capability_override(
840 CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)),
841 || {
842 with_capability_override(CapabilityOverride::new().mouse_sgr(Some(false)), || {
843 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
844 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
845 assert!(!caps.mouse_sgr);
846 });
847 },
848 );
849 }
850
851 #[test]
852 fn with_overrides_method() {
853 clear_all_overrides();
854
855 with_capability_override(CapabilityOverride::dumb(), || {
856 let caps = TerminalCapabilities::with_overrides();
857 assert_eq!(caps.color_depth, ColorDepth::Mono);
858 assert!(!caps.unicode_box_drawing);
859 assert!(!caps.unicode_emoji);
860 assert!(!caps.double_width);
861 });
862 }
863
864 #[test]
865 fn clear_all_overrides_works() {
866 let _g1 = push_override(CapabilityOverride::dumb());
867 let _g2 = push_override(CapabilityOverride::modern());
868 assert_eq!(override_depth(), 2);
869
870 clear_all_overrides();
871 assert_eq!(override_depth(), 0);
872 }
873
874 #[test]
875 fn default_override_is_empty() {
876 let over = CapabilityOverride::default();
877 assert!(over.is_empty());
878 }
879
880 #[test]
881 fn is_empty_false_for_single_override() {
882 let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
883 assert!(!over.is_empty());
884 }
885
886 #[test]
887 fn dumb_disables_all_fields() {
888 let over = CapabilityOverride::dumb();
889 assert_eq!(over.unicode_box_drawing, Some(false));
890 assert_eq!(over.unicode_emoji, Some(false));
891 assert_eq!(over.double_width, Some(false));
892 assert_eq!(over.osc8_hyperlinks, Some(false));
893 assert_eq!(over.scroll_region, Some(false));
894 assert_eq!(over.kitty_keyboard, Some(false));
895 assert_eq!(over.focus_events, Some(false));
896 assert_eq!(over.bracketed_paste, Some(false));
897 assert_eq!(over.osc52_clipboard, Some(false));
898 assert_eq!(over.in_tmux, Some(false));
899 assert_eq!(over.in_screen, Some(false));
900 assert_eq!(over.in_zellij, Some(false));
901 }
902
903 #[test]
904 fn modern_enables_features_disables_mux() {
905 let over = CapabilityOverride::modern();
906 assert_eq!(over.unicode_box_drawing, Some(true));
907 assert_eq!(over.unicode_emoji, Some(true));
908 assert_eq!(over.double_width, Some(true));
909 assert_eq!(over.osc8_hyperlinks, Some(true));
910 assert_eq!(over.scroll_region, Some(true));
911 assert_eq!(over.focus_events, Some(true));
912 assert_eq!(over.bracketed_paste, Some(true));
913 assert_eq!(over.osc52_clipboard, Some(true));
914 assert_eq!(over.in_screen, Some(false));
915 assert_eq!(over.in_zellij, Some(false));
916 }
917
918 #[test]
919 fn tmux_preserves_color_depth_and_sets_bracketed_paste() {
920 let over = CapabilityOverride::tmux();
921 assert_eq!(over.color_depth, None);
922 assert_eq!(over.bracketed_paste, Some(true));
923 assert_eq!(over.mouse_sgr, Some(true));
924 assert_eq!(over.scroll_region, Some(true));
925 assert_eq!(over.kitty_keyboard, Some(false));
926 }
927
928 #[test]
929 fn builder_all_optional_features() {
930 let over = CapabilityOverride::new()
931 .unicode_emoji(Some(true))
932 .double_width(Some(false))
933 .in_screen(Some(true))
934 .in_zellij(Some(true))
935 .osc8_hyperlinks(Some(true))
936 .osc52_clipboard(Some(false))
937 .scroll_region(Some(true))
938 .focus_events(Some(true))
939 .bracketed_paste(Some(false))
940 .kitty_keyboard(Some(true));
941
942 assert_eq!(over.unicode_emoji, Some(true));
943 assert_eq!(over.double_width, Some(false));
944 assert_eq!(over.in_screen, Some(true));
945 assert_eq!(over.in_zellij, Some(true));
946 assert_eq!(over.osc8_hyperlinks, Some(true));
947 assert_eq!(over.osc52_clipboard, Some(false));
948 assert_eq!(over.scroll_region, Some(true));
949 assert_eq!(over.focus_events, Some(true));
950 assert_eq!(over.bracketed_paste, Some(false));
951 assert_eq!(over.kitty_keyboard, Some(true));
952 }
953
954 #[test]
955 fn apply_to_covers_all_mux_flags() {
956 let base = TerminalCapabilities::dumb();
957 let over = CapabilityOverride::new()
958 .in_tmux(Some(true))
959 .in_screen(Some(true))
960 .in_zellij(Some(true))
961 .in_wezterm_mux(Some(true));
962 let result = over.apply_to(base);
963 assert!(result.in_tmux);
964 assert!(result.in_screen);
965 assert!(result.in_zellij);
966 assert!(result.in_wezterm_mux);
967 }
968
969 #[test]
970 fn modern_override_simulates_non_mux_even_under_wezterm() {
971 let mut base = TerminalCapabilities::modern();
976 base.in_wezterm_mux = true; let result = CapabilityOverride::modern().apply_to(base);
979 assert!(!result.in_wezterm_mux);
980 assert!(!result.in_any_mux());
981 assert!(result.use_sync_output());
982 assert!(result.use_scroll_region());
983 assert!(result.use_hyperlinks());
984 assert!(result.use_clipboard());
985 }
986
987 #[test]
988 fn dumb_override_clears_wezterm_mux() {
989 let mut base = TerminalCapabilities::modern();
990 base.in_wezterm_mux = true;
991 let result = CapabilityOverride::dumb().apply_to(base);
992 assert!(!result.in_wezterm_mux);
993 }
994
995 #[test]
996 fn is_empty_false_for_in_wezterm_mux() {
997 assert!(
998 !CapabilityOverride::new()
999 .in_wezterm_mux(Some(true))
1000 .is_empty()
1001 );
1002 }
1003
1004 #[test]
1005 fn apply_to_covers_input_features() {
1006 let base = TerminalCapabilities::dumb();
1007 let over = CapabilityOverride::new()
1008 .kitty_keyboard(Some(true))
1009 .focus_events(Some(true))
1010 .bracketed_paste(Some(true))
1011 .osc52_clipboard(Some(true));
1012 let result = over.apply_to(base);
1013 assert!(result.kitty_keyboard);
1014 assert!(result.focus_events);
1015 assert!(result.bracketed_paste);
1016 assert!(result.osc52_clipboard);
1017 }
1018
1019 #[test]
1020 fn current_capabilities_with_base_composes_stack() {
1021 clear_all_overrides();
1022 let base = TerminalCapabilities::dumb();
1023
1024 let _g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1025 let _g2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1026
1027 let caps = current_capabilities_with_base(base);
1028 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1029 assert!(caps.mouse_sgr);
1030
1031 clear_all_overrides();
1032 }
1033
1034 #[test]
1035 fn override_clone() {
1036 let over = CapabilityOverride::new()
1037 .color_depth(Some(ColorDepth::TrueColor))
1038 .in_tmux(Some(false));
1039 let cloned = over.clone();
1040 assert_eq!(over.color_depth, cloned.color_depth);
1041 assert_eq!(over.in_tmux, cloned.in_tmux);
1042 }
1043
1044 #[test]
1047 fn is_empty_false_for_color_depth() {
1048 assert!(
1049 !CapabilityOverride::new()
1050 .color_depth(Some(ColorDepth::Ansi256))
1051 .is_empty()
1052 );
1053 }
1054
1055 #[test]
1056 fn is_empty_false_for_unicode_box_drawing() {
1057 assert!(
1058 !CapabilityOverride::new()
1059 .unicode_box_drawing(Some(false))
1060 .is_empty()
1061 );
1062 }
1063
1064 #[test]
1065 fn is_empty_false_for_unicode_emoji() {
1066 assert!(
1067 !CapabilityOverride::new()
1068 .unicode_emoji(Some(true))
1069 .is_empty()
1070 );
1071 }
1072
1073 #[test]
1074 fn is_empty_false_for_double_width() {
1075 assert!(
1076 !CapabilityOverride::new()
1077 .double_width(Some(true))
1078 .is_empty()
1079 );
1080 }
1081
1082 #[test]
1083 fn is_empty_false_for_sync_output() {
1084 assert!(
1085 !CapabilityOverride::new()
1086 .sync_output(Some(false))
1087 .is_empty()
1088 );
1089 }
1090
1091 #[test]
1092 fn is_empty_false_for_osc8_hyperlinks() {
1093 assert!(
1094 !CapabilityOverride::new()
1095 .osc8_hyperlinks(Some(true))
1096 .is_empty()
1097 );
1098 }
1099
1100 #[test]
1101 fn is_empty_false_for_scroll_region() {
1102 assert!(
1103 !CapabilityOverride::new()
1104 .scroll_region(Some(true))
1105 .is_empty()
1106 );
1107 }
1108
1109 #[test]
1110 fn is_empty_false_for_in_tmux() {
1111 assert!(!CapabilityOverride::new().in_tmux(Some(true)).is_empty());
1112 }
1113
1114 #[test]
1115 fn is_empty_false_for_in_screen() {
1116 assert!(!CapabilityOverride::new().in_screen(Some(true)).is_empty());
1117 }
1118
1119 #[test]
1120 fn is_empty_false_for_in_zellij() {
1121 assert!(!CapabilityOverride::new().in_zellij(Some(true)).is_empty());
1122 }
1123
1124 #[test]
1125 fn is_empty_false_for_kitty_keyboard() {
1126 assert!(
1127 !CapabilityOverride::new()
1128 .kitty_keyboard(Some(true))
1129 .is_empty()
1130 );
1131 }
1132
1133 #[test]
1134 fn is_empty_false_for_focus_events() {
1135 assert!(
1136 !CapabilityOverride::new()
1137 .focus_events(Some(false))
1138 .is_empty()
1139 );
1140 }
1141
1142 #[test]
1143 fn is_empty_false_for_bracketed_paste() {
1144 assert!(
1145 !CapabilityOverride::new()
1146 .bracketed_paste(Some(true))
1147 .is_empty()
1148 );
1149 }
1150
1151 #[test]
1152 fn is_empty_false_for_mouse_sgr() {
1153 assert!(!CapabilityOverride::new().mouse_sgr(Some(true)).is_empty());
1154 }
1155
1156 #[test]
1157 fn is_empty_false_for_osc52_clipboard() {
1158 assert!(
1159 !CapabilityOverride::new()
1160 .osc52_clipboard(Some(false))
1161 .is_empty()
1162 );
1163 }
1164
1165 #[test]
1168 fn apply_to_covers_unicode_emoji() {
1169 let base = TerminalCapabilities::dumb();
1170 let result = CapabilityOverride::new()
1171 .unicode_emoji(Some(true))
1172 .apply_to(base);
1173 assert!(result.unicode_emoji);
1174 }
1175
1176 #[test]
1177 fn apply_to_covers_double_width() {
1178 let base = TerminalCapabilities::dumb();
1179 let result = CapabilityOverride::new()
1180 .double_width(Some(true))
1181 .apply_to(base);
1182 assert!(result.double_width);
1183 }
1184
1185 #[test]
1186 fn apply_to_covers_sync_output() {
1187 let base = TerminalCapabilities::dumb();
1188 let result = CapabilityOverride::new()
1189 .sync_output(Some(true))
1190 .apply_to(base);
1191 assert!(result.sync_output);
1192 }
1193
1194 #[test]
1195 fn apply_to_covers_osc8_hyperlinks() {
1196 let base = TerminalCapabilities::dumb();
1197 let result = CapabilityOverride::new()
1198 .osc8_hyperlinks(Some(true))
1199 .apply_to(base);
1200 assert!(result.osc8_hyperlinks);
1201 }
1202
1203 #[test]
1204 fn apply_to_covers_scroll_region() {
1205 let base = TerminalCapabilities::dumb();
1206 let result = CapabilityOverride::new()
1207 .scroll_region(Some(true))
1208 .apply_to(base);
1209 assert!(result.scroll_region);
1210 }
1211
1212 #[test]
1213 fn apply_to_covers_mouse_sgr() {
1214 let base = TerminalCapabilities::dumb();
1215 let result = CapabilityOverride::new()
1216 .mouse_sgr(Some(true))
1217 .apply_to(base);
1218 assert!(result.mouse_sgr);
1219 }
1220
1221 #[test]
1224 fn dumb_override_disables_all_on_modern_base() {
1225 let base = TerminalCapabilities::modern();
1226 let result = CapabilityOverride::dumb().apply_to(base);
1227 assert_eq!(result.color_depth, ColorDepth::Mono);
1228 assert!(!result.unicode_box_drawing);
1229 assert!(!result.unicode_emoji);
1230 assert!(!result.double_width);
1231 assert!(!result.sync_output);
1232 assert!(!result.osc8_hyperlinks);
1233 assert!(!result.scroll_region);
1234 assert!(!result.in_tmux);
1235 assert!(!result.in_screen);
1236 assert!(!result.in_zellij);
1237 assert!(!result.kitty_keyboard);
1238 assert!(!result.focus_events);
1239 assert!(!result.bracketed_paste);
1240 assert!(!result.mouse_sgr);
1241 assert!(!result.osc52_clipboard);
1242 }
1243
1244 #[test]
1245 fn modern_override_enables_features_on_dumb_base() {
1246 let base = TerminalCapabilities::dumb();
1247 let result = CapabilityOverride::modern().apply_to(base);
1248 assert_eq!(result.color_depth, ColorDepth::TrueColor);
1249 assert!(result.unicode_box_drawing);
1250 assert!(result.unicode_emoji);
1251 assert!(result.double_width);
1252 assert!(result.sync_output);
1253 assert!(result.osc8_hyperlinks);
1254 assert!(result.scroll_region);
1255 assert!(!result.in_tmux);
1257 assert!(!result.in_screen);
1258 assert!(!result.in_zellij);
1259 assert!(result.kitty_keyboard);
1260 assert!(result.focus_events);
1261 assert!(result.bracketed_paste);
1262 assert!(result.mouse_sgr);
1263 assert!(result.osc52_clipboard);
1264 }
1265
1266 #[test]
1269 fn tmux_leaves_depth_and_unrelated_fields_unset() {
1270 let over = CapabilityOverride::tmux();
1271 assert_eq!(over.color_depth, None);
1272 assert!(over.unicode_box_drawing.is_none());
1273 assert!(over.unicode_emoji.is_none());
1274 assert!(over.double_width.is_none());
1275 }
1276
1277 #[test]
1280 fn builder_in_tmux_individually() {
1281 let over = CapabilityOverride::new().in_tmux(Some(true));
1282 assert_eq!(over.in_tmux, Some(true));
1283 assert!(over.color_depth.is_none()); }
1285
1286 #[test]
1287 fn builder_sync_output_individually() {
1288 let over = CapabilityOverride::new().sync_output(Some(false));
1289 assert_eq!(over.sync_output, Some(false));
1290 assert!(over.color_depth.is_none());
1291 }
1292
1293 #[test]
1296 fn builder_overwrite_field_to_none() {
1297 let over = CapabilityOverride::new()
1298 .color_depth(Some(ColorDepth::TrueColor))
1299 .color_depth(None);
1300 assert!(over.color_depth.is_none());
1301 assert!(over.is_empty());
1302 }
1303
1304 #[test]
1305 fn builder_overwrite_dumb_field_to_none() {
1306 let over = CapabilityOverride::dumb().color_depth(None);
1307 assert!(over.color_depth.is_none());
1308 assert!(!over.is_empty()); }
1310
1311 #[test]
1314 fn guard_drop_after_clear_all_is_noop() {
1315 clear_all_overrides();
1316
1317 let guard = push_override(CapabilityOverride::dumb());
1318 assert_eq!(override_depth(), 1);
1319
1320 clear_all_overrides();
1321 assert_eq!(override_depth(), 0);
1322
1323 drop(guard);
1325 assert_eq!(override_depth(), 0);
1326 }
1327
1328 #[test]
1329 fn multiple_guards_drop_after_clear_all() {
1330 clear_all_overrides();
1331
1332 let g1 = push_override(CapabilityOverride::dumb());
1333 let g2 = push_override(CapabilityOverride::modern());
1334 assert_eq!(override_depth(), 2);
1335
1336 clear_all_overrides();
1337 assert_eq!(override_depth(), 0);
1338
1339 drop(g2);
1341 drop(g1);
1342 assert_eq!(override_depth(), 0);
1343 }
1344
1345 #[test]
1348 fn three_level_nesting_innermost_wins() {
1349 clear_all_overrides();
1350
1351 let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1352 let _l2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1353 let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1354
1355 assert_eq!(override_depth(), 3);
1356 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1357 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1358
1359 clear_all_overrides();
1360 }
1361
1362 #[test]
1363 fn three_level_nesting_partial_overrides() {
1364 clear_all_overrides();
1365
1366 let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1367 let _l2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1368 let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Ansi256)));
1369
1370 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1371 assert_eq!(caps.color_depth, ColorDepth::Ansi256); assert!(caps.mouse_sgr); assert!(!caps.sync_output); clear_all_overrides();
1376 }
1377
1378 #[test]
1381 fn with_overrides_from_applies_stack() {
1382 clear_all_overrides();
1383
1384 let base = TerminalCapabilities::dumb();
1385 let _g = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1386
1387 let caps = base.with_overrides_from(base);
1389 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1390
1391 clear_all_overrides();
1392 }
1393
1394 #[test]
1395 fn with_overrides_from_without_active_overrides() {
1396 clear_all_overrides();
1397
1398 let base = TerminalCapabilities::modern();
1399 let caps = base.with_overrides_from(base);
1400 assert_eq!(caps.color_depth, base.color_depth);
1402 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1403 }
1404
1405 #[test]
1408 fn with_capability_override_cleans_up_on_panic() {
1409 clear_all_overrides();
1410
1411 let result = std::panic::catch_unwind(|| {
1412 with_capability_override(CapabilityOverride::dumb(), || {
1413 assert!(has_active_overrides());
1414 panic!("deliberate panic");
1415 });
1416 });
1417
1418 assert!(result.is_err());
1419 assert!(!has_active_overrides());
1421 assert_eq!(override_depth(), 0);
1422 }
1423
1424 #[test]
1427 fn debug_format_contains_field_names() {
1428 let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
1429 let dbg = format!("{over:?}");
1430 assert!(dbg.contains("color_depth"));
1431 assert!(dbg.contains("TrueColor"));
1432 }
1433
1434 #[test]
1435 fn debug_format_empty_override() {
1436 let over = CapabilityOverride::new();
1437 let dbg = format!("{over:?}");
1438 assert!(dbg.contains("CapabilityOverride"));
1439 assert!(dbg.contains("None"));
1440 }
1441
1442 #[test]
1445 fn clear_all_then_push_resumes_normally() {
1446 clear_all_overrides();
1447
1448 let _g1 = push_override(CapabilityOverride::dumb());
1449 clear_all_overrides();
1450 assert_eq!(override_depth(), 0);
1451
1452 let _g2 = push_override(CapabilityOverride::modern());
1454 assert_eq!(override_depth(), 1);
1455 assert!(has_active_overrides());
1456
1457 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1458 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1459
1460 clear_all_overrides();
1461 }
1462
1463 #[test]
1466 fn current_capabilities_uses_detect_as_base() {
1467 clear_all_overrides();
1468
1469 let _g = push_override(CapabilityOverride::dumb());
1471 let caps = current_capabilities();
1472 assert_eq!(caps.color_depth, ColorDepth::Mono);
1473 assert!(!caps.mouse_sgr);
1474
1475 clear_all_overrides();
1476 }
1477
1478 #[test]
1481 fn with_overrides_integrates_full_stack() {
1482 clear_all_overrides();
1483
1484 let _g = push_override(CapabilityOverride::modern());
1485 let caps = TerminalCapabilities::with_overrides();
1486 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1487 assert!(caps.kitty_keyboard);
1488 assert!(!caps.in_tmux); clear_all_overrides();
1491 }
1492
1493 #[test]
1496 fn second_guard_dropped_first_still_active() {
1497 clear_all_overrides();
1498
1499 let g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1500 let g2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1501
1502 drop(g2);
1504 assert_eq!(override_depth(), 1);
1505 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1506 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1507
1508 drop(g1);
1509 assert_eq!(override_depth(), 0);
1510 }
1511
1512 #[test]
1515 fn with_capability_override_returns_string() {
1516 clear_all_overrides();
1517
1518 let val = with_capability_override(CapabilityOverride::dumb(), || {
1519 String::from("computed value")
1520 });
1521 assert_eq!(val, "computed value");
1522 }
1523
1524 #[test]
1525 fn with_capability_override_returns_tuple() {
1526 clear_all_overrides();
1527
1528 let (a, b) = with_capability_override(CapabilityOverride::modern(), || {
1529 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1530 (caps.supports_true_color(), caps.mouse_sgr)
1531 });
1532 assert!(a);
1533 assert!(b);
1534 }
1535
1536 #[test]
1539 fn apply_to_disables_on_modern_base() {
1540 let base = TerminalCapabilities::modern();
1541 let result = CapabilityOverride::new()
1542 .color_depth(Some(ColorDepth::Ansi256))
1543 .kitty_keyboard(Some(false))
1544 .apply_to(base);
1545 assert_eq!(result.color_depth, ColorDepth::Ansi256);
1546 assert!(!result.kitty_keyboard);
1547 assert!(result.supports_256_colors());
1549 assert!(result.unicode_box_drawing);
1550 }
1551
1552 #[test]
1555 fn current_capabilities_with_base_no_overrides_returns_base() {
1556 clear_all_overrides();
1557
1558 let base = TerminalCapabilities::modern();
1559 let caps = current_capabilities_with_base(base);
1560 assert_eq!(caps.color_depth, base.color_depth);
1561 assert_eq!(caps.unicode_box_drawing, base.unicode_box_drawing);
1562 assert_eq!(caps.unicode_emoji, base.unicode_emoji);
1563 assert_eq!(caps.double_width, base.double_width);
1564 assert_eq!(caps.sync_output, base.sync_output);
1565 assert_eq!(caps.osc8_hyperlinks, base.osc8_hyperlinks);
1566 assert_eq!(caps.scroll_region, base.scroll_region);
1567 assert_eq!(caps.in_tmux, base.in_tmux);
1568 assert_eq!(caps.in_screen, base.in_screen);
1569 assert_eq!(caps.in_zellij, base.in_zellij);
1570 assert_eq!(caps.kitty_keyboard, base.kitty_keyboard);
1571 assert_eq!(caps.focus_events, base.focus_events);
1572 assert_eq!(caps.bracketed_paste, base.bracketed_paste);
1573 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1574 assert_eq!(caps.osc52_clipboard, base.osc52_clipboard);
1575 }
1576}