1#![forbid(unsafe_code)]
2
3use crate::terminal_capabilities::TerminalCapabilities;
56use std::cell::RefCell;
57
58#[derive(Debug, Clone, Default)]
69pub struct CapabilityOverride {
70 pub true_color: Option<bool>,
72 pub colors_256: Option<bool>,
73
74 pub unicode_box_drawing: Option<bool>,
76 pub unicode_emoji: Option<bool>,
77 pub double_width: Option<bool>,
78
79 pub sync_output: Option<bool>,
81 pub osc8_hyperlinks: Option<bool>,
82 pub scroll_region: Option<bool>,
83
84 pub in_tmux: Option<bool>,
86 pub in_screen: Option<bool>,
87 pub in_zellij: Option<bool>,
88
89 pub kitty_keyboard: Option<bool>,
91 pub focus_events: Option<bool>,
92 pub bracketed_paste: Option<bool>,
93 pub mouse_sgr: Option<bool>,
94
95 pub osc52_clipboard: Option<bool>,
97}
98
99impl CapabilityOverride {
100 #[must_use]
102 pub const fn new() -> Self {
103 Self {
104 true_color: None,
105 colors_256: None,
106 unicode_box_drawing: None,
107 unicode_emoji: None,
108 double_width: None,
109 sync_output: None,
110 osc8_hyperlinks: None,
111 scroll_region: None,
112 in_tmux: None,
113 in_screen: None,
114 in_zellij: None,
115 kitty_keyboard: None,
116 focus_events: None,
117 bracketed_paste: None,
118 mouse_sgr: None,
119 osc52_clipboard: None,
120 }
121 }
122
123 #[must_use]
125 pub const fn dumb() -> Self {
126 Self {
127 true_color: Some(false),
128 colors_256: Some(false),
129 unicode_box_drawing: Some(false),
130 unicode_emoji: Some(false),
131 double_width: Some(false),
132 sync_output: Some(false),
133 osc8_hyperlinks: Some(false),
134 scroll_region: Some(false),
135 in_tmux: Some(false),
136 in_screen: Some(false),
137 in_zellij: Some(false),
138 kitty_keyboard: Some(false),
139 focus_events: Some(false),
140 bracketed_paste: Some(false),
141 mouse_sgr: Some(false),
142 osc52_clipboard: Some(false),
143 }
144 }
145
146 #[must_use]
148 pub const fn modern() -> Self {
149 Self {
150 true_color: Some(true),
151 colors_256: Some(true),
152 unicode_box_drawing: Some(true),
153 unicode_emoji: Some(true),
154 double_width: Some(true),
155 sync_output: Some(true),
156 osc8_hyperlinks: Some(true),
157 scroll_region: Some(true),
158 in_tmux: Some(false),
159 in_screen: Some(false),
160 in_zellij: Some(false),
161 kitty_keyboard: Some(true),
162 focus_events: Some(true),
163 bracketed_paste: Some(true),
164 mouse_sgr: Some(true),
165 osc52_clipboard: Some(true),
166 }
167 }
168
169 #[must_use]
171 pub const fn tmux() -> Self {
172 Self {
173 true_color: None,
174 colors_256: Some(true),
175 unicode_box_drawing: None,
176 unicode_emoji: None,
177 double_width: None,
178 sync_output: Some(false),
179 osc8_hyperlinks: Some(false),
180 scroll_region: Some(true),
181 in_tmux: Some(true),
182 in_screen: Some(false),
183 in_zellij: Some(false),
184 kitty_keyboard: Some(false),
185 focus_events: Some(false),
186 bracketed_paste: Some(true),
187 mouse_sgr: Some(true),
188 osc52_clipboard: Some(false),
189 }
190 }
191
192 #[must_use]
196 pub const fn true_color(mut self, value: Option<bool>) -> Self {
197 self.true_color = value;
198 self
199 }
200
201 #[must_use]
203 pub const fn colors_256(mut self, value: Option<bool>) -> Self {
204 self.colors_256 = value;
205 self
206 }
207
208 #[must_use]
210 pub const fn unicode_box_drawing(mut self, value: Option<bool>) -> Self {
211 self.unicode_box_drawing = value;
212 self
213 }
214
215 #[must_use]
217 pub const fn unicode_emoji(mut self, value: Option<bool>) -> Self {
218 self.unicode_emoji = value;
219 self
220 }
221
222 #[must_use]
224 pub const fn double_width(mut self, value: Option<bool>) -> Self {
225 self.double_width = value;
226 self
227 }
228
229 #[must_use]
231 pub const fn sync_output(mut self, value: Option<bool>) -> Self {
232 self.sync_output = value;
233 self
234 }
235
236 #[must_use]
238 pub const fn osc8_hyperlinks(mut self, value: Option<bool>) -> Self {
239 self.osc8_hyperlinks = value;
240 self
241 }
242
243 #[must_use]
245 pub const fn scroll_region(mut self, value: Option<bool>) -> Self {
246 self.scroll_region = value;
247 self
248 }
249
250 #[must_use]
252 pub const fn in_tmux(mut self, value: Option<bool>) -> Self {
253 self.in_tmux = value;
254 self
255 }
256
257 #[must_use]
259 pub const fn in_screen(mut self, value: Option<bool>) -> Self {
260 self.in_screen = value;
261 self
262 }
263
264 #[must_use]
266 pub const fn in_zellij(mut self, value: Option<bool>) -> Self {
267 self.in_zellij = value;
268 self
269 }
270
271 #[must_use]
273 pub const fn kitty_keyboard(mut self, value: Option<bool>) -> Self {
274 self.kitty_keyboard = value;
275 self
276 }
277
278 #[must_use]
280 pub const fn focus_events(mut self, value: Option<bool>) -> Self {
281 self.focus_events = value;
282 self
283 }
284
285 #[must_use]
287 pub const fn bracketed_paste(mut self, value: Option<bool>) -> Self {
288 self.bracketed_paste = value;
289 self
290 }
291
292 #[must_use]
294 pub const fn mouse_sgr(mut self, value: Option<bool>) -> Self {
295 self.mouse_sgr = value;
296 self
297 }
298
299 #[must_use]
301 pub const fn osc52_clipboard(mut self, value: Option<bool>) -> Self {
302 self.osc52_clipboard = value;
303 self
304 }
305
306 #[must_use]
308 pub const fn is_empty(&self) -> bool {
309 self.true_color.is_none()
310 && self.colors_256.is_none()
311 && self.unicode_box_drawing.is_none()
312 && self.unicode_emoji.is_none()
313 && self.double_width.is_none()
314 && self.sync_output.is_none()
315 && self.osc8_hyperlinks.is_none()
316 && self.scroll_region.is_none()
317 && self.in_tmux.is_none()
318 && self.in_screen.is_none()
319 && self.in_zellij.is_none()
320 && self.kitty_keyboard.is_none()
321 && self.focus_events.is_none()
322 && self.bracketed_paste.is_none()
323 && self.mouse_sgr.is_none()
324 && self.osc52_clipboard.is_none()
325 }
326
327 #[must_use]
329 pub fn apply_to(&self, mut caps: TerminalCapabilities) -> TerminalCapabilities {
330 if let Some(v) = self.true_color {
331 caps.true_color = v;
332 }
333 if let Some(v) = self.colors_256 {
334 caps.colors_256 = v;
335 }
336 if let Some(v) = self.unicode_box_drawing {
337 caps.unicode_box_drawing = v;
338 }
339 if let Some(v) = self.unicode_emoji {
340 caps.unicode_emoji = v;
341 }
342 if let Some(v) = self.double_width {
343 caps.double_width = v;
344 }
345 if let Some(v) = self.sync_output {
346 caps.sync_output = v;
347 }
348 if let Some(v) = self.osc8_hyperlinks {
349 caps.osc8_hyperlinks = v;
350 }
351 if let Some(v) = self.scroll_region {
352 caps.scroll_region = v;
353 }
354 if let Some(v) = self.in_tmux {
355 caps.in_tmux = v;
356 }
357 if let Some(v) = self.in_screen {
358 caps.in_screen = v;
359 }
360 if let Some(v) = self.in_zellij {
361 caps.in_zellij = v;
362 }
363 if let Some(v) = self.kitty_keyboard {
364 caps.kitty_keyboard = v;
365 }
366 if let Some(v) = self.focus_events {
367 caps.focus_events = v;
368 }
369 if let Some(v) = self.bracketed_paste {
370 caps.bracketed_paste = v;
371 }
372 if let Some(v) = self.mouse_sgr {
373 caps.mouse_sgr = v;
374 }
375 if let Some(v) = self.osc52_clipboard {
376 caps.osc52_clipboard = v;
377 }
378 caps
379 }
380}
381
382thread_local! {
387 static OVERRIDE_STACK: RefCell<Vec<CapabilityOverride>> = const { RefCell::new(Vec::new()) };
389}
390
391#[must_use]
395pub struct OverrideGuard {
396 _marker: std::marker::PhantomData<*const ()>,
398}
399
400impl Drop for OverrideGuard {
401 fn drop(&mut self) {
402 OVERRIDE_STACK.with(|stack| {
405 stack.borrow_mut().pop();
406 });
407 }
408}
409
410#[must_use = "the override is removed when the guard is dropped"]
424pub fn push_override(over: CapabilityOverride) -> OverrideGuard {
425 OVERRIDE_STACK.with(|stack| {
426 stack.borrow_mut().push(over);
427 });
428 OverrideGuard {
429 _marker: std::marker::PhantomData,
430 }
431}
432
433pub fn with_capability_override<F, R>(over: CapabilityOverride, f: F) -> R
450where
451 F: FnOnce() -> R,
452{
453 let _guard = push_override(over);
454 f()
455}
456
457#[must_use]
462pub fn current_capabilities() -> TerminalCapabilities {
463 let base = TerminalCapabilities::detect();
464 current_capabilities_with_base(base)
465}
466
467#[must_use]
469pub fn current_capabilities_with_base(base: TerminalCapabilities) -> TerminalCapabilities {
470 OVERRIDE_STACK.with(|stack| {
471 let stack = stack.borrow();
472 stack.iter().fold(base, |caps, over| over.apply_to(caps))
473 })
474}
475
476#[must_use]
478pub fn has_active_overrides() -> bool {
479 OVERRIDE_STACK.with(|stack| !stack.borrow().is_empty())
480}
481
482#[must_use]
484pub fn override_depth() -> usize {
485 OVERRIDE_STACK.with(|stack| stack.borrow().len())
486}
487
488pub fn clear_all_overrides() {
493 OVERRIDE_STACK.with(|stack| {
494 stack.borrow_mut().clear();
495 });
496}
497
498impl TerminalCapabilities {
503 #[must_use]
508 pub fn with_overrides() -> Self {
509 current_capabilities()
510 }
511
512 #[must_use]
514 pub fn with_overrides_from(self, base: Self) -> Self {
515 current_capabilities_with_base(base)
516 }
517}
518
519#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn override_new_is_empty() {
529 let over = CapabilityOverride::new();
530 assert!(over.is_empty());
531 }
532
533 #[test]
534 fn override_dumb_disables_all() {
535 let over = CapabilityOverride::dumb();
536 assert!(!over.is_empty());
537 assert_eq!(over.true_color, Some(false));
538 assert_eq!(over.colors_256, Some(false));
539 assert_eq!(over.sync_output, Some(false));
540 assert_eq!(over.mouse_sgr, Some(false));
541 }
542
543 #[test]
544 fn override_modern_enables_all() {
545 let over = CapabilityOverride::modern();
546 assert_eq!(over.true_color, Some(true));
547 assert_eq!(over.colors_256, Some(true));
548 assert_eq!(over.sync_output, Some(true));
549 assert_eq!(over.kitty_keyboard, Some(true));
550 assert_eq!(over.in_tmux, Some(false));
552 }
553
554 #[test]
555 fn override_tmux_sets_mux() {
556 let over = CapabilityOverride::tmux();
557 assert_eq!(over.in_tmux, Some(true));
558 assert_eq!(over.sync_output, Some(false));
559 assert_eq!(over.osc52_clipboard, Some(false));
560 }
561
562 #[test]
563 fn override_builder_chain() {
564 let over = CapabilityOverride::new()
565 .true_color(Some(true))
566 .colors_256(Some(true))
567 .unicode_box_drawing(Some(false))
568 .mouse_sgr(Some(false));
569
570 assert_eq!(over.true_color, Some(true));
571 assert_eq!(over.colors_256, Some(true));
572 assert_eq!(over.unicode_box_drawing, Some(false));
573 assert_eq!(over.mouse_sgr, Some(false));
574 assert!(over.sync_output.is_none());
575 }
576
577 #[test]
578 fn apply_to_overrides_caps() {
579 let base = TerminalCapabilities::dumb();
580 let over = CapabilityOverride::new()
581 .true_color(Some(true))
582 .colors_256(Some(true))
583 .unicode_box_drawing(Some(true));
584
585 let result = over.apply_to(base);
586 assert!(result.true_color);
587 assert!(result.colors_256);
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.true_color, base.true_color);
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 .true_color(Some(true))
627 .mouse_sgr(Some(true)),
628 );
629 assert_eq!(override_depth(), 1);
630
631 {
632 let _inner = push_override(CapabilityOverride::new().true_color(Some(false)));
633 assert_eq!(override_depth(), 2);
634
635 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
637 assert!(!caps.true_color); assert!(caps.mouse_sgr); }
640
641 assert_eq!(override_depth(), 1);
643 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
644 assert!(caps.true_color); }
646
647 assert_eq!(override_depth(), 0);
648 }
649
650 #[test]
651 fn with_capability_override_scope() {
652 clear_all_overrides();
653
654 let result = with_capability_override(CapabilityOverride::modern(), || {
655 assert!(has_active_overrides());
656 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
657 caps.true_color
658 });
659
660 assert!(result);
661 assert!(!has_active_overrides());
662 }
663
664 #[test]
665 fn with_capability_override_nested() {
666 clear_all_overrides();
667
668 with_capability_override(CapabilityOverride::new().true_color(Some(true)), || {
669 with_capability_override(CapabilityOverride::new().mouse_sgr(Some(false)), || {
670 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
671 assert!(caps.true_color);
672 assert!(!caps.mouse_sgr);
673 });
674 });
675 }
676
677 #[test]
678 fn with_overrides_method() {
679 clear_all_overrides();
680
681 with_capability_override(CapabilityOverride::dumb(), || {
682 let caps = TerminalCapabilities::with_overrides();
683 assert!(!caps.true_color);
684 assert!(!caps.colors_256);
685 assert!(!caps.unicode_box_drawing);
686 assert!(!caps.unicode_emoji);
687 assert!(!caps.double_width);
688 });
689 }
690
691 #[test]
692 fn clear_all_overrides_works() {
693 let _g1 = push_override(CapabilityOverride::dumb());
694 let _g2 = push_override(CapabilityOverride::modern());
695 assert_eq!(override_depth(), 2);
696
697 clear_all_overrides();
698 assert_eq!(override_depth(), 0);
699 }
700
701 #[test]
702 fn default_override_is_empty() {
703 let over = CapabilityOverride::default();
704 assert!(over.is_empty());
705 }
706
707 #[test]
708 fn is_empty_false_for_single_override() {
709 let over = CapabilityOverride::new().true_color(Some(true));
710 assert!(!over.is_empty());
711 }
712
713 #[test]
714 fn dumb_disables_all_fields() {
715 let over = CapabilityOverride::dumb();
716 assert_eq!(over.unicode_box_drawing, Some(false));
717 assert_eq!(over.unicode_emoji, Some(false));
718 assert_eq!(over.double_width, Some(false));
719 assert_eq!(over.osc8_hyperlinks, Some(false));
720 assert_eq!(over.scroll_region, Some(false));
721 assert_eq!(over.kitty_keyboard, Some(false));
722 assert_eq!(over.focus_events, Some(false));
723 assert_eq!(over.bracketed_paste, Some(false));
724 assert_eq!(over.osc52_clipboard, Some(false));
725 assert_eq!(over.in_tmux, Some(false));
726 assert_eq!(over.in_screen, Some(false));
727 assert_eq!(over.in_zellij, Some(false));
728 }
729
730 #[test]
731 fn modern_enables_features_disables_mux() {
732 let over = CapabilityOverride::modern();
733 assert_eq!(over.unicode_box_drawing, Some(true));
734 assert_eq!(over.unicode_emoji, Some(true));
735 assert_eq!(over.double_width, Some(true));
736 assert_eq!(over.osc8_hyperlinks, Some(true));
737 assert_eq!(over.scroll_region, Some(true));
738 assert_eq!(over.focus_events, Some(true));
739 assert_eq!(over.bracketed_paste, Some(true));
740 assert_eq!(over.osc52_clipboard, Some(true));
741 assert_eq!(over.in_screen, Some(false));
742 assert_eq!(over.in_zellij, Some(false));
743 }
744
745 #[test]
746 fn tmux_sets_bracketed_paste_and_colors() {
747 let over = CapabilityOverride::tmux();
748 assert_eq!(over.colors_256, Some(true));
749 assert_eq!(over.bracketed_paste, Some(true));
750 assert_eq!(over.mouse_sgr, Some(true));
751 assert_eq!(over.scroll_region, Some(true));
752 assert_eq!(over.kitty_keyboard, Some(false));
753 }
754
755 #[test]
756 fn builder_all_optional_features() {
757 let over = CapabilityOverride::new()
758 .unicode_emoji(Some(true))
759 .double_width(Some(false))
760 .in_screen(Some(true))
761 .in_zellij(Some(true))
762 .osc8_hyperlinks(Some(true))
763 .osc52_clipboard(Some(false))
764 .scroll_region(Some(true))
765 .focus_events(Some(true))
766 .bracketed_paste(Some(false))
767 .kitty_keyboard(Some(true));
768
769 assert_eq!(over.unicode_emoji, Some(true));
770 assert_eq!(over.double_width, Some(false));
771 assert_eq!(over.in_screen, Some(true));
772 assert_eq!(over.in_zellij, Some(true));
773 assert_eq!(over.osc8_hyperlinks, Some(true));
774 assert_eq!(over.osc52_clipboard, Some(false));
775 assert_eq!(over.scroll_region, Some(true));
776 assert_eq!(over.focus_events, Some(true));
777 assert_eq!(over.bracketed_paste, Some(false));
778 assert_eq!(over.kitty_keyboard, Some(true));
779 }
780
781 #[test]
782 fn apply_to_covers_all_mux_flags() {
783 let base = TerminalCapabilities::dumb();
784 let over = CapabilityOverride::new()
785 .in_tmux(Some(true))
786 .in_screen(Some(true))
787 .in_zellij(Some(true));
788 let result = over.apply_to(base);
789 assert!(result.in_tmux);
790 assert!(result.in_screen);
791 assert!(result.in_zellij);
792 }
793
794 #[test]
795 fn apply_to_covers_input_features() {
796 let base = TerminalCapabilities::dumb();
797 let over = CapabilityOverride::new()
798 .kitty_keyboard(Some(true))
799 .focus_events(Some(true))
800 .bracketed_paste(Some(true))
801 .osc52_clipboard(Some(true));
802 let result = over.apply_to(base);
803 assert!(result.kitty_keyboard);
804 assert!(result.focus_events);
805 assert!(result.bracketed_paste);
806 assert!(result.osc52_clipboard);
807 }
808
809 #[test]
810 fn current_capabilities_with_base_composes_stack() {
811 clear_all_overrides();
812 let base = TerminalCapabilities::dumb();
813
814 let _g1 = push_override(CapabilityOverride::new().true_color(Some(true)));
815 let _g2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
816
817 let caps = current_capabilities_with_base(base);
818 assert!(caps.true_color);
819 assert!(caps.mouse_sgr);
820 assert!(!caps.colors_256); clear_all_overrides();
823 }
824
825 #[test]
826 fn override_clone() {
827 let over = CapabilityOverride::new()
828 .true_color(Some(true))
829 .in_tmux(Some(false));
830 let cloned = over.clone();
831 assert_eq!(over.true_color, cloned.true_color);
832 assert_eq!(over.in_tmux, cloned.in_tmux);
833 }
834
835 #[test]
838 fn is_empty_false_for_colors_256() {
839 assert!(!CapabilityOverride::new().colors_256(Some(true)).is_empty());
840 }
841
842 #[test]
843 fn is_empty_false_for_unicode_box_drawing() {
844 assert!(
845 !CapabilityOverride::new()
846 .unicode_box_drawing(Some(false))
847 .is_empty()
848 );
849 }
850
851 #[test]
852 fn is_empty_false_for_unicode_emoji() {
853 assert!(
854 !CapabilityOverride::new()
855 .unicode_emoji(Some(true))
856 .is_empty()
857 );
858 }
859
860 #[test]
861 fn is_empty_false_for_double_width() {
862 assert!(
863 !CapabilityOverride::new()
864 .double_width(Some(true))
865 .is_empty()
866 );
867 }
868
869 #[test]
870 fn is_empty_false_for_sync_output() {
871 assert!(
872 !CapabilityOverride::new()
873 .sync_output(Some(false))
874 .is_empty()
875 );
876 }
877
878 #[test]
879 fn is_empty_false_for_osc8_hyperlinks() {
880 assert!(
881 !CapabilityOverride::new()
882 .osc8_hyperlinks(Some(true))
883 .is_empty()
884 );
885 }
886
887 #[test]
888 fn is_empty_false_for_scroll_region() {
889 assert!(
890 !CapabilityOverride::new()
891 .scroll_region(Some(true))
892 .is_empty()
893 );
894 }
895
896 #[test]
897 fn is_empty_false_for_in_tmux() {
898 assert!(!CapabilityOverride::new().in_tmux(Some(true)).is_empty());
899 }
900
901 #[test]
902 fn is_empty_false_for_in_screen() {
903 assert!(!CapabilityOverride::new().in_screen(Some(true)).is_empty());
904 }
905
906 #[test]
907 fn is_empty_false_for_in_zellij() {
908 assert!(!CapabilityOverride::new().in_zellij(Some(true)).is_empty());
909 }
910
911 #[test]
912 fn is_empty_false_for_kitty_keyboard() {
913 assert!(
914 !CapabilityOverride::new()
915 .kitty_keyboard(Some(true))
916 .is_empty()
917 );
918 }
919
920 #[test]
921 fn is_empty_false_for_focus_events() {
922 assert!(
923 !CapabilityOverride::new()
924 .focus_events(Some(false))
925 .is_empty()
926 );
927 }
928
929 #[test]
930 fn is_empty_false_for_bracketed_paste() {
931 assert!(
932 !CapabilityOverride::new()
933 .bracketed_paste(Some(true))
934 .is_empty()
935 );
936 }
937
938 #[test]
939 fn is_empty_false_for_mouse_sgr() {
940 assert!(!CapabilityOverride::new().mouse_sgr(Some(true)).is_empty());
941 }
942
943 #[test]
944 fn is_empty_false_for_osc52_clipboard() {
945 assert!(
946 !CapabilityOverride::new()
947 .osc52_clipboard(Some(false))
948 .is_empty()
949 );
950 }
951
952 #[test]
955 fn apply_to_covers_unicode_emoji() {
956 let base = TerminalCapabilities::dumb();
957 let result = CapabilityOverride::new()
958 .unicode_emoji(Some(true))
959 .apply_to(base);
960 assert!(result.unicode_emoji);
961 }
962
963 #[test]
964 fn apply_to_covers_double_width() {
965 let base = TerminalCapabilities::dumb();
966 let result = CapabilityOverride::new()
967 .double_width(Some(true))
968 .apply_to(base);
969 assert!(result.double_width);
970 }
971
972 #[test]
973 fn apply_to_covers_sync_output() {
974 let base = TerminalCapabilities::dumb();
975 let result = CapabilityOverride::new()
976 .sync_output(Some(true))
977 .apply_to(base);
978 assert!(result.sync_output);
979 }
980
981 #[test]
982 fn apply_to_covers_osc8_hyperlinks() {
983 let base = TerminalCapabilities::dumb();
984 let result = CapabilityOverride::new()
985 .osc8_hyperlinks(Some(true))
986 .apply_to(base);
987 assert!(result.osc8_hyperlinks);
988 }
989
990 #[test]
991 fn apply_to_covers_scroll_region() {
992 let base = TerminalCapabilities::dumb();
993 let result = CapabilityOverride::new()
994 .scroll_region(Some(true))
995 .apply_to(base);
996 assert!(result.scroll_region);
997 }
998
999 #[test]
1000 fn apply_to_covers_mouse_sgr() {
1001 let base = TerminalCapabilities::dumb();
1002 let result = CapabilityOverride::new()
1003 .mouse_sgr(Some(true))
1004 .apply_to(base);
1005 assert!(result.mouse_sgr);
1006 }
1007
1008 #[test]
1011 fn dumb_override_disables_all_on_modern_base() {
1012 let base = TerminalCapabilities::modern();
1013 let result = CapabilityOverride::dumb().apply_to(base);
1014 assert!(!result.true_color);
1015 assert!(!result.colors_256);
1016 assert!(!result.unicode_box_drawing);
1017 assert!(!result.unicode_emoji);
1018 assert!(!result.double_width);
1019 assert!(!result.sync_output);
1020 assert!(!result.osc8_hyperlinks);
1021 assert!(!result.scroll_region);
1022 assert!(!result.in_tmux);
1023 assert!(!result.in_screen);
1024 assert!(!result.in_zellij);
1025 assert!(!result.kitty_keyboard);
1026 assert!(!result.focus_events);
1027 assert!(!result.bracketed_paste);
1028 assert!(!result.mouse_sgr);
1029 assert!(!result.osc52_clipboard);
1030 }
1031
1032 #[test]
1033 fn modern_override_enables_features_on_dumb_base() {
1034 let base = TerminalCapabilities::dumb();
1035 let result = CapabilityOverride::modern().apply_to(base);
1036 assert!(result.true_color);
1037 assert!(result.colors_256);
1038 assert!(result.unicode_box_drawing);
1039 assert!(result.unicode_emoji);
1040 assert!(result.double_width);
1041 assert!(result.sync_output);
1042 assert!(result.osc8_hyperlinks);
1043 assert!(result.scroll_region);
1044 assert!(!result.in_tmux);
1046 assert!(!result.in_screen);
1047 assert!(!result.in_zellij);
1048 assert!(result.kitty_keyboard);
1049 assert!(result.focus_events);
1050 assert!(result.bracketed_paste);
1051 assert!(result.mouse_sgr);
1052 assert!(result.osc52_clipboard);
1053 }
1054
1055 #[test]
1058 fn tmux_none_fields_passthrough() {
1059 let over = CapabilityOverride::tmux();
1060 assert!(over.true_color.is_none());
1061 assert!(over.unicode_box_drawing.is_none());
1062 assert!(over.unicode_emoji.is_none());
1063 assert!(over.double_width.is_none());
1064 }
1065
1066 #[test]
1069 fn builder_in_tmux_individually() {
1070 let over = CapabilityOverride::new().in_tmux(Some(true));
1071 assert_eq!(over.in_tmux, Some(true));
1072 assert!(over.true_color.is_none()); }
1074
1075 #[test]
1076 fn builder_sync_output_individually() {
1077 let over = CapabilityOverride::new().sync_output(Some(false));
1078 assert_eq!(over.sync_output, Some(false));
1079 assert!(over.colors_256.is_none());
1080 }
1081
1082 #[test]
1085 fn builder_overwrite_field_to_none() {
1086 let over = CapabilityOverride::new()
1087 .true_color(Some(true))
1088 .true_color(None);
1089 assert!(over.true_color.is_none());
1090 assert!(over.is_empty());
1091 }
1092
1093 #[test]
1094 fn builder_overwrite_dumb_field_to_none() {
1095 let over = CapabilityOverride::dumb().true_color(None);
1096 assert!(over.true_color.is_none());
1097 assert!(!over.is_empty()); }
1099
1100 #[test]
1103 fn guard_drop_after_clear_all_is_noop() {
1104 clear_all_overrides();
1105
1106 let guard = push_override(CapabilityOverride::dumb());
1107 assert_eq!(override_depth(), 1);
1108
1109 clear_all_overrides();
1110 assert_eq!(override_depth(), 0);
1111
1112 drop(guard);
1114 assert_eq!(override_depth(), 0);
1115 }
1116
1117 #[test]
1118 fn multiple_guards_drop_after_clear_all() {
1119 clear_all_overrides();
1120
1121 let g1 = push_override(CapabilityOverride::dumb());
1122 let g2 = push_override(CapabilityOverride::modern());
1123 assert_eq!(override_depth(), 2);
1124
1125 clear_all_overrides();
1126 assert_eq!(override_depth(), 0);
1127
1128 drop(g2);
1130 drop(g1);
1131 assert_eq!(override_depth(), 0);
1132 }
1133
1134 #[test]
1137 fn three_level_nesting_innermost_wins() {
1138 clear_all_overrides();
1139
1140 let _l1 = push_override(CapabilityOverride::new().true_color(Some(true)));
1141 let _l2 = push_override(CapabilityOverride::new().true_color(Some(false)));
1142 let _l3 = push_override(CapabilityOverride::new().true_color(Some(true)));
1143
1144 assert_eq!(override_depth(), 3);
1145 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1146 assert!(caps.true_color); clear_all_overrides();
1149 }
1150
1151 #[test]
1152 fn three_level_nesting_partial_overrides() {
1153 clear_all_overrides();
1154
1155 let _l1 = push_override(CapabilityOverride::new().true_color(Some(true)));
1156 let _l2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1157 let _l3 = push_override(CapabilityOverride::new().colors_256(Some(true)));
1158
1159 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1160 assert!(caps.true_color); assert!(caps.mouse_sgr); assert!(caps.colors_256); assert!(!caps.sync_output); clear_all_overrides();
1166 }
1167
1168 #[test]
1171 fn with_overrides_from_applies_stack() {
1172 clear_all_overrides();
1173
1174 let base = TerminalCapabilities::dumb();
1175 let _g = push_override(CapabilityOverride::new().true_color(Some(true)));
1176
1177 let caps = base.with_overrides_from(base);
1179 assert!(caps.true_color);
1180 assert!(!caps.colors_256); clear_all_overrides();
1183 }
1184
1185 #[test]
1186 fn with_overrides_from_without_active_overrides() {
1187 clear_all_overrides();
1188
1189 let base = TerminalCapabilities::modern();
1190 let caps = base.with_overrides_from(base);
1191 assert_eq!(caps.true_color, base.true_color);
1193 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1194 }
1195
1196 #[test]
1199 fn with_capability_override_cleans_up_on_panic() {
1200 clear_all_overrides();
1201
1202 let result = std::panic::catch_unwind(|| {
1203 with_capability_override(CapabilityOverride::dumb(), || {
1204 assert!(has_active_overrides());
1205 panic!("deliberate panic");
1206 });
1207 });
1208
1209 assert!(result.is_err());
1210 assert!(!has_active_overrides());
1212 assert_eq!(override_depth(), 0);
1213 }
1214
1215 #[test]
1218 fn debug_format_contains_field_names() {
1219 let over = CapabilityOverride::new().true_color(Some(true));
1220 let dbg = format!("{over:?}");
1221 assert!(dbg.contains("true_color"));
1222 assert!(dbg.contains("Some(true)"));
1223 }
1224
1225 #[test]
1226 fn debug_format_empty_override() {
1227 let over = CapabilityOverride::new();
1228 let dbg = format!("{over:?}");
1229 assert!(dbg.contains("CapabilityOverride"));
1230 assert!(dbg.contains("None"));
1231 }
1232
1233 #[test]
1236 fn clear_all_then_push_resumes_normally() {
1237 clear_all_overrides();
1238
1239 let _g1 = push_override(CapabilityOverride::dumb());
1240 clear_all_overrides();
1241 assert_eq!(override_depth(), 0);
1242
1243 let _g2 = push_override(CapabilityOverride::modern());
1245 assert_eq!(override_depth(), 1);
1246 assert!(has_active_overrides());
1247
1248 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1249 assert!(caps.true_color);
1250
1251 clear_all_overrides();
1252 }
1253
1254 #[test]
1257 fn current_capabilities_uses_detect_as_base() {
1258 clear_all_overrides();
1259
1260 let _g = push_override(CapabilityOverride::dumb());
1262 let caps = current_capabilities();
1263 assert!(!caps.true_color);
1264 assert!(!caps.mouse_sgr);
1265
1266 clear_all_overrides();
1267 }
1268
1269 #[test]
1272 fn with_overrides_integrates_full_stack() {
1273 clear_all_overrides();
1274
1275 let _g = push_override(CapabilityOverride::modern());
1276 let caps = TerminalCapabilities::with_overrides();
1277 assert!(caps.true_color);
1278 assert!(caps.kitty_keyboard);
1279 assert!(!caps.in_tmux); clear_all_overrides();
1282 }
1283
1284 #[test]
1287 fn second_guard_dropped_first_still_active() {
1288 clear_all_overrides();
1289
1290 let g1 = push_override(CapabilityOverride::new().true_color(Some(true)));
1291 let g2 = push_override(CapabilityOverride::new().true_color(Some(false)));
1292
1293 drop(g2);
1295 assert_eq!(override_depth(), 1);
1296 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1297 assert!(caps.true_color); drop(g1);
1300 assert_eq!(override_depth(), 0);
1301 }
1302
1303 #[test]
1306 fn with_capability_override_returns_string() {
1307 clear_all_overrides();
1308
1309 let val = with_capability_override(CapabilityOverride::dumb(), || {
1310 String::from("computed value")
1311 });
1312 assert_eq!(val, "computed value");
1313 }
1314
1315 #[test]
1316 fn with_capability_override_returns_tuple() {
1317 clear_all_overrides();
1318
1319 let (a, b) = with_capability_override(CapabilityOverride::modern(), || {
1320 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1321 (caps.true_color, caps.mouse_sgr)
1322 });
1323 assert!(a);
1324 assert!(b);
1325 }
1326
1327 #[test]
1330 fn apply_to_disables_on_modern_base() {
1331 let base = TerminalCapabilities::modern();
1332 let result = CapabilityOverride::new()
1333 .true_color(Some(false))
1334 .kitty_keyboard(Some(false))
1335 .apply_to(base);
1336 assert!(!result.true_color);
1337 assert!(!result.kitty_keyboard);
1338 assert!(result.colors_256);
1340 assert!(result.unicode_box_drawing);
1341 }
1342
1343 #[test]
1346 fn current_capabilities_with_base_no_overrides_returns_base() {
1347 clear_all_overrides();
1348
1349 let base = TerminalCapabilities::modern();
1350 let caps = current_capabilities_with_base(base);
1351 assert_eq!(caps.true_color, base.true_color);
1352 assert_eq!(caps.colors_256, base.colors_256);
1353 assert_eq!(caps.unicode_box_drawing, base.unicode_box_drawing);
1354 assert_eq!(caps.unicode_emoji, base.unicode_emoji);
1355 assert_eq!(caps.double_width, base.double_width);
1356 assert_eq!(caps.sync_output, base.sync_output);
1357 assert_eq!(caps.osc8_hyperlinks, base.osc8_hyperlinks);
1358 assert_eq!(caps.scroll_region, base.scroll_region);
1359 assert_eq!(caps.in_tmux, base.in_tmux);
1360 assert_eq!(caps.in_screen, base.in_screen);
1361 assert_eq!(caps.in_zellij, base.in_zellij);
1362 assert_eq!(caps.kitty_keyboard, base.kitty_keyboard);
1363 assert_eq!(caps.focus_events, base.focus_events);
1364 assert_eq!(caps.bracketed_paste, base.bracketed_paste);
1365 assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1366 assert_eq!(caps.osc52_clipboard, base.osc52_clipboard);
1367 }
1368}