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 sync_output: Option<bool>,
76 pub osc8_hyperlinks: Option<bool>,
77 pub scroll_region: Option<bool>,
78
79 pub in_tmux: Option<bool>,
81 pub in_screen: Option<bool>,
82 pub in_zellij: Option<bool>,
83
84 pub kitty_keyboard: Option<bool>,
86 pub focus_events: Option<bool>,
87 pub bracketed_paste: Option<bool>,
88 pub mouse_sgr: Option<bool>,
89
90 pub osc52_clipboard: Option<bool>,
92}
93
94impl CapabilityOverride {
95 #[must_use]
97 pub const fn new() -> Self {
98 Self {
99 true_color: None,
100 colors_256: None,
101 sync_output: None,
102 osc8_hyperlinks: None,
103 scroll_region: None,
104 in_tmux: None,
105 in_screen: None,
106 in_zellij: None,
107 kitty_keyboard: None,
108 focus_events: None,
109 bracketed_paste: None,
110 mouse_sgr: None,
111 osc52_clipboard: None,
112 }
113 }
114
115 #[must_use]
117 pub const fn dumb() -> Self {
118 Self {
119 true_color: Some(false),
120 colors_256: Some(false),
121 sync_output: Some(false),
122 osc8_hyperlinks: Some(false),
123 scroll_region: Some(false),
124 in_tmux: Some(false),
125 in_screen: Some(false),
126 in_zellij: Some(false),
127 kitty_keyboard: Some(false),
128 focus_events: Some(false),
129 bracketed_paste: Some(false),
130 mouse_sgr: Some(false),
131 osc52_clipboard: Some(false),
132 }
133 }
134
135 #[must_use]
137 pub const fn modern() -> Self {
138 Self {
139 true_color: Some(true),
140 colors_256: Some(true),
141 sync_output: Some(true),
142 osc8_hyperlinks: Some(true),
143 scroll_region: Some(true),
144 in_tmux: Some(false),
145 in_screen: Some(false),
146 in_zellij: Some(false),
147 kitty_keyboard: Some(true),
148 focus_events: Some(true),
149 bracketed_paste: Some(true),
150 mouse_sgr: Some(true),
151 osc52_clipboard: Some(true),
152 }
153 }
154
155 #[must_use]
157 pub const fn tmux() -> Self {
158 Self {
159 true_color: None,
160 colors_256: Some(true),
161 sync_output: Some(false),
162 osc8_hyperlinks: Some(false),
163 scroll_region: Some(true),
164 in_tmux: Some(true),
165 in_screen: Some(false),
166 in_zellij: Some(false),
167 kitty_keyboard: Some(false),
168 focus_events: Some(false),
169 bracketed_paste: Some(true),
170 mouse_sgr: Some(true),
171 osc52_clipboard: Some(false),
172 }
173 }
174
175 #[must_use]
179 pub const fn true_color(mut self, value: Option<bool>) -> Self {
180 self.true_color = value;
181 self
182 }
183
184 #[must_use]
186 pub const fn colors_256(mut self, value: Option<bool>) -> Self {
187 self.colors_256 = value;
188 self
189 }
190
191 #[must_use]
193 pub const fn sync_output(mut self, value: Option<bool>) -> Self {
194 self.sync_output = value;
195 self
196 }
197
198 #[must_use]
200 pub const fn osc8_hyperlinks(mut self, value: Option<bool>) -> Self {
201 self.osc8_hyperlinks = value;
202 self
203 }
204
205 #[must_use]
207 pub const fn scroll_region(mut self, value: Option<bool>) -> Self {
208 self.scroll_region = value;
209 self
210 }
211
212 #[must_use]
214 pub const fn in_tmux(mut self, value: Option<bool>) -> Self {
215 self.in_tmux = value;
216 self
217 }
218
219 #[must_use]
221 pub const fn in_screen(mut self, value: Option<bool>) -> Self {
222 self.in_screen = value;
223 self
224 }
225
226 #[must_use]
228 pub const fn in_zellij(mut self, value: Option<bool>) -> Self {
229 self.in_zellij = value;
230 self
231 }
232
233 #[must_use]
235 pub const fn kitty_keyboard(mut self, value: Option<bool>) -> Self {
236 self.kitty_keyboard = value;
237 self
238 }
239
240 #[must_use]
242 pub const fn focus_events(mut self, value: Option<bool>) -> Self {
243 self.focus_events = value;
244 self
245 }
246
247 #[must_use]
249 pub const fn bracketed_paste(mut self, value: Option<bool>) -> Self {
250 self.bracketed_paste = value;
251 self
252 }
253
254 #[must_use]
256 pub const fn mouse_sgr(mut self, value: Option<bool>) -> Self {
257 self.mouse_sgr = value;
258 self
259 }
260
261 #[must_use]
263 pub const fn osc52_clipboard(mut self, value: Option<bool>) -> Self {
264 self.osc52_clipboard = value;
265 self
266 }
267
268 #[must_use]
270 pub const fn is_empty(&self) -> bool {
271 self.true_color.is_none()
272 && self.colors_256.is_none()
273 && self.sync_output.is_none()
274 && self.osc8_hyperlinks.is_none()
275 && self.scroll_region.is_none()
276 && self.in_tmux.is_none()
277 && self.in_screen.is_none()
278 && self.in_zellij.is_none()
279 && self.kitty_keyboard.is_none()
280 && self.focus_events.is_none()
281 && self.bracketed_paste.is_none()
282 && self.mouse_sgr.is_none()
283 && self.osc52_clipboard.is_none()
284 }
285
286 #[must_use]
288 pub fn apply_to(&self, mut caps: TerminalCapabilities) -> TerminalCapabilities {
289 if let Some(v) = self.true_color {
290 caps.true_color = v;
291 }
292 if let Some(v) = self.colors_256 {
293 caps.colors_256 = v;
294 }
295 if let Some(v) = self.sync_output {
296 caps.sync_output = v;
297 }
298 if let Some(v) = self.osc8_hyperlinks {
299 caps.osc8_hyperlinks = v;
300 }
301 if let Some(v) = self.scroll_region {
302 caps.scroll_region = v;
303 }
304 if let Some(v) = self.in_tmux {
305 caps.in_tmux = v;
306 }
307 if let Some(v) = self.in_screen {
308 caps.in_screen = v;
309 }
310 if let Some(v) = self.in_zellij {
311 caps.in_zellij = v;
312 }
313 if let Some(v) = self.kitty_keyboard {
314 caps.kitty_keyboard = v;
315 }
316 if let Some(v) = self.focus_events {
317 caps.focus_events = v;
318 }
319 if let Some(v) = self.bracketed_paste {
320 caps.bracketed_paste = v;
321 }
322 if let Some(v) = self.mouse_sgr {
323 caps.mouse_sgr = v;
324 }
325 if let Some(v) = self.osc52_clipboard {
326 caps.osc52_clipboard = v;
327 }
328 caps
329 }
330}
331
332thread_local! {
337 static OVERRIDE_STACK: RefCell<Vec<CapabilityOverride>> = const { RefCell::new(Vec::new()) };
339}
340
341#[must_use]
345pub struct OverrideGuard {
346 _marker: std::marker::PhantomData<*const ()>,
348}
349
350impl Drop for OverrideGuard {
351 fn drop(&mut self) {
352 OVERRIDE_STACK.with(|stack| {
355 stack.borrow_mut().pop();
356 });
357 }
358}
359
360#[must_use = "the override is removed when the guard is dropped"]
374pub fn push_override(over: CapabilityOverride) -> OverrideGuard {
375 OVERRIDE_STACK.with(|stack| {
376 stack.borrow_mut().push(over);
377 });
378 OverrideGuard {
379 _marker: std::marker::PhantomData,
380 }
381}
382
383pub fn with_capability_override<F, R>(over: CapabilityOverride, f: F) -> R
400where
401 F: FnOnce() -> R,
402{
403 let _guard = push_override(over);
404 f()
405}
406
407#[must_use]
412pub fn current_capabilities() -> TerminalCapabilities {
413 let base = TerminalCapabilities::detect();
414 current_capabilities_with_base(base)
415}
416
417#[must_use]
419pub fn current_capabilities_with_base(base: TerminalCapabilities) -> TerminalCapabilities {
420 OVERRIDE_STACK.with(|stack| {
421 let stack = stack.borrow();
422 stack.iter().fold(base, |caps, over| over.apply_to(caps))
423 })
424}
425
426#[must_use]
428pub fn has_active_overrides() -> bool {
429 OVERRIDE_STACK.with(|stack| !stack.borrow().is_empty())
430}
431
432#[must_use]
434pub fn override_depth() -> usize {
435 OVERRIDE_STACK.with(|stack| stack.borrow().len())
436}
437
438pub fn clear_all_overrides() {
443 OVERRIDE_STACK.with(|stack| {
444 stack.borrow_mut().clear();
445 });
446}
447
448impl TerminalCapabilities {
453 #[must_use]
458 pub fn with_overrides() -> Self {
459 current_capabilities()
460 }
461
462 #[must_use]
464 pub fn with_overrides_from(self, base: Self) -> Self {
465 current_capabilities_with_base(base)
466 }
467}
468
469#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn override_new_is_empty() {
479 let over = CapabilityOverride::new();
480 assert!(over.is_empty());
481 }
482
483 #[test]
484 fn override_dumb_disables_all() {
485 let over = CapabilityOverride::dumb();
486 assert!(!over.is_empty());
487 assert_eq!(over.true_color, Some(false));
488 assert_eq!(over.colors_256, Some(false));
489 assert_eq!(over.sync_output, Some(false));
490 assert_eq!(over.mouse_sgr, Some(false));
491 }
492
493 #[test]
494 fn override_modern_enables_all() {
495 let over = CapabilityOverride::modern();
496 assert_eq!(over.true_color, Some(true));
497 assert_eq!(over.colors_256, Some(true));
498 assert_eq!(over.sync_output, Some(true));
499 assert_eq!(over.kitty_keyboard, Some(true));
500 assert_eq!(over.in_tmux, Some(false));
502 }
503
504 #[test]
505 fn override_tmux_sets_mux() {
506 let over = CapabilityOverride::tmux();
507 assert_eq!(over.in_tmux, Some(true));
508 assert_eq!(over.sync_output, Some(false));
509 assert_eq!(over.osc52_clipboard, Some(false));
510 }
511
512 #[test]
513 fn override_builder_chain() {
514 let over = CapabilityOverride::new()
515 .true_color(Some(true))
516 .colors_256(Some(true))
517 .mouse_sgr(Some(false));
518
519 assert_eq!(over.true_color, Some(true));
520 assert_eq!(over.colors_256, Some(true));
521 assert_eq!(over.mouse_sgr, Some(false));
522 assert!(over.sync_output.is_none());
523 }
524
525 #[test]
526 fn apply_to_overrides_caps() {
527 let base = TerminalCapabilities::dumb();
528 let over = CapabilityOverride::new()
529 .true_color(Some(true))
530 .colors_256(Some(true));
531
532 let result = over.apply_to(base);
533 assert!(result.true_color);
534 assert!(result.colors_256);
535 assert!(!result.mouse_sgr);
537 }
538
539 #[test]
540 fn apply_to_none_keeps_original() {
541 let base = TerminalCapabilities::modern();
542 let over = CapabilityOverride::new(); let result = over.apply_to(base);
545 assert_eq!(result.true_color, base.true_color);
546 assert_eq!(result.mouse_sgr, base.mouse_sgr);
547 }
548
549 #[test]
550 fn push_pop_override() {
551 clear_all_overrides();
552 assert!(!has_active_overrides());
553 assert_eq!(override_depth(), 0);
554
555 {
556 let _guard = push_override(CapabilityOverride::dumb());
557 assert!(has_active_overrides());
558 assert_eq!(override_depth(), 1);
559 }
560
561 assert!(!has_active_overrides());
562 assert_eq!(override_depth(), 0);
563 }
564
565 #[test]
566 fn nested_overrides() {
567 clear_all_overrides();
568
569 {
570 let _outer = push_override(
571 CapabilityOverride::new()
572 .true_color(Some(true))
573 .mouse_sgr(Some(true)),
574 );
575 assert_eq!(override_depth(), 1);
576
577 {
578 let _inner = push_override(CapabilityOverride::new().true_color(Some(false)));
579 assert_eq!(override_depth(), 2);
580
581 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
583 assert!(!caps.true_color); assert!(caps.mouse_sgr); }
586
587 assert_eq!(override_depth(), 1);
589 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
590 assert!(caps.true_color); }
592
593 assert_eq!(override_depth(), 0);
594 }
595
596 #[test]
597 fn with_capability_override_scope() {
598 clear_all_overrides();
599
600 let result = with_capability_override(CapabilityOverride::modern(), || {
601 assert!(has_active_overrides());
602 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
603 caps.true_color
604 });
605
606 assert!(result);
607 assert!(!has_active_overrides());
608 }
609
610 #[test]
611 fn with_capability_override_nested() {
612 clear_all_overrides();
613
614 with_capability_override(CapabilityOverride::new().true_color(Some(true)), || {
615 with_capability_override(CapabilityOverride::new().mouse_sgr(Some(false)), || {
616 let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
617 assert!(caps.true_color);
618 assert!(!caps.mouse_sgr);
619 });
620 });
621 }
622
623 #[test]
624 fn with_overrides_method() {
625 clear_all_overrides();
626
627 with_capability_override(CapabilityOverride::dumb(), || {
628 let caps = TerminalCapabilities::with_overrides();
629 assert!(!caps.true_color);
630 assert!(!caps.colors_256);
631 });
632 }
633
634 #[test]
635 fn clear_all_overrides_works() {
636 let _g1 = push_override(CapabilityOverride::dumb());
637 let _g2 = push_override(CapabilityOverride::modern());
638 assert_eq!(override_depth(), 2);
639
640 clear_all_overrides();
641 assert_eq!(override_depth(), 0);
642 }
643
644 #[test]
645 fn default_override_is_empty() {
646 let over = CapabilityOverride::default();
647 assert!(over.is_empty());
648 }
649}