Skip to main content

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}