tui_lipan/widgets/terminal/
events.rs1use std::sync::Arc;
2
3use crate::core::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseKind};
4use crate::style::Span;
5use crate::utils::{GridSelection, GridSelectionEvent};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum TerminalInputKind {
10 Key,
12 Paste,
14 FocusIn,
16 FocusOut,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct TerminalInputEvent {
23 pub kind: TerminalInputKind,
25 pub key: Option<KeyEvent>,
27 pub bytes: Arc<[u8]>,
29}
30
31pub type TerminalSelection = GridSelection;
33
34pub type TerminalSelectionEvent = GridSelectionEvent;
36
37pub fn terminal_selection_text(lines: &[Vec<Span>], selection: &GridSelection) -> String {
38 if selection.is_empty() {
39 return String::new();
40 }
41
42 let mut row_strings = Vec::with_capacity(lines.len());
43 for line in lines {
44 let mut row = String::new();
45 for span in line {
46 row.push_str(span.content.as_ref());
47 }
48 row_strings.push(row);
49 }
50
51 selection.extract_text(&row_strings)
52}
53
54pub fn key_event_to_bytes(key: KeyEvent) -> Option<Vec<u8>> {
58 let mut bytes = match key.code {
59 KeyCode::Char(ch) => {
60 if key.mods.ctrl {
61 vec![ctrl_char(ch)?]
62 } else {
63 ch.to_string().into_bytes()
64 }
65 }
66 KeyCode::Enter => vec![b'\r'],
67 KeyCode::Tab => vec![b'\t'],
68 KeyCode::BackTab => b"\x1b[Z".to_vec(),
69 KeyCode::Backspace => vec![0x7f],
70 KeyCode::Esc => vec![0x1b],
71 KeyCode::Up => b"\x1b[A".to_vec(),
72 KeyCode::Down => b"\x1b[B".to_vec(),
73 KeyCode::Right => b"\x1b[C".to_vec(),
74 KeyCode::Left => b"\x1b[D".to_vec(),
75 KeyCode::Home => b"\x1b[H".to_vec(),
76 KeyCode::End => b"\x1b[F".to_vec(),
77 KeyCode::PageUp => b"\x1b[5~".to_vec(),
78 KeyCode::PageDown => b"\x1b[6~".to_vec(),
79 KeyCode::Insert => b"\x1b[2~".to_vec(),
80 KeyCode::Delete => b"\x1b[3~".to_vec(),
81 KeyCode::F(n) => format!(
82 "\x1b[{}",
83 match n {
84 1 => "11~",
85 2 => "12~",
86 3 => "13~",
87 4 => "14~",
88 5 => "15~",
89 6 => "17~",
90 7 => "18~",
91 8 => "19~",
92 9 => "20~",
93 10 => "21~",
94 11 => "23~",
95 12 => "24~",
96 _ => return None,
97 }
98 )
99 .into_bytes(),
100 };
101
102 if key.mods.alt {
103 let mut alt_prefixed = Vec::with_capacity(bytes.len() + 1);
104 alt_prefixed.push(0x1b);
105 alt_prefixed.extend(bytes);
106 bytes = alt_prefixed;
107 }
108
109 Some(bytes)
110}
111
112fn ctrl_char(ch: char) -> Option<u8> {
113 if ch.is_ascii_alphabetic() {
114 return Some((ch.to_ascii_uppercase() as u8) - b'@');
115 }
116
117 match ch {
118 ' ' => Some(0),
119 '[' => Some(27),
120 '\\' => Some(28),
121 ']' => Some(29),
122 '^' => Some(30),
123 '_' => Some(31),
124 _ => None,
125 }
126}
127
128#[cfg_attr(
130 feature = "terminal-serde",
131 derive(serde::Serialize, serde::Deserialize)
132)]
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
134pub enum MouseMode {
135 #[default]
137 None,
138 X10,
140 Normal,
142 AnyEvent,
144}
145
146#[cfg_attr(
148 feature = "terminal-serde",
149 derive(serde::Serialize, serde::Deserialize)
150)]
151#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub enum MouseEncoding {
153 #[default]
155 X10,
156 Sgr,
158 Utf8,
160}
161
162#[cfg_attr(
164 feature = "terminal-serde",
165 derive(serde::Serialize, serde::Deserialize)
166)]
167#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
168pub struct MouseModeState {
169 pub mode: MouseMode,
171 pub encoding: MouseEncoding,
173 pub focus_events_enabled: bool,
175}
176
177pub fn mouse_event_to_bytes(
179 event: MouseEvent,
180 encoding: MouseEncoding,
181 viewport_offset: (u16, u16),
182) -> Option<Vec<u8>> {
183 let (button_code, is_release) = match event.kind {
184 MouseKind::Down(btn) => (button_to_code(btn), false),
185 MouseKind::Up(btn) => (button_to_code(btn), true),
186 MouseKind::Drag(btn) => (button_to_code(btn).saturating_add(32), false),
187 MouseKind::ScrollUp => (64, false),
188 MouseKind::ScrollDown => (65, false),
189 MouseKind::Moved => (35, false),
192 };
193
194 let mut cb = button_code;
195 if event.mods.shift {
196 cb = cb.saturating_add(4);
197 }
198 if event.mods.alt {
199 cb = cb.saturating_add(8);
200 }
201 if event.mods.ctrl {
202 cb = cb.saturating_add(16);
203 }
204
205 let cx = event.x.saturating_sub(viewport_offset.0).saturating_add(1);
206 let cy = event.y.saturating_sub(viewport_offset.1).saturating_add(1);
207
208 match encoding {
209 MouseEncoding::Sgr => {
210 let suffix = if is_release { 'm' } else { 'M' };
211 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, suffix).into_bytes())
212 }
213 MouseEncoding::X10 => {
214 if cx > 223 || cy > 223 {
215 return None;
216 }
217 let cb = cb.saturating_add(32);
218 let cx = cx.saturating_add(32) as u8;
219 let cy = cy.saturating_add(32) as u8;
220 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
221 }
222 MouseEncoding::Utf8 => {
223 let mut out = Vec::with_capacity(6);
224 out.extend_from_slice(b"\x1b[M");
225 out.push(cb.saturating_add(32));
226 push_utf8_coord(&mut out, cx.saturating_add(32))?;
227 push_utf8_coord(&mut out, cy.saturating_add(32))?;
228 Some(out)
229 }
230 }
231}
232
233#[cfg(all(test, feature = "terminal-serde"))]
234mod terminal_serde_tests {
235 use super::*;
236
237 #[test]
238 fn mouse_mode_state_round_trips() {
239 let state = MouseModeState {
240 mode: MouseMode::AnyEvent,
241 encoding: MouseEncoding::Sgr,
242 focus_events_enabled: true,
243 };
244 let json = serde_json::to_string(&state).unwrap();
245 assert_eq!(
246 serde_json::from_str::<MouseModeState>(&json).unwrap(),
247 state
248 );
249 }
250}
251
252fn push_utf8_coord(out: &mut Vec<u8>, value: u16) -> Option<()> {
253 let mut buffer = [0u8; 4];
254 let ch = char::from_u32(u32::from(value))?;
255 let encoded = ch.encode_utf8(&mut buffer);
256 out.extend_from_slice(encoded.as_bytes());
257 Some(())
258}
259
260fn button_to_code(btn: MouseButton) -> u8 {
261 match btn {
262 MouseButton::Left => 0,
263 MouseButton::Middle => 1,
264 MouseButton::Right => 2,
265 }
266}
267
268pub fn focus_in_sequence() -> &'static [u8] {
270 b"\x1b[I"
271}
272
273pub fn focus_out_sequence() -> &'static [u8] {
275 b"\x1b[O"
276}
277
278pub fn focus_sequences() -> (&'static [u8], &'static [u8]) {
280 (focus_in_sequence(), focus_out_sequence())
281}
282
283pub fn wrap_bracketed_paste(text: &str) -> Vec<u8> {
285 let (start, end) = paste_sequences();
286 let mut out = Vec::with_capacity(text.len() + start.len() + end.len());
287 out.extend_from_slice(start);
288 out.extend_from_slice(text.as_bytes());
289 out.extend_from_slice(end);
290 out
291}
292
293pub fn paste_sequences() -> (&'static [u8], &'static [u8]) {
295 (b"\x1b[200~", b"\x1b[201~")
296}