1use std::sync::Arc;
2
3use crate::core::event::{KeyCode, KeyEvent, KeyMods, MouseButton, MouseEvent, MouseKind};
4use crate::style::Span;
5use crate::utils::spans::{line_text, line_width, slice_columns};
6use crate::utils::{GridSelection, SelectionEnd};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum TerminalInputKind {
11 Key,
13 Paste,
15 FocusIn,
17 FocusOut,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct TerminalInputEvent {
24 pub kind: TerminalInputKind,
26 pub key: Option<KeyEvent>,
28 pub bytes: Arc<[u8]>,
30}
31
32pub fn terminal_selection_text(lines: &[Vec<Span>], selection: &GridSelection) -> String {
38 terminal_selection_text_with(lines, selection, SelectionEnd::Exclusive, false)
39}
40
41pub(crate) fn terminal_selection_text_with(
43 lines: &[Vec<Span>],
44 selection: &GridSelection,
45 endpoint: SelectionEnd,
46 trim_row_end: bool,
47) -> String {
48 if selection.is_empty() && matches!(endpoint, SelectionEnd::Exclusive) {
49 return String::new();
50 }
51
52 let (start, end) = selection.normalized();
53 let mut result = String::new();
54 for row in start.row..=end.row {
55 let Some(line) = lines.get(row) else { continue };
56 let width = line_width(line);
57 let col_start = if row == start.row { start.col } else { 0 };
58 let col_end = if row == end.row {
59 end.col
60 .saturating_add(matches!(endpoint, SelectionEnd::Inclusive) as usize)
61 } else {
62 width
63 };
64 let mut text = line_text(&slice_columns(line, col_start, col_end));
65 if trim_row_end {
66 text.truncate(text.trim_end().len());
67 }
68 result.push_str(&text);
69 if row < end.row {
70 result.push('\n');
71 }
72 }
73 result
74}
75
76#[cfg_attr(
84 feature = "terminal-serde",
85 derive(serde::Serialize, serde::Deserialize)
86)]
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct KittyKeyboardFlags {
89 pub disambiguate_escape_codes: bool,
92 pub report_event_types: bool,
94 pub report_alternate_keys: bool,
96 pub report_all_keys_as_escape_codes: bool,
98 pub report_associated_text: bool,
100}
101
102impl KittyKeyboardFlags {
103 pub fn any(&self) -> bool {
105 self.disambiguate_escape_codes
106 || self.report_event_types
107 || self.report_alternate_keys
108 || self.report_all_keys_as_escape_codes
109 || self.report_associated_text
110 }
111}
112
113#[cfg_attr(
121 feature = "terminal-serde",
122 derive(serde::Serialize, serde::Deserialize)
123)]
124#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
125pub struct TerminalKeyModes {
126 pub app_cursor: bool,
129 pub bracketed_paste: bool,
133 pub kitty_keyboard: KittyKeyboardFlags,
135}
136
137#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
139pub enum TerminalPasteShortcutBehavior {
140 #[default]
142 Forward,
143 Performable,
146}
147
148pub fn key_event_to_bytes(key: KeyEvent, modes: TerminalKeyModes) -> Option<Vec<u8>> {
163 if key.mods.super_key {
167 return None;
168 }
169
170 if modes.kitty_keyboard.disambiguate_escape_codes
173 && let Some(bytes) = kitty_csi_u_bytes(key.code, key.mods)
174 {
175 return Some(bytes);
176 }
177
178 if let Some(bytes) = modified_special_key_bytes(key.code, key.mods) {
179 return Some(bytes);
180 }
181
182 if key.mods.ctrl && key.code == KeyCode::Backspace {
188 return Some(vec![0x1b, 0x7f]);
189 }
190
191 let mut bytes = match key.code {
192 KeyCode::Char(ch) => {
193 if key.mods.ctrl {
194 vec![ctrl_char(ch)?]
195 } else {
196 ch.to_string().into_bytes()
197 }
198 }
199 KeyCode::Enter => vec![b'\r'],
200 KeyCode::Tab => vec![b'\t'],
201 KeyCode::BackTab => b"\x1b[Z".to_vec(),
202 KeyCode::Backspace => vec![0x7f],
203 KeyCode::Esc => vec![0x1b],
204 KeyCode::Up => cursor_key_bytes(b'A', modes),
205 KeyCode::Down => cursor_key_bytes(b'B', modes),
206 KeyCode::Right => cursor_key_bytes(b'C', modes),
207 KeyCode::Left => cursor_key_bytes(b'D', modes),
208 KeyCode::Home => cursor_key_bytes(b'H', modes),
209 KeyCode::End => cursor_key_bytes(b'F', modes),
210 KeyCode::PageUp => b"\x1b[5~".to_vec(),
211 KeyCode::PageDown => b"\x1b[6~".to_vec(),
212 KeyCode::Insert => b"\x1b[2~".to_vec(),
213 KeyCode::Delete => b"\x1b[3~".to_vec(),
214 KeyCode::F(n) => format!("\x1b[{}~", f_key_number(n)?).into_bytes(),
215 };
216
217 if key.mods.alt {
218 let mut alt_prefixed = Vec::with_capacity(bytes.len() + 1);
219 alt_prefixed.push(0x1b);
220 alt_prefixed.extend(bytes);
221 bytes = alt_prefixed;
222 }
223
224 Some(bytes)
225}
226
227fn cursor_key_bytes(final_byte: u8, modes: TerminalKeyModes) -> Vec<u8> {
231 let introducer: &[u8] = if modes.app_cursor { b"\x1bO" } else { b"\x1b[" };
232 let mut bytes = Vec::with_capacity(3);
233 bytes.extend_from_slice(introducer);
234 bytes.push(final_byte);
235 bytes
236}
237
238fn xterm_modifier_param(mods: KeyMods) -> u8 {
242 1 + u8::from(mods.shift) + 2 * u8::from(mods.alt) + 4 * u8::from(mods.ctrl)
243}
244
245fn f_key_number(n: u8) -> Option<u8> {
248 Some(match n {
249 1 => 11,
250 2 => 12,
251 3 => 13,
252 4 => 14,
253 5 => 15,
254 6 => 17,
255 7 => 18,
256 8 => 19,
257 9 => 20,
258 10 => 21,
259 11 => 23,
260 12 => 24,
261 13 => 25,
262 14 => 26,
263 15 => 28,
264 16 => 29,
265 17 => 31,
266 18 => 32,
267 19 => 33,
268 20 => 34,
269 _ => return None,
270 })
271}
272
273fn kitty_csi_u_bytes(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
283 let (codepoint, mods) = match code {
284 KeyCode::Char(ch) if mods.ctrl || mods.alt => (kitty_char_codepoint(ch), mods),
287 KeyCode::Esc => (27, mods),
290 KeyCode::Enter if !mods.is_empty() => (13, mods),
291 KeyCode::Tab if mods.ctrl || mods.alt => (9, mods),
292 KeyCode::BackTab => (
295 9,
296 KeyMods {
297 shift: true,
298 ..mods
299 },
300 ),
301 KeyCode::Backspace if mods.ctrl || mods.alt => (127, mods),
302 _ => return None,
303 };
304
305 let m = xterm_modifier_param(mods);
306 let seq = if m == 1 {
307 format!("\x1b[{codepoint}u")
308 } else {
309 format!("\x1b[{codepoint};{m}u")
310 };
311 Some(seq.into_bytes())
312}
313
314fn kitty_char_codepoint(ch: char) -> u32 {
317 ch.to_lowercase().next().unwrap_or(ch) as u32
318}
319
320fn shift_reserved_by_emulator(code: KeyCode, mods: KeyMods) -> bool {
328 mods.shift
329 && !mods.ctrl
330 && !mods.alt
331 && matches!(code, KeyCode::Insert | KeyCode::PageUp | KeyCode::PageDown)
332}
333
334fn modified_special_key_bytes(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
343 if (!mods.ctrl && !mods.shift) || shift_reserved_by_emulator(code, mods) {
344 return None;
345 }
346
347 let m = xterm_modifier_param(mods);
348 let seq = match code {
349 KeyCode::Up => format!("\x1b[1;{m}A"),
350 KeyCode::Down => format!("\x1b[1;{m}B"),
351 KeyCode::Right => format!("\x1b[1;{m}C"),
352 KeyCode::Left => format!("\x1b[1;{m}D"),
353 KeyCode::Home => format!("\x1b[1;{m}H"),
354 KeyCode::End => format!("\x1b[1;{m}F"),
355 KeyCode::Insert => format!("\x1b[2;{m}~"),
356 KeyCode::Delete => format!("\x1b[3;{m}~"),
357 KeyCode::PageUp => format!("\x1b[5;{m}~"),
358 KeyCode::PageDown => format!("\x1b[6;{m}~"),
359 KeyCode::F(n) => format!("\x1b[{};{m}~", f_key_number(n)?),
360 _ => return None,
361 };
362 Some(seq.into_bytes())
363}
364
365fn ctrl_char(ch: char) -> Option<u8> {
368 if ch.is_ascii_alphabetic() {
369 return Some((ch.to_ascii_uppercase() as u8) - b'@');
370 }
371
372 Some(match ch {
373 ' ' | '@' => 0x00,
374 '[' => 0x1b,
375 '\\' => 0x1c,
376 ']' => 0x1d,
377 '^' => 0x1e,
378 '_' | '/' => 0x1f,
380 '?' => 0x7f,
381 '2' => 0x00,
383 '3' => 0x1b,
384 '4' => 0x1c,
385 '5' => 0x1d,
386 '6' => 0x1e,
387 '7' => 0x1f,
388 '8' => 0x7f,
389 _ => return None,
390 })
391}
392
393#[cfg_attr(
395 feature = "terminal-serde",
396 derive(serde::Serialize, serde::Deserialize)
397)]
398#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
399pub enum MouseMode {
400 #[default]
402 None,
403 X10,
405 Normal,
407 AnyEvent,
409}
410
411#[cfg_attr(
413 feature = "terminal-serde",
414 derive(serde::Serialize, serde::Deserialize)
415)]
416#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
417pub enum MouseEncoding {
418 #[default]
420 X10,
421 Sgr,
423 Utf8,
425}
426
427#[cfg_attr(
429 feature = "terminal-serde",
430 derive(serde::Serialize, serde::Deserialize)
431)]
432#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
433pub struct MouseModeState {
434 pub mode: MouseMode,
436 pub encoding: MouseEncoding,
438 pub focus_events_enabled: bool,
440}
441
442pub fn mouse_event_to_bytes(
444 event: MouseEvent,
445 encoding: MouseEncoding,
446 viewport_offset: (u16, u16),
447) -> Option<Vec<u8>> {
448 let (button_code, is_release) = match event.kind {
449 MouseKind::Down(btn) => (button_to_code(btn), false),
450 MouseKind::Up(btn) => (button_to_code(btn), true),
451 MouseKind::Drag(btn) => (button_to_code(btn).saturating_add(32), false),
452 MouseKind::ScrollUp => (64, false),
453 MouseKind::ScrollDown => (65, false),
454 MouseKind::Moved => (35, false),
457 };
458
459 let mut cb = button_code;
460 if event.mods.shift {
461 cb = cb.saturating_add(4);
462 }
463 if event.mods.alt {
464 cb = cb.saturating_add(8);
465 }
466 if event.mods.ctrl {
467 cb = cb.saturating_add(16);
468 }
469
470 let cx = event.x.saturating_sub(viewport_offset.0).saturating_add(1);
471 let cy = event.y.saturating_sub(viewport_offset.1).saturating_add(1);
472
473 match encoding {
474 MouseEncoding::Sgr => {
475 let suffix = if is_release { 'm' } else { 'M' };
476 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, suffix).into_bytes())
477 }
478 MouseEncoding::X10 => {
479 if cx > 223 || cy > 223 {
480 return None;
481 }
482 let cb = cb.saturating_add(32);
483 let cx = cx.saturating_add(32) as u8;
484 let cy = cy.saturating_add(32) as u8;
485 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
486 }
487 MouseEncoding::Utf8 => {
488 let mut out = Vec::with_capacity(6);
489 out.extend_from_slice(b"\x1b[M");
490 out.push(cb.saturating_add(32));
491 push_utf8_coord(&mut out, cx.saturating_add(32))?;
492 push_utf8_coord(&mut out, cy.saturating_add(32))?;
493 Some(out)
494 }
495 }
496}
497
498#[cfg(all(test, feature = "terminal-serde"))]
499mod terminal_serde_tests {
500 use super::*;
501
502 #[test]
503 fn mouse_mode_state_round_trips() {
504 let state = MouseModeState {
505 mode: MouseMode::AnyEvent,
506 encoding: MouseEncoding::Sgr,
507 focus_events_enabled: true,
508 };
509 let json = serde_json::to_string(&state).unwrap();
510 assert_eq!(
511 serde_json::from_str::<MouseModeState>(&json).unwrap(),
512 state
513 );
514 }
515}
516
517fn push_utf8_coord(out: &mut Vec<u8>, value: u16) -> Option<()> {
518 let mut buffer = [0u8; 4];
519 let ch = char::from_u32(u32::from(value))?;
520 let encoded = ch.encode_utf8(&mut buffer);
521 out.extend_from_slice(encoded.as_bytes());
522 Some(())
523}
524
525fn button_to_code(btn: MouseButton) -> u8 {
526 match btn {
527 MouseButton::Left => 0,
528 MouseButton::Middle => 1,
529 MouseButton::Right => 2,
530 }
531}
532
533pub fn focus_in_sequence() -> &'static [u8] {
535 b"\x1b[I"
536}
537
538pub fn focus_out_sequence() -> &'static [u8] {
540 b"\x1b[O"
541}
542
543pub fn focus_sequences() -> (&'static [u8], &'static [u8]) {
545 (focus_in_sequence(), focus_out_sequence())
546}
547
548pub fn encode_paste(text: &str, modes: TerminalKeyModes) -> Vec<u8> {
554 if !modes.bracketed_paste {
555 return text.as_bytes().to_vec();
556 }
557
558 let (start, end) = paste_sequences();
559 let mut out = Vec::with_capacity(text.len() + start.len() + end.len());
560 out.extend_from_slice(start);
561 out.extend_from_slice(text.as_bytes());
562 out.extend_from_slice(end);
563 out
564}
565
566pub fn paste_sequences() -> (&'static [u8], &'static [u8]) {
568 (b"\x1b[200~", b"\x1b[201~")
569}
570
571#[cfg(test)]
572mod selection_tests {
573 use super::*;
574
575 #[test]
576 fn terminal_selection_text_uses_display_columns_for_wide_characters() {
577 use crate::utils::GridPos;
578
579 let lines = vec![vec![Span::new("a界🙂b")]];
580 let mut cjk = GridSelection::new(GridPos { row: 0, col: 1 });
581 cjk.extend_to(GridPos { row: 0, col: 3 });
582 assert_eq!(terminal_selection_text(&lines, &cjk), "界");
583
584 let mut emoji = GridSelection::new(GridPos { row: 0, col: 3 });
585 emoji.extend_to(GridPos { row: 0, col: 5 });
586 assert_eq!(terminal_selection_text(&lines, &emoji), "🙂");
587 }
588}