tear_types/modes.rs
1//! The terminal modes a client must read from the AUTHORITY, never from a
2//! parser of its own.
3//!
4//! ## Why these are ten types and not ten `bool`s
5//!
6//! A mode decides how a client encodes what the operator does. Get the
7//! wrong one and the failure is not cosmetic:
8//!
9//! - **bracketed paste** gates paste SANITISATION. mado reads it, then
10//! passes it to `sanitize_paste`. A wrong answer there is a paste-
11//! injection surface, not a rendering glitch.
12//! - **cursor keys (DECCKM)** decides whether arrows are `ESC [ A` or
13//! `ESC O A`. Wrong, and every editor and pager receives the wrong keys.
14//! - **mouse tracking** decides whether a click is reported at all.
15//!
16//! If these were bare `bool`s, `sanitize_paste(text, modes.focus_reporting)`
17//! would type-check. Ten distinct newtypes make substituting one mode for
18//! another an **`E0308`** — the compiler will not let a client confuse them.
19//!
20//! ## Why [`ModeSet`] is carried BY a view and never fetched separately
21//!
22//! A client that could ask for modes independently could render frame N's
23//! cells while encoding a keystroke under frame N+1's modes — bracketed
24//! paste toggling in the gap between the grid you drew and the key you
25//! sent. Because a `ModeSet` is only obtainable from the view it came
26//! from, "modes from a different instant than the cells" has no
27//! representation. The cost is ten bytes.
28
29use serde::{Deserialize, Serialize};
30
31/// Declare a boolean mode newtype with its DEC number in the docs.
32macro_rules! mode_flag {
33 ($(#[$m:meta])* $name:ident) => {
34 $(#[$m])*
35 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
36 #[serde(transparent)]
37 pub struct $name(bool);
38
39 impl $name {
40 #[must_use]
41 pub const fn new(on: bool) -> Self { Self(on) }
42 /// Is this mode on?
43 #[must_use]
44 pub const fn enabled(self) -> bool { self.0 }
45 }
46 };
47}
48
49mode_flag! {
50 /// DEC 2004 — bracketed paste. When on, a paste is framed with
51 /// `ESC[200~` / `ESC[201~` so the program can tell it from typing.
52 ///
53 /// **This one gates paste sanitisation. Read it from the authority.**
54 BracketedPaste
55}
56mode_flag! {
57 /// DEC 1 (DECCKM) — application cursor keys. Arrows become `ESC O A`
58 /// instead of `ESC [ A`.
59 CursorKeys
60}
61mode_flag! {
62 /// DEC 1004 — focus reporting. The program is told when the terminal
63 /// gains (`ESC[I`) or loses (`ESC[O`) focus.
64 FocusReporting
65}
66mode_flag! {
67 /// DEC 2026 — synchronized output. The program has asked that nothing
68 /// be presented until it says done, so a renderer holds the frame.
69 ///
70 /// A renderer MUST bound its hold: an app that never clears the flag
71 /// would otherwise freeze the pane forever.
72 SyncOutput
73}
74mode_flag! {
75 /// DEC 1006 — SGR extended mouse encoding. Decides the REPORT format,
76 /// independently of whether tracking is on at all.
77 MouseSgr
78}
79mode_flag! {
80 /// DEC 25 (DECTCEM) — cursor visibility.
81 CursorVisible
82}
83mode_flag! {
84 /// DEC 7 (DECAWM) — autowrap at the right margin.
85 AutoWrap
86}
87mode_flag! {
88 /// The alternate screen buffer is active (vim, less, htop). Set via
89 /// DEC 47 / 1047 / 1049.
90 AltScreen
91}
92
93/// Mouse tracking level — DEC 1000 / 1002 / 1003.
94///
95/// An enum and not three flags: the levels are **mutually exclusive**, so
96/// three bools would make "click AND motion tracking simultaneously"
97/// constructible, which no terminal can mean.
98#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum MouseTracking {
101 /// No mouse reporting.
102 #[default]
103 Off,
104 /// DEC 1000 — press and release only.
105 Click,
106 /// DEC 1002 — press, release, and motion while a button is held.
107 Drag,
108 /// DEC 1003 — all motion, button or not.
109 Motion,
110}
111
112impl MouseTracking {
113 /// Is the program listening for mouse events at all?
114 #[must_use]
115 pub const fn is_on(self) -> bool {
116 !matches!(self, Self::Off)
117 }
118}
119
120/// Every mode a client needs, taken at ONE instant.
121///
122/// Obtainable only from a pane view — see the module docs for why that is
123/// the point rather than an inconvenience.
124#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
125pub struct ModeSet {
126 pub bracketed_paste: BracketedPaste,
127 pub cursor_keys: CursorKeys,
128 pub focus_reporting: FocusReporting,
129 pub sync_output: SyncOutput,
130 pub mouse: MouseTracking,
131 pub mouse_sgr: MouseSgr,
132 pub cursor_visible: CursorVisible,
133 pub autowrap: AutoWrap,
134 pub alt_screen: AltScreen,
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn a_mode_cannot_be_substituted_for_another() {
143 // Compile-time property, asserted by construction: this function
144 // accepts ONLY BracketedPaste. Passing `FocusReporting` here is
145 // E0308 — which is the whole reason these are ten types.
146 fn sanitize(_text: &str, bracketed: BracketedPaste) -> bool {
147 bracketed.enabled()
148 }
149 assert!(sanitize("x", BracketedPaste::new(true)));
150 assert!(!sanitize("x", BracketedPaste::new(false)));
151 }
152
153 #[test]
154 fn mouse_levels_are_exclusive_by_construction() {
155 // Three bools would let two levels be true at once. An enum has no
156 // such value.
157 let m = MouseTracking::Drag;
158 assert!(m.is_on());
159 assert_eq!(MouseTracking::default(), MouseTracking::Off);
160 assert!(!MouseTracking::Off.is_on());
161 }
162
163 #[test]
164 fn modes_default_to_off_which_is_what_a_fresh_terminal_means() {
165 let m = ModeSet::default();
166 assert!(!m.bracketed_paste.enabled());
167 assert!(!m.cursor_keys.enabled());
168 assert!(!m.sync_output.enabled());
169 assert!(!m.mouse.is_on());
170 }
171
172 #[test]
173 fn a_mode_flag_is_wire_identical_to_the_bool_it_wraps() {
174 // `#[serde(transparent)]` — a mode must not change the wire shape
175 // of the field it replaces.
176 let a = serde_json::to_string(&BracketedPaste::new(true)).unwrap();
177 let b = serde_json::to_string(&true).unwrap();
178 assert_eq!(a, b);
179 }
180}