freya_terminal/
parser.rs

1use vt100::{
2    MouseProtocolEncoding,
3    Parser,
4};
5
6/// Check for terminal queries in PTY output and return appropriate responses.
7///
8/// This handles DSR, DA, and other queries that shells like nushell send.
9pub(crate) fn check_for_terminal_queries(data: &[u8], parser: &Parser) -> Vec<Vec<u8>> {
10    let mut responses = Vec::new();
11
12    // DSR 6n - Cursor Position Report
13    if data.windows(4).any(|w| w == b"\x1b[6n") {
14        let (row, col) = parser.screen().cursor_position();
15        let response = format!("\x1b[{};{}R", row + 1, col + 1);
16        responses.push(response.into_bytes());
17    }
18
19    // DSR ?6n - Extended Cursor Position Report
20    if data.windows(5).any(|w| w == b"\x1b[?6n") {
21        let (row, col) = parser.screen().cursor_position();
22        let response = format!("\x1b[?{};{}R", row + 1, col + 1);
23        responses.push(response.into_bytes());
24    }
25
26    // DSR 5n - Device Status Report (terminal OK)
27    if data.windows(4).any(|w| w == b"\x1b[5n") {
28        responses.push(b"\x1b[0n".to_vec());
29    }
30
31    // DA1 - Primary Device Attributes
32    if data.windows(3).any(|w| w == b"\x1b[c") || data.windows(4).any(|w| w == b"\x1b[0c") {
33        responses.push(b"\x1b[?62;22c".to_vec());
34    }
35
36    // DA2 - Secondary Device Attributes
37    if data.windows(4).any(|w| w == b"\x1b>c") || data.windows(5).any(|w| w == b"\x1b>0c") {
38        responses.push(b"\x1b[>0;0;0c".to_vec());
39    }
40
41    responses
42}
43
44/// Mouse button for terminal encoding.
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum TerminalMouseButton {
47    Left,
48    Middle,
49    Right,
50}
51
52impl TerminalMouseButton {
53    /// X11/SGR button code (without modifier bits).
54    fn code(self) -> u8 {
55        match self {
56            Self::Left => 0,
57            Self::Middle => 1,
58            Self::Right => 2,
59        }
60    }
61}
62
63/// Encode a mouse button press event.
64pub fn encode_mouse_press(
65    row: usize,
66    col: usize,
67    button: TerminalMouseButton,
68    encoding: MouseProtocolEncoding,
69) -> String {
70    match encoding {
71        MouseProtocolEncoding::Sgr => {
72            let sgr_row = row.saturating_add(1);
73            let sgr_col = col.saturating_add(1);
74            format!("\x1b[<{};{};{}M", button.code(), sgr_col, sgr_row)
75        }
76        _ => {
77            let button_byte = button.code().saturating_add(32);
78            let col_byte = col.saturating_add(1).min(255) as u8;
79            let row_byte = row.saturating_add(1).min(255) as u8;
80            format!(
81                "\x1b[M{}{}{}",
82                button_byte as char, col_byte as char, row_byte as char
83            )
84        }
85    }
86}
87
88/// Encode a mouse button release event.
89pub fn encode_mouse_release(
90    row: usize,
91    col: usize,
92    button: TerminalMouseButton,
93    encoding: MouseProtocolEncoding,
94) -> String {
95    match encoding {
96        MouseProtocolEncoding::Sgr => {
97            // SGR uses lowercase 'm' for release with the original button code
98            let sgr_row = row.saturating_add(1);
99            let sgr_col = col.saturating_add(1);
100            format!("\x1b[<{};{};{}m", button.code(), sgr_col, sgr_row)
101        }
102        _ => {
103            // X11: release is always button code 3 (no specific button) + 32
104            let button_byte = 35u8; // 3 + 32
105            let col_byte = col.saturating_add(1).min(255) as u8;
106            let row_byte = row.saturating_add(1).min(255) as u8;
107            format!(
108                "\x1b[M{}{}{}",
109                button_byte as char, col_byte as char, row_byte as char
110            )
111        }
112    }
113}
114
115/// Encode a mouse motion event using the specified protocol encoding.
116///
117/// When `button` is `None`, this encodes hover motion (no button pressed):
118/// button code = 3 + 32 (motion flag) = 35.
119/// When `button` is `Some`, this encodes drag motion (button held):
120/// button code = button.code() + 32 (motion flag).
121pub fn encode_mouse_move(
122    row: usize,
123    col: usize,
124    button: Option<TerminalMouseButton>,
125    encoding: MouseProtocolEncoding,
126) -> String {
127    // Motion flag = 32. No-button code = 3.
128    let code = match button {
129        Some(b) => b.code() + 32,
130        None => 3 + 32, // 35
131    };
132
133    match encoding {
134        MouseProtocolEncoding::Sgr => {
135            let sgr_row = row.saturating_add(1);
136            let sgr_col = col.saturating_add(1);
137            format!("\x1b[<{};{};{}M", code, sgr_col, sgr_row)
138        }
139        _ => {
140            let button_byte = code.saturating_add(32);
141            let col_byte = col.saturating_add(1).min(255) as u8;
142            let row_byte = row.saturating_add(1).min(255) as u8;
143            format!(
144                "\x1b[M{}{}{}",
145                button_byte as char, col_byte as char, row_byte as char
146            )
147        }
148    }
149}
150
151/// Encode a mouse wheel event using the specified protocol encoding.
152///
153/// Positive `delta_y` = wheel up (away from user), negative = wheel down.
154pub fn encode_wheel_event(
155    row: usize,
156    col: usize,
157    delta_y: f64,
158    encoding: MouseProtocolEncoding,
159) -> String {
160    // Terminal protocol: wheel up = button 64, wheel down = button 65.
161    match encoding {
162        MouseProtocolEncoding::Sgr => {
163            let button = if delta_y > 0.0 { 64 } else { 65 };
164            let sgr_row = row.saturating_add(1);
165            let sgr_col = col.saturating_add(1);
166            // Wheel events are press-only (M), no release needed
167            format!("\x1b[<{};{};{}M", button, sgr_col, sgr_row)
168        }
169        // Default and Utf8 both use the X11-style encoding
170        _ => {
171            // \x1b[M followed by 3 bytes:
172            //   Byte 1: button + 32 (wheel up = 64+32=96, wheel down = 65+32=97)
173            //   Byte 2: column (1-indexed + 32)
174            //   Byte 3: row (1-indexed + 32)
175            let button_byte = if delta_y > 0.0 { 96u8 } else { 97u8 };
176            let col_byte = col.saturating_add(1).min(255) as u8;
177            let row_byte = row.saturating_add(1).min(255) as u8;
178            format!(
179                "\x1b[M{}{}{}",
180                button_byte as char, col_byte as char, row_byte as char
181            )
182        }
183    }
184}