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
504impl TerminalCapabilities {
509 #[must_use]
514 pub fn with_overrides() -> Self {
515 current_capabilities()
516 }
517
518 #[must_use]
520 pub fn with_overrides_from(self, base: Self) -> Self {
521 current_capabilities_with_base(base)
522 }
523}
524
525#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn override_new_is_empty() {
535 let over = CapabilityOverride::new();
536 assert!(over.is_empty());
537 }
538
539 #[test]
540 fn override_dumb_disables_all() {
541 let over = CapabilityOverride::dumb();
542 assert!(!over.is_empty());
543 assert_eq!(over.color_depth, Some(ColorDepth::Mono));
544 assert_eq!(over.sync_output, Some(false));
545 assert_eq!(over.mouse_sgr, Some(false));
546 }
547
548 #[test]
549 fn override_modern_enables_all() {
550 let over = CapabilityOverride::modern();
551 assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
552 assert_eq!(over.sync_output, Some(true));
553 assert_eq!(over.kitty_keyboard, Some(true));
554 assert_eq!(over.in_tmux, Some(false));
556 }
557
558 #[test]
559 fn override_tmux_sets_mux() {
560 let over = CapabilityOverride::tmux();
561 assert_eq!(over.in_tmux, Some(true));
562 assert_eq!(over.sync_output, Some(false));
563 assert_eq!(over.osc52_clipboard, Some(false));
564 }
565
566 #[test]
567 fn override_builder_chain() {
568 let over = CapabilityOverride::new()
569 .color_depth(Some(ColorDepth::TrueColor))
570 .unicode_box_drawing(Some(false))
571 .mouse_sgr(Some(false));
572
573 assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
574 assert_eq!(over.unicode_box_drawing, Some(false));
575 assert_eq!(over.mouse_sgr, Some(false));
576 assert!(over.sync_output.is_none());
577 }
578
579 #[test]
580 fn apply_to_overrides_caps() {
581 let base = TerminalCapabilities::dumb();
582 let over = CapabilityOverride::new()
583 .color_depth(Some(ColorDepth::TrueColor))
584 .unicode_box_drawing(Some(true));
585
586 let result = over.apply_to(base);
587 assert_eq!(result.color_depth, ColorDepth::TrueColor);
588 assert!(result.unicode_box_drawing);
589 assert!(!result.mouse_sgr);
591 }
592
593 #[test]
594 fn apply_to_none_keeps_original() {
595 let base = TerminalCapabilities::modern();
596 let over = CapabilityOverride::new(); let result = over.apply_to(base);
599 assert_eq!(result.color_depth, base.color_depth);
600 assert_eq!(result.mouse_sgr, base.mouse_sgr);
601 }
602
603 #[test]
604 fn push_pop_override() {
605 clear_all_overrides();
606 assert!(!has_active_overrides());
607 assert_eq!(override_depth(), 0);
608
609 {
610 let _guard = push_override(CapabilityOverride::dumb());
611 assert!(has_active_overrides());
612 assert_eq!(override_depth(), 1);
613 }
614
615 assert!(!has_active_overrides());
616 assert_eq!(override_depth(), 0);
617 }
618
619 #[test]
620 fn nested_overrides() {
621 clear_all_overrides();
622
623 {
624 let _outer = push_override(
625 CapabilityOverride::new()
626 .color_depth(Some(ColorDepth::TrueColor))
627 .mouse_sgr(Some(true)),
628 );
629 assert_eq!(override_depth(), 1);
630
631 {
632 let _inner =
633 push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
634 assert_eq!(override_depth(), 2);
635
636 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
638 assert_eq!(caps.color_depth, ColorDepth::Mono);
639 assert!(caps.mouse_sgr); }
641
642 assert_eq!(override_depth(), 1);
644 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
645 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
646 }
647
648 assert_eq!(override_depth(), 0);
649 }
650
651 #[test]
652 fn with_capability_override_scope() {
653 clear_all_overrides();
654
655 let result = with_capability_override(CapabilityOverride::modern(), || {
656 assert!(has_active_overrides());
657 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
658 caps.supports_true_color()
659 });
660
661 assert!(result);
662 assert!(!has_active_overrides());
663 }
664
665 #[test]
666 fn with_capability_override_nested() {
667 clear_all_overrides();
668
669 with_capability_override(
670 CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)),
671 || {
672 with_capability_override(CapabilityOverride::new().mouse_sgr(Some(false)), || {
673 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
674 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
675 assert!(!caps.mouse_sgr);
676 });
677 },
678 );
679 }
680
681 #[test]
682 fn with_overrides_method() {
683 clear_all_overrides();
684
685 with_capability_override(CapabilityOverride::dumb(), || {
686 let caps = TerminalCapabilities::with_overrides();
687 assert_eq!(caps.color_depth, ColorDepth::Mono);
688 assert!(!caps.unicode_box_drawing);
689 assert!(!caps.unicode_emoji);
690 assert!(!caps.double_width);
691 });
692 }
693
694 #[test]
695 fn clear_all_overrides_works() {
696 let _g1 = push_override(CapabilityOverride::dumb());
697 let _g2 = push_override(CapabilityOverride::modern());
698 assert_eq!(override_depth(), 2);
699
700 clear_all_overrides();
701 assert_eq!(override_depth(), 0);
702 }
703
704 #[test]
705 fn default_override_is_empty() {
706 let over = CapabilityOverride::default();
707 assert!(over.is_empty());
708 }
709
710 #[test]
711 fn is_empty_false_for_single_override() {
712 let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
713 assert!(!over.is_empty());
714 }
715
716 #[test]
717 fn dumb_disables_all_fields() {
718 let over = CapabilityOverride::dumb();
719 assert_eq!(over.unicode_box_drawing, Some(false));
720 assert_eq!(over.unicode_emoji, Some(false));
721 assert_eq!(over.double_width, Some(false));
722 assert_eq!(over.osc8_hyperlinks, Some(false));
723 assert_eq!(over.scroll_region, Some(false));
724 assert_eq!(over.kitty_keyboard, Some(false));
725 assert_eq!(over.focus_events, Some(false));
726 assert_eq!(over.bracketed_paste, Some(false));
727 assert_eq!(over.osc52_clipboard, Some(false));
728 assert_eq!(over.in_tmux, Some(false));
729 assert_eq!(over.in_screen, Some(false));
730 assert_eq!(over.in_zellij, Some(false));
731 }
732
733 #[test]
734 fn modern_enables_features_disables_mux() {
735 let over = CapabilityOverride::modern();
736 assert_eq!(over.unicode_box_drawing, Some(true));
737 assert_eq!(over.unicode_emoji, Some(true));
738 assert_eq!(over.double_width, Some(true));
739 assert_eq!(over.osc8_hyperlinks, Some(true));
740 assert_eq!(over.scroll_region, Some(true));
741 assert_eq!(over.focus_events, Some(true));
742 assert_eq!(over.bracketed_paste, Some(true));
743 assert_eq!(over.osc52_clipboard, Some(true));
744 assert_eq!(over.in_screen, Some(false));
745 assert_eq!(over.in_zellij, Some(false));
746 }
747
748 #[test]
749 fn tmux_preserves_color_depth_and_sets_bracketed_paste() {
750 let over = CapabilityOverride::tmux();
751 assert_eq!(over.color_depth, None);
752 assert_eq!(over.bracketed_paste, Some(true));
753 assert_eq!(over.mouse_sgr, Some(true));
754 assert_eq!(over.scroll_region, Some(true));
755 assert_eq!(over.kitty_keyboard, Some(false));
756 }
757
758 #[test]
759 fn builder_all_optional_features() {
760 let over = CapabilityOverride::new()
761 .unicode_emoji(Some(true))
762 .double_width(Some(false))
763 .in_screen(Some(true))
764 .in_zellij(Some(true))
765 .osc8_hyperlinks(Some(true))
766 .osc52_clipboard(Some(false))
767 .scroll_region(Some(true))
768 .focus_events(Some(true))
769 .bracketed_paste(Some(false))
770 .kitty_keyboard(Some(true));
771
772 assert_eq!(over.unicode_emoji, Some(true));
773 assert_eq!(over.double_width, Some(false));
774 assert_eq!(over.in_screen, Some(true));
775 assert_eq!(over.in_zellij, Some(true));
776 assert_eq!(over.osc8_hyperlinks, Some(true));
777 assert_eq!(over.osc52_clipboard, Some(false));
778 assert_eq!(over.scroll_region, Some(true));
779 assert_eq!(over.focus_events, Some(true));
780 assert_eq!(over.bracketed_paste, Some(false));
781 assert_eq!(over.kitty_keyboard, Some(true));
782 }
783
784 #[test]
785 fn apply_to_covers_all_mux_flags() {
786 let base = TerminalCapabilities::dumb();
787 let over = CapabilityOverride::new()
788 .in_tmux(Some(true))
789 .in_screen(Some(true))
790 .in_zellij(Some(true))
791 .in_wezterm_mux(Some(true));
792 let result = over.apply_to(base);
793 assert!(result.in_tmux);
794 assert!(result.in_screen);
795 assert!(result.in_zellij);
796 assert!(result.in_wezterm_mux);
797 }
798
799 #[test]
800 fn modern_override_simulates_non_mux_even_under_wezterm() {
801 let mut base = TerminalCapabilities::modern();
806 base.in_wezterm_mux = true; let result = CapabilityOverride::modern().apply_to(base);
809 assert!(!result.in_wezterm_mux);
810 assert!(!result.in_any_mux());
811 assert!(result.use_sync_output());
812 assert!(result.use_scroll_region());
813 assert!(result.use_hyperlinks());
814 assert!(result.use_clipboard());
815 }
816
817 #[test]
818 fn dumb_override_clears_wezterm_mux() {
819 let mut base = TerminalCapabilities::modern();
820 base.in_wezterm_mux = true;
821 let result = CapabilityOverride::dumb().apply_to(base);
822 assert!(!result.in_wezterm_mux);
823 }
824
825 #[test]
826 fn is_empty_false_for_in_wezterm_mux() {
827 assert!(
828 !CapabilityOverride::new()
829 .in_wezterm_mux(Some(true))
830 .is_empty()
831 );
832 }
833
834 #[test]
835 fn apply_to_covers_input_features() {
836 let base = TerminalCapabilities::dumb();
837 let over = CapabilityOverride::new()
838 .kitty_keyboard(Some(true))
839 .focus_events(Some(true))
840 .bracketed_paste(Some(true))
841 .osc52_clipboard(Some(true));
842 let result = over.apply_to(base);
843 assert!(result.kitty_keyboard);
844 assert!(result.focus_events);
845 assert!(result.bracketed_paste);
846 assert!(result.osc52_clipboard);
847 }
848
849 #[test]
850 fn current_capabilities_with_base_composes_stack() {
851 clear_all_overrides();
852 let base = TerminalCapabilities::dumb();
853
854 let _g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
855 let _g2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
856
857 let caps = current_capabilities_with_base(base);
858 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
859 assert!(caps.mouse_sgr);
860
861 clear_all_overrides();
862 }
863
864 #[test]
865 fn override_clone() {
866 let over = CapabilityOverride::new()
867 .color_depth(Some(ColorDepth::TrueColor))
868 .in_tmux(Some(false));
869 let cloned = over.clone();
870 assert_eq!(over.color_depth, cloned.color_depth);
871 assert_eq!(over.in_tmux, cloned.in_tmux);
872 }
873
874 #[test]
877 fn is_empty_false_for_color_depth() {
878 assert!(
879 !CapabilityOverride::new()
880 .color_depth(Some(ColorDepth::Ansi256))
881 .is_empty()
882 );
883 }
884
885 #[test]
886 fn is_empty_false_for_unicode_box_drawing() {
887 assert!(
888 !CapabilityOverride::new()
889 .unicode_box_drawing(Some(false))
890 .is_empty()
891 );
892 }
893
894 #[test]
895 fn is_empty_false_for_unicode_emoji() {
896 assert!(
897 !CapabilityOverride::new()
898 .unicode_emoji(Some(true))
899 .is_empty()
900 );
901 }
902
903 #[test]
904 fn is_empty_false_for_double_width() {
905 assert!(
906 !CapabilityOverride::new()
907 .double_width(Some(true))
908 .is_empty()
909 );
910 }
911
912 #[test]
913 fn is_empty_false_for_sync_output() {
914 assert!(
915 !CapabilityOverride::new()
916 .sync_output(Some(false))
917 .is_empty()
918 );
919 }
920
921 #[test]
922 fn is_empty_false_for_osc8_hyperlinks() {
923 assert!(
924 !CapabilityOverride::new()
925 .osc8_hyperlinks(Some(true))
926 .is_empty()
927 );
928 }
929
930 #[test]
931 fn is_empty_false_for_scroll_region() {
932 assert!(
933 !CapabilityOverride::new()
934 .scroll_region(Some(true))
935 .is_empty()
936 );
937 }
938
939 #[test]
940 fn is_empty_false_for_in_tmux() {
941 assert!(!CapabilityOverride::new().in_tmux(Some(true)).is_empty());
942 }
943
944 #[test]
945 fn is_empty_false_for_in_screen() {
946 assert!(!CapabilityOverride::new().in_screen(Some(true)).is_empty());
947 }
948
949 #[test]
950 fn is_empty_false_for_in_zellij() {
951 assert!(!CapabilityOverride::new().in_zellij(Some(true)).is_empty());
952 }
953
954 #[test]
955 fn is_empty_false_for_kitty_keyboard() {
956 assert!(
957 !CapabilityOverride::new()
958 .kitty_keyboard(Some(true))
959 .is_empty()
960 );
961 }
962
963 #[test]
964 fn is_empty_false_for_focus_events() {
965 assert!(
966 !CapabilityOverride::new()
967 .focus_events(Some(false))
968 .is_empty()
969 );
970 }
971
972 #[test]
973 fn is_empty_false_for_bracketed_paste() {
974 assert!(
975 !CapabilityOverride::new()
976 .bracketed_paste(Some(true))
977 .is_empty()
978 );
979 }
980
981 #[test]
982 fn is_empty_false_for_mouse_sgr() {
983 assert!(!CapabilityOverride::new().mouse_sgr(Some(true)).is_empty());
984 }
985
986 #[test]
987 fn is_empty_false_for_osc52_clipboard() {
988 assert!(
989 !CapabilityOverride::new()
990 .osc52_clipboard(Some(false))
991 .is_empty()
992 );
993 }
994
995 #[test]
998 fn apply_to_covers_unicode_emoji() {
999 let base = TerminalCapabilities::dumb();
1000 let result = CapabilityOverride::new()
1001 .unicode_emoji(Some(true))
1002 .apply_to(base);
1003 assert!(result.unicode_emoji);
1004 }
1005
1006 #[test]
1007 fn apply_to_covers_double_width() {
1008 let base = TerminalCapabilities::dumb();
1009 let result = CapabilityOverride::new()
1010 .double_width(Some(true))
1011 .apply_to(base);
1012 assert!(result.double_width);
1013 }
1014
1015 #[test]
1016 fn apply_to_covers_sync_output() {
1017 let base = TerminalCapabilities::dumb();
1018 let result = CapabilityOverride::new()
1019 .sync_output(Some(true))
1020 .apply_to(base);
1021 assert!(result.sync_output);
1022 }
1023
1024 #[test]
1025 fn apply_to_covers_osc8_hyperlinks() {
1026 let base = TerminalCapabilities::dumb();
1027 let result = CapabilityOverride::new()
1028 .osc8_hyperlinks(Some(true))
1029 .apply_to(base);
1030 assert!(result.osc8_hyperlinks);
1031 }
1032
1033 #[test]
1034 fn apply_to_covers_scroll_region() {
1035 let base = TerminalCapabilities::dumb();
1036 let result = CapabilityOverride::new()
1037 .scroll_region(Some(true))
1038 .apply_to(base);
1039 assert!(result.scroll_region);
1040 }
1041
1042 #[test]
1043 fn apply_to_covers_mouse_sgr() {
1044 let base = TerminalCapabilities::dumb();
1045 let result = CapabilityOverride::new()
1046 .mouse_sgr(Some(true))
1047 .apply_to(base);
1048 assert!(result.mouse_sgr);
1049 }
1050
1051 #[test]
1054 fn dumb_override_disables_all_on_modern_base() {
1055 let base = TerminalCapabilities::modern();
1056 let result = CapabilityOverride::dumb().apply_to(base);
1057 assert_eq!(result.color_depth, ColorDepth::Mono);
1058 assert!(!result.unicode_box_drawing);
1059 assert!(!result.unicode_emoji);
1060 assert!(!result.double_width);
1061 assert!(!result.sync_output);
1062 assert!(!result.osc8_hyperlinks);
1063 assert!(!result.scroll_region);
1064 assert!(!result.in_tmux);
1065 assert!(!result.in_screen);
1066 assert!(!result.in_zellij);
1067 assert!(!result.kitty_keyboard);
1068 assert!(!result.focus_events);
1069 assert!(!result.bracketed_paste);
1070 assert!(!result.mouse_sgr);
1071 assert!(!result.osc52_clipboard);
1072 }
1073
1074 #[test]
1075 fn modern_override_enables_features_on_dumb_base() {
1076 let base = TerminalCapabilities::dumb();
1077 let result = CapabilityOverride::modern().apply_to(base);
1078 assert_eq!(result.color_depth, ColorDepth::TrueColor);
1079 assert!(result.unicode_box_drawing);
1080 assert!(result.unicode_emoji);
1081 assert!(result.double_width);
1082 assert!(result.sync_output);
1083 assert!(result.osc8_hyperlinks);
1084 assert!(result.scroll_region);
1085 assert!(!result.in_tmux);
1087 assert!(!result.in_screen);
1088 assert!(!result.in_zellij);
1089 assert!(result.kitty_keyboard);
1090 assert!(result.focus_events);
1091 assert!(result.bracketed_paste);
1092 assert!(result.mouse_sgr);
1093 assert!(result.osc52_clipboard);
1094 }
1095
1096 #[test]
1099 fn tmux_leaves_depth_and_unrelated_fields_unset() {
1100 let over = CapabilityOverride::tmux();
1101 assert_eq!(over.color_depth, None);
1102 assert!(over.unicode_box_drawing.is_none());
1103 assert!(over.unicode_emoji.is_none());
1104 assert!(over.double_width.is_none());
1105 }
1106
1107 #[test]
1110 fn builder_in_tmux_individually() {
1111 let over = CapabilityOverride::new().in_tmux(Some(true));
1112 assert_eq!(over.in_tmux, Some(true));
1113 assert!(over.color_depth.is_none()); }
1115
1116 #[test]
1117 fn builder_sync_output_individually() {
1118 let over = CapabilityOverride::new().sync_output(Some(false));
1119 assert_eq!(over.sync_output, Some(false));
1120 assert!(over.color_depth.is_none());
1121 }
1122
1123 #[test]
1126 fn builder_overwrite_field_to_none() {
1127 let over = CapabilityOverride::new()
1128 .color_depth(Some(ColorDepth::TrueColor))
1129 .color_depth(None);
1130 assert!(over.color_depth.is_none());
1131 assert!(over.is_empty());
1132 }
1133
1134 #[test]
1135 fn builder_overwrite_dumb_field_to_none() {
1136 let over = CapabilityOverride::dumb().color_depth(None);
1137 assert!(over.color_depth.is_none());
1138 assert!(!over.is_empty()); }
1140
1141 #[test]
1144 fn guard_drop_after_clear_all_is_noop() {
1145 clear_all_overrides();
1146
1147 let guard = push_override(CapabilityOverride::dumb());
1148 assert_eq!(override_depth(), 1);
1149
1150 clear_all_overrides();
1151 assert_eq!(override_depth(), 0);
1152
1153 drop(guard);
1155 assert_eq!(override_depth(), 0);
1156 }
1157
1158 #[test]
1159 fn multiple_guards_drop_after_clear_all() {
1160 clear_all_overrides();
1161
1162 let g1 = push_override(CapabilityOverride::dumb());
1163 let g2 = push_override(CapabilityOverride::modern());
1164 assert_eq!(override_depth(), 2);
1165
1166 clear_all_overrides();
1167 assert_eq!(override_depth(), 0);
1168
1169 drop(g2);
1171 drop(g1);
1172 assert_eq!(override_depth(), 0);
1173 }
1174
1175 #[test]
1178 fn three_level_nesting_innermost_wins() {
1179 clear_all_overrides();
1180
1181 let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1182 let _l2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1183 let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1184
1185 assert_eq!(override_depth(), 3);
1186 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1187 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1188
1189 clear_all_overrides();
1190 }
1191
1192 #[test]
1193 fn three_level_nesting_partial_overrides() {
1194 clear_all_overrides();
1195
1196 let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1197 let _l2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1198 let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Ansi256)));
1199
1200 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1201 assert_eq!(caps.color_depth, ColorDepth::Ansi256); assert!(caps.mouse_sgr); assert!(!caps.sync_output); clear_all_overrides();
1206 }
1207
1208 #[test]
1211 fn with_overrides_from_applies_stack() {
1212 clear_all_overrides();
1213
1214 let base = TerminalCapabilities::dumb();
1215 let _g = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1216
1217 let caps = base.with_overrides_from(base);
1219 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1220
1221 clear_all_overrides();
1222 }
1223
1224 #[test]
1225 fn with_overrides_from_without_active_overrides() {
1226 clear_all_overrides();
1227
1228 let base = TerminalCapabilities::modern();
1229 let caps = base.with_overrides_from(base);
1230 assert_eq!(caps.color_depth, base.color_depth);
1232 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1233 }
1234
1235 #[test]
1238 fn with_capability_override_cleans_up_on_panic() {
1239 clear_all_overrides();
1240
1241 let result = std::panic::catch_unwind(|| {
1242 with_capability_override(CapabilityOverride::dumb(), || {
1243 assert!(has_active_overrides());
1244 panic!("deliberate panic");
1245 });
1246 });
1247
1248 assert!(result.is_err());
1249 assert!(!has_active_overrides());
1251 assert_eq!(override_depth(), 0);
1252 }
1253
1254 #[test]
1257 fn debug_format_contains_field_names() {
1258 let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
1259 let dbg = format!("{over:?}");
1260 assert!(dbg.contains("color_depth"));
1261 assert!(dbg.contains("TrueColor"));
1262 }
1263
1264 #[test]
1265 fn debug_format_empty_override() {
1266 let over = CapabilityOverride::new();
1267 let dbg = format!("{over:?}");
1268 assert!(dbg.contains("CapabilityOverride"));
1269 assert!(dbg.contains("None"));
1270 }
1271
1272 #[test]
1275 fn clear_all_then_push_resumes_normally() {
1276 clear_all_overrides();
1277
1278 let _g1 = push_override(CapabilityOverride::dumb());
1279 clear_all_overrides();
1280 assert_eq!(override_depth(), 0);
1281
1282 let _g2 = push_override(CapabilityOverride::modern());
1284 assert_eq!(override_depth(), 1);
1285 assert!(has_active_overrides());
1286
1287 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1288 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1289
1290 clear_all_overrides();
1291 }
1292
1293 #[test]
1296 fn current_capabilities_uses_detect_as_base() {
1297 clear_all_overrides();
1298
1299 let _g = push_override(CapabilityOverride::dumb());
1301 let caps = current_capabilities();
1302 assert_eq!(caps.color_depth, ColorDepth::Mono);
1303 assert!(!caps.mouse_sgr);
1304
1305 clear_all_overrides();
1306 }
1307
1308 #[test]
1311 fn with_overrides_integrates_full_stack() {
1312 clear_all_overrides();
1313
1314 let _g = push_override(CapabilityOverride::modern());
1315 let caps = TerminalCapabilities::with_overrides();
1316 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1317 assert!(caps.kitty_keyboard);
1318 assert!(!caps.in_tmux); clear_all_overrides();
1321 }
1322
1323 #[test]
1326 fn second_guard_dropped_first_still_active() {
1327 clear_all_overrides();
1328
1329 let g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1330 let g2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1331
1332 drop(g2);
1334 assert_eq!(override_depth(), 1);
1335 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1336 assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1337
1338 drop(g1);
1339 assert_eq!(override_depth(), 0);
1340 }
1341
1342 #[test]
1345 fn with_capability_override_returns_string() {
1346 clear_all_overrides();
1347
1348 let val = with_capability_override(CapabilityOverride::dumb(), || {
1349 String::from("computed value")
1350 });
1351 assert_eq!(val, "computed value");
1352 }
1353
1354 #[test]
1355 fn with_capability_override_returns_tuple() {
1356 clear_all_overrides();
1357
1358 let (a, b) = with_capability_override(CapabilityOverride::modern(), || {
1359 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1360 (caps.supports_true_color(), caps.mouse_sgr)
1361 });
1362 assert!(a);
1363 assert!(b);
1364 }
1365
1366 #[test]
1369 fn apply_to_disables_on_modern_base() {
1370 let base = TerminalCapabilities::modern();
1371 let result = CapabilityOverride::new()
1372 .color_depth(Some(ColorDepth::Ansi256))
1373 .kitty_keyboard(Some(false))
1374 .apply_to(base);
1375 assert_eq!(result.color_depth, ColorDepth::Ansi256);
1376 assert!(!result.kitty_keyboard);
1377 assert!(result.supports_256_colors());
1379 assert!(result.unicode_box_drawing);
1380 }
1381
1382 #[test]
1385 fn current_capabilities_with_base_no_overrides_returns_base() {
1386 clear_all_overrides();
1387
1388 let base = TerminalCapabilities::modern();
1389 let caps = current_capabilities_with_base(base);
1390 assert_eq!(caps.color_depth, base.color_depth);
1391 assert_eq!(caps.unicode_box_drawing, base.unicode_box_drawing);
1392 assert_eq!(caps.unicode_emoji, base.unicode_emoji);
1393 assert_eq!(caps.double_width, base.double_width);
1394 assert_eq!(caps.sync_output, base.sync_output);
1395 assert_eq!(caps.osc8_hyperlinks, base.osc8_hyperlinks);
1396 assert_eq!(caps.scroll_region, base.scroll_region);
1397 assert_eq!(caps.in_tmux, base.in_tmux);
1398 assert_eq!(caps.in_screen, base.in_screen);
1399 assert_eq!(caps.in_zellij, base.in_zellij);
1400 assert_eq!(caps.kitty_keyboard, base.kitty_keyboard);
1401 assert_eq!(caps.focus_events, base.focus_events);
1402 assert_eq!(caps.bracketed_paste, base.bracketed_paste);
1403 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1404 assert_eq!(caps.osc52_clipboard, base.osc52_clipboard);
1405 }
1406}