freya_terminal/
parser.rs

1use vt100::MouseProtocolEncoding;
2
3/// Mouse button for terminal encoding.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum TerminalMouseButton {
6    Left,
7    Middle,
8    Right,
9}
10
11impl TerminalMouseButton {
12    /// X11/SGR button code (without modifier bits).
13    fn code(self) -> u8 {
14        match self {
15            Self::Left => 0,
16            Self::Middle => 1,
17            Self::Right => 2,
18        }
19    }
20}
21
22/// Encode a mouse button press event.
23pub fn encode_mouse_press(
24    row: usize,
25    col: usize,
26    button: TerminalMouseButton,
27    encoding: MouseProtocolEncoding,
28) -> String {
29    match encoding {
30        MouseProtocolEncoding::Sgr => {
31            let sgr_row = row.saturating_add(1);
32            let sgr_col = col.saturating_add(1);
33            format!("\x1b[<{};{};{}M", button.code(), sgr_col, sgr_row)
34        }
35        _ => {
36            let button_byte = button.code().saturating_add(32);
37            let col_byte = col.saturating_add(1).min(255) as u8;
38            let row_byte = row.saturating_add(1).min(255) as u8;
39            format!(
40                "\x1b[M{}{}{}",
41                button_byte as char, col_byte as char, row_byte as char
42            )
43        }
44    }
45}
46
47/// Encode a mouse button release event.
48pub fn encode_mouse_release(
49    row: usize,
50    col: usize,
51    button: TerminalMouseButton,
52    encoding: MouseProtocolEncoding,
53) -> String {
54    match encoding {
55        MouseProtocolEncoding::Sgr => {
56            // SGR uses lowercase 'm' for release with the original button code
57            let sgr_row = row.saturating_add(1);
58            let sgr_col = col.saturating_add(1);
59            format!("\x1b[<{};{};{}m", button.code(), sgr_col, sgr_row)
60        }
61        _ => {
62            // X11: release is always button code 3 (no specific button) + 32
63            let button_byte = 35u8; // 3 + 32
64            let col_byte = col.saturating_add(1).min(255) as u8;
65            let row_byte = row.saturating_add(1).min(255) as u8;
66            format!(
67                "\x1b[M{}{}{}",
68                button_byte as char, col_byte as char, row_byte as char
69            )
70        }
71    }
72}
73
74/// Encode a mouse motion event using the specified protocol encoding.
75///
76/// When `button` is `None`, this encodes hover motion (no button pressed):
77/// button code = 3 + 32 (motion flag) = 35.
78/// When `button` is `Some`, this encodes drag motion (button held):
79/// button code = button.code() + 32 (motion flag).
80pub fn encode_mouse_move(
81    row: usize,
82    col: usize,
83    button: Option<TerminalMouseButton>,
84    encoding: MouseProtocolEncoding,
85) -> String {
86    // Motion flag = 32. No-button code = 3.
87    let code = match button {
88        Some(b) => b.code() + 32,
89        None => 3 + 32, // 35
90    };
91
92    match encoding {
93        MouseProtocolEncoding::Sgr => {
94            let sgr_row = row.saturating_add(1);
95            let sgr_col = col.saturating_add(1);
96            format!("\x1b[<{};{};{}M", code, sgr_col, sgr_row)
97        }
98        _ => {
99            let button_byte = code.saturating_add(32);
100            let col_byte = col.saturating_add(1).min(255) as u8;
101            let row_byte = row.saturating_add(1).min(255) as u8;
102            format!(
103                "\x1b[M{}{}{}",
104                button_byte as char, col_byte as char, row_byte as char
105            )
106        }
107    }
108}
109
110/// Encode a mouse wheel event using the specified protocol encoding.
111///
112/// Positive `delta_y` = wheel up (away from user), negative = wheel down.
113pub fn encode_wheel_event(
114    row: usize,
115    col: usize,
116    delta_y: f64,
117    encoding: MouseProtocolEncoding,
118) -> String {
119    // Terminal protocol: wheel up = button 64, wheel down = button 65.
120    match encoding {
121        MouseProtocolEncoding::Sgr => {
122            let button = if delta_y > 0.0 { 64 } else { 65 };
123            let sgr_row = row.saturating_add(1);
124            let sgr_col = col.saturating_add(1);
125            // Wheel events are press-only (M), no release needed
126            format!("\x1b[<{};{};{}M", button, sgr_col, sgr_row)
127        }
128        // Default and Utf8 both use the X11-style encoding
129        _ => {
130            // \x1b[M followed by 3 bytes:
131            //   Byte 1: button + 32 (wheel up = 64+32=96, wheel down = 65+32=97)
132            //   Byte 2: column (1-indexed + 32)
133            //   Byte 3: row (1-indexed + 32)
134            let button_byte = if delta_y > 0.0 { 96u8 } else { 97u8 };
135            let col_byte = col.saturating_add(1).min(255) as u8;
136            let row_byte = row.saturating_add(1).min(255) as u8;
137            format!(
138                "\x1b[M{}{}{}",
139                button_byte as char, col_byte as char, row_byte as char
140            )
141        }
142    }
143}