Skip to main content

badterm_core/
mouse.rs

1//! Mouse reports for the BBS, as xterm (and CTerm, after it) sends them
2//! once the BBS asks: X10's presses (`CSI ? 9 h`), presses and releases
3//! (1000), with drags (1002) or every movement (1003), in the old
4//! `CSI M b x y` form or, with 1006, `CSI < b ; x ; y M`/`m` (1016: the
5//! same, in pixels).
6
7/// What the mouse did. Buttons are 0 (left), 1 (middle) and 2 (right).
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum MouseEvent {
10    Press(u8),
11    Release(u8),
12    /// It moved, with this button held (or none).
13    Move(Option<u8>),
14    /// The wheel: up or down.
15    Wheel(bool),
16}
17
18/// Keys held with it.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub struct MouseMods {
21    pub shift: bool,
22    pub alt: bool,
23    pub ctrl: bool,
24}
25
26/// The reporting the BBS has asked for.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct MouseModes {
29    pub x10: bool,
30    pub normal: bool,
31    pub drag: bool,
32    pub any: bool,
33    pub sgr: bool,
34    pub pixels: bool,
35}
36
37impl MouseModes {
38    /// Whether the BBS wants the mouse at all.
39    pub fn tracking(&self) -> bool {
40        self.x10 || self.normal || self.drag || self.any
41    }
42
43    /// Set or reset a DEC private mode, if it's one of these.
44    pub fn set(&mut self, mode: u32, on: bool) -> bool {
45        match mode {
46            9 => self.x10 = on,
47            1000 => self.normal = on,
48            1002 => self.drag = on,
49            1003 => self.any = on,
50            1006 => self.sgr = on,
51            1016 => self.pixels = on,
52            _ => return false,
53        }
54        true
55    }
56
57    /// The modes that are set, for CTerm's mode report.
58    pub fn set_modes(&self) -> Vec<u32> {
59        [(9, self.x10), (1000, self.normal), (1002, self.drag), (1003, self.any), (1006, self.sgr), (1016, self.pixels)]
60            .iter()
61            .filter(|m| m.1)
62            .map(|m| m.0)
63            .collect()
64    }
65
66    /// The report for an event at a cell (0-based) and pixel (0-based,
67    /// on the terminal's own pixel grid), if one is due.
68    pub fn report(
69        &self,
70        event: MouseEvent,
71        cell: (usize, usize),
72        pixel: (usize, usize),
73        mods: MouseMods,
74    ) -> Option<Vec<u8>> {
75        if !self.tracking() {
76            return None;
77        }
78        let (code, release) = match event {
79            MouseEvent::Press(b) => (b.min(2) as u32, false),
80            MouseEvent::Release(_) if self.x10 && !self.normal && !self.drag && !self.any => return None,
81            MouseEvent::Release(b) => (if self.sgr || self.pixels { b.min(2) as u32 } else { 3 }, true),
82            MouseEvent::Move(held) => {
83                if !(self.any || self.drag && held.is_some()) {
84                    return None;
85                }
86                (32 + held.map_or(3, |b| b.min(2) as u32), false)
87            }
88            MouseEvent::Wheel(up) if !self.x10 || self.normal || self.drag || self.any => {
89                (if up { 64 } else { 65 }, false)
90            }
91            MouseEvent::Wheel(_) => return None,
92        };
93        // X10's reports carry no keys.
94        let only_x10 = self.x10 && !self.normal && !self.drag && !self.any;
95        let keys =
96            if only_x10 { 0 } else { ((mods.shift as u32) * 4) | ((mods.alt as u32) * 8) | ((mods.ctrl as u32) * 16) };
97        let b = code | keys;
98        if self.sgr || self.pixels {
99            let (x, y) = if self.pixels { pixel } else { cell };
100            let end = if release { 'm' } else { 'M' };
101            return Some(format!("\x1b[<{b};{};{}{end}", x + 1, y + 1).into_bytes());
102        }
103        // The old form: a byte each, so positions past 222 can't be sent.
104        let (x, y) = (cell.0 + 33, cell.1 + 33);
105        (x <= 255 && y <= 255).then(|| vec![0x1B, b'[', b'M', (32 + b) as u8, x as u8, y as u8])
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    fn modes(list: &[u32]) -> MouseModes {
114        let mut m = MouseModes::default();
115        for &n in list {
116            m.set(n, true);
117        }
118        m
119    }
120
121    #[test]
122    fn reports() {
123        let none = MouseMods::default();
124        let at = (4, 2);
125        let px = (37, 21);
126        // Nothing asked for, nothing sent.
127        assert_eq!(MouseModes::default().report(MouseEvent::Press(0), at, px, none), None);
128        // X10: presses only, the old form.
129        let x10 = modes(&[9]);
130        assert_eq!(x10.report(MouseEvent::Press(0), at, px, none).unwrap(), b"\x1b[M \x25\x23");
131        assert_eq!(x10.report(MouseEvent::Release(0), at, px, none), None);
132        // 1000: releases are button 3; keys add 4, 8, 16.
133        let normal = modes(&[1000]);
134        assert_eq!(normal.report(MouseEvent::Release(0), at, px, none).unwrap(), b"\x1b[M#\x25\x23");
135        let ctrl = MouseMods { ctrl: true, ..none };
136        assert_eq!(normal.report(MouseEvent::Press(2), at, px, ctrl).unwrap(), b"\x1b[M\x32\x25\x23");
137        assert_eq!(normal.report(MouseEvent::Move(Some(0)), at, px, none), None);
138        // 1002 reports drags, 1003 any movement; SGR is one-based text.
139        let drag = modes(&[1002, 1006]);
140        assert_eq!(drag.report(MouseEvent::Move(Some(0)), at, px, none).unwrap(), b"\x1b[<32;5;3M");
141        assert_eq!(drag.report(MouseEvent::Move(None), at, px, none), None);
142        let any = modes(&[1003, 1006]);
143        assert_eq!(any.report(MouseEvent::Move(None), at, px, none).unwrap(), b"\x1b[<35;5;3M");
144        assert_eq!(any.report(MouseEvent::Release(2), at, px, none).unwrap(), b"\x1b[<2;5;3m");
145        assert_eq!(any.report(MouseEvent::Wheel(false), at, px, none).unwrap(), b"\x1b[<65;5;3M");
146        // 1016: pixels.
147        let pixels = modes(&[1000, 1016]);
148        assert_eq!(pixels.report(MouseEvent::Press(0), at, px, none).unwrap(), b"\x1b[<0;38;22M");
149        // The old form can't send columns past 222.
150        assert_eq!(normal.report(MouseEvent::Press(0), (230, 0), px, none), None);
151    }
152}