use std::sync::Arc;
use crate::core::event::{KeyCode, KeyEvent, KeyMods, MouseButton, MouseEvent, MouseKind};
use crate::style::Span;
use crate::utils::spans::{line_text, line_width, slice_columns};
use crate::utils::{GridSelection, SelectionEnd};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TerminalInputKind {
Key,
Paste,
FocusIn,
FocusOut,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TerminalInputEvent {
pub kind: TerminalInputKind,
pub key: Option<KeyEvent>,
pub bytes: Arc<[u8]>,
}
pub fn terminal_selection_text(lines: &[Vec<Span>], selection: &GridSelection) -> String {
terminal_selection_text_with(lines, selection, SelectionEnd::Exclusive, false)
}
pub(crate) fn terminal_selection_text_with(
lines: &[Vec<Span>],
selection: &GridSelection,
endpoint: SelectionEnd,
trim_row_end: bool,
) -> String {
if selection.is_empty() && matches!(endpoint, SelectionEnd::Exclusive) {
return String::new();
}
let (start, end) = selection.normalized();
let mut result = String::new();
for row in start.row..=end.row {
let Some(line) = lines.get(row) else { continue };
let width = line_width(line);
let col_start = if row == start.row { start.col } else { 0 };
let col_end = if row == end.row {
end.col
.saturating_add(matches!(endpoint, SelectionEnd::Inclusive) as usize)
} else {
width
};
let mut text = line_text(&slice_columns(line, col_start, col_end));
if trim_row_end {
text.truncate(text.trim_end().len());
}
result.push_str(&text);
if row < end.row {
result.push('\n');
}
}
result
}
#[cfg_attr(
feature = "terminal-serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct KittyKeyboardFlags {
pub disambiguate_escape_codes: bool,
pub report_event_types: bool,
pub report_alternate_keys: bool,
pub report_all_keys_as_escape_codes: bool,
pub report_associated_text: bool,
}
impl KittyKeyboardFlags {
pub fn any(&self) -> bool {
self.disambiguate_escape_codes
|| self.report_event_types
|| self.report_alternate_keys
|| self.report_all_keys_as_escape_codes
|| self.report_associated_text
}
}
#[cfg_attr(
feature = "terminal-serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TerminalKeyModes {
pub app_cursor: bool,
pub bracketed_paste: bool,
pub kitty_keyboard: KittyKeyboardFlags,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TerminalPasteShortcutBehavior {
#[default]
Forward,
Performable,
}
pub fn key_event_to_bytes(key: KeyEvent, modes: TerminalKeyModes) -> Option<Vec<u8>> {
if key.mods.super_key {
return None;
}
if modes.kitty_keyboard.disambiguate_escape_codes
&& let Some(bytes) = kitty_csi_u_bytes(key.code, key.mods)
{
return Some(bytes);
}
if let Some(bytes) = modified_special_key_bytes(key.code, key.mods) {
return Some(bytes);
}
if key.mods.ctrl && key.code == KeyCode::Backspace {
return Some(vec![0x1b, 0x7f]);
}
let mut bytes = match key.code {
KeyCode::Char(ch) => {
if key.mods.ctrl {
vec![ctrl_char(ch)?]
} else {
ch.to_string().into_bytes()
}
}
KeyCode::Enter => vec![b'\r'],
KeyCode::Tab => vec![b'\t'],
KeyCode::BackTab => b"\x1b[Z".to_vec(),
KeyCode::Backspace => vec![0x7f],
KeyCode::Esc => vec![0x1b],
KeyCode::Up => cursor_key_bytes(b'A', modes),
KeyCode::Down => cursor_key_bytes(b'B', modes),
KeyCode::Right => cursor_key_bytes(b'C', modes),
KeyCode::Left => cursor_key_bytes(b'D', modes),
KeyCode::Home => cursor_key_bytes(b'H', modes),
KeyCode::End => cursor_key_bytes(b'F', modes),
KeyCode::PageUp => b"\x1b[5~".to_vec(),
KeyCode::PageDown => b"\x1b[6~".to_vec(),
KeyCode::Insert => b"\x1b[2~".to_vec(),
KeyCode::Delete => b"\x1b[3~".to_vec(),
KeyCode::F(n) => format!("\x1b[{}~", f_key_number(n)?).into_bytes(),
};
if key.mods.alt {
let mut alt_prefixed = Vec::with_capacity(bytes.len() + 1);
alt_prefixed.push(0x1b);
alt_prefixed.extend(bytes);
bytes = alt_prefixed;
}
Some(bytes)
}
fn cursor_key_bytes(final_byte: u8, modes: TerminalKeyModes) -> Vec<u8> {
let introducer: &[u8] = if modes.app_cursor { b"\x1bO" } else { b"\x1b[" };
let mut bytes = Vec::with_capacity(3);
bytes.extend_from_slice(introducer);
bytes.push(final_byte);
bytes
}
fn xterm_modifier_param(mods: KeyMods) -> u8 {
1 + u8::from(mods.shift) + 2 * u8::from(mods.alt) + 4 * u8::from(mods.ctrl)
}
fn f_key_number(n: u8) -> Option<u8> {
Some(match n {
1 => 11,
2 => 12,
3 => 13,
4 => 14,
5 => 15,
6 => 17,
7 => 18,
8 => 19,
9 => 20,
10 => 21,
11 => 23,
12 => 24,
13 => 25,
14 => 26,
15 => 28,
16 => 29,
17 => 31,
18 => 32,
19 => 33,
20 => 34,
_ => return None,
})
}
fn kitty_csi_u_bytes(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
let (codepoint, mods) = match code {
KeyCode::Char(ch) if mods.ctrl || mods.alt => (kitty_char_codepoint(ch), mods),
KeyCode::Esc => (27, mods),
KeyCode::Enter if !mods.is_empty() => (13, mods),
KeyCode::Tab if mods.ctrl || mods.alt => (9, mods),
KeyCode::BackTab => (
9,
KeyMods {
shift: true,
..mods
},
),
KeyCode::Backspace if mods.ctrl || mods.alt => (127, mods),
_ => return None,
};
let m = xterm_modifier_param(mods);
let seq = if m == 1 {
format!("\x1b[{codepoint}u")
} else {
format!("\x1b[{codepoint};{m}u")
};
Some(seq.into_bytes())
}
fn kitty_char_codepoint(ch: char) -> u32 {
ch.to_lowercase().next().unwrap_or(ch) as u32
}
fn shift_reserved_by_emulator(code: KeyCode, mods: KeyMods) -> bool {
mods.shift
&& !mods.ctrl
&& !mods.alt
&& matches!(code, KeyCode::Insert | KeyCode::PageUp | KeyCode::PageDown)
}
fn modified_special_key_bytes(code: KeyCode, mods: KeyMods) -> Option<Vec<u8>> {
if (!mods.ctrl && !mods.shift) || shift_reserved_by_emulator(code, mods) {
return None;
}
let m = xterm_modifier_param(mods);
let seq = match code {
KeyCode::Up => format!("\x1b[1;{m}A"),
KeyCode::Down => format!("\x1b[1;{m}B"),
KeyCode::Right => format!("\x1b[1;{m}C"),
KeyCode::Left => format!("\x1b[1;{m}D"),
KeyCode::Home => format!("\x1b[1;{m}H"),
KeyCode::End => format!("\x1b[1;{m}F"),
KeyCode::Insert => format!("\x1b[2;{m}~"),
KeyCode::Delete => format!("\x1b[3;{m}~"),
KeyCode::PageUp => format!("\x1b[5;{m}~"),
KeyCode::PageDown => format!("\x1b[6;{m}~"),
KeyCode::F(n) => format!("\x1b[{};{m}~", f_key_number(n)?),
_ => return None,
};
Some(seq.into_bytes())
}
fn ctrl_char(ch: char) -> Option<u8> {
if ch.is_ascii_alphabetic() {
return Some((ch.to_ascii_uppercase() as u8) - b'@');
}
Some(match ch {
' ' | '@' => 0x00,
'[' => 0x1b,
'\\' => 0x1c,
']' => 0x1d,
'^' => 0x1e,
'_' | '/' => 0x1f,
'?' => 0x7f,
'2' => 0x00,
'3' => 0x1b,
'4' => 0x1c,
'5' => 0x1d,
'6' => 0x1e,
'7' => 0x1f,
'8' => 0x7f,
_ => return None,
})
}
#[cfg_attr(
feature = "terminal-serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MouseMode {
#[default]
None,
X10,
Normal,
AnyEvent,
}
#[cfg_attr(
feature = "terminal-serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MouseEncoding {
#[default]
X10,
Sgr,
Utf8,
}
#[cfg_attr(
feature = "terminal-serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MouseModeState {
pub mode: MouseMode,
pub encoding: MouseEncoding,
pub focus_events_enabled: bool,
}
pub fn mouse_event_to_bytes(
event: MouseEvent,
encoding: MouseEncoding,
viewport_offset: (u16, u16),
) -> Option<Vec<u8>> {
let (button_code, is_release) = match event.kind {
MouseKind::Down(btn) => (button_to_code(btn), false),
MouseKind::Up(btn) => (button_to_code(btn), true),
MouseKind::Drag(btn) => (button_to_code(btn).saturating_add(32), false),
MouseKind::ScrollUp => (64, false),
MouseKind::ScrollDown => (65, false),
MouseKind::Moved => (35, false),
};
let mut cb = button_code;
if event.mods.shift {
cb = cb.saturating_add(4);
}
if event.mods.alt {
cb = cb.saturating_add(8);
}
if event.mods.ctrl {
cb = cb.saturating_add(16);
}
let cx = event.x.saturating_sub(viewport_offset.0).saturating_add(1);
let cy = event.y.saturating_sub(viewport_offset.1).saturating_add(1);
match encoding {
MouseEncoding::Sgr => {
let suffix = if is_release { 'm' } else { 'M' };
Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, suffix).into_bytes())
}
MouseEncoding::X10 => {
if cx > 223 || cy > 223 {
return None;
}
let cb = cb.saturating_add(32);
let cx = cx.saturating_add(32) as u8;
let cy = cy.saturating_add(32) as u8;
Some(vec![0x1b, b'[', b'M', cb, cx, cy])
}
MouseEncoding::Utf8 => {
let mut out = Vec::with_capacity(6);
out.extend_from_slice(b"\x1b[M");
out.push(cb.saturating_add(32));
push_utf8_coord(&mut out, cx.saturating_add(32))?;
push_utf8_coord(&mut out, cy.saturating_add(32))?;
Some(out)
}
}
}
#[cfg(all(test, feature = "terminal-serde"))]
mod terminal_serde_tests {
use super::*;
#[test]
fn mouse_mode_state_round_trips() {
let state = MouseModeState {
mode: MouseMode::AnyEvent,
encoding: MouseEncoding::Sgr,
focus_events_enabled: true,
};
let json = serde_json::to_string(&state).unwrap();
assert_eq!(
serde_json::from_str::<MouseModeState>(&json).unwrap(),
state
);
}
}
fn push_utf8_coord(out: &mut Vec<u8>, value: u16) -> Option<()> {
let mut buffer = [0u8; 4];
let ch = char::from_u32(u32::from(value))?;
let encoded = ch.encode_utf8(&mut buffer);
out.extend_from_slice(encoded.as_bytes());
Some(())
}
fn button_to_code(btn: MouseButton) -> u8 {
match btn {
MouseButton::Left => 0,
MouseButton::Middle => 1,
MouseButton::Right => 2,
}
}
pub fn focus_in_sequence() -> &'static [u8] {
b"\x1b[I"
}
pub fn focus_out_sequence() -> &'static [u8] {
b"\x1b[O"
}
pub fn focus_sequences() -> (&'static [u8], &'static [u8]) {
(focus_in_sequence(), focus_out_sequence())
}
pub fn encode_paste(text: &str, modes: TerminalKeyModes) -> Vec<u8> {
if !modes.bracketed_paste {
return text.as_bytes().to_vec();
}
let (start, end) = paste_sequences();
let mut out = Vec::with_capacity(text.len() + start.len() + end.len());
out.extend_from_slice(start);
out.extend_from_slice(text.as_bytes());
out.extend_from_slice(end);
out
}
pub fn paste_sequences() -> (&'static [u8], &'static [u8]) {
(b"\x1b[200~", b"\x1b[201~")
}
#[cfg(test)]
mod selection_tests {
use super::*;
#[test]
fn terminal_selection_text_uses_display_columns_for_wide_characters() {
use crate::utils::GridPos;
let lines = vec![vec![Span::new("a界🙂b")]];
let mut cjk = GridSelection::new(GridPos { row: 0, col: 1 });
cjk.extend_to(GridPos { row: 0, col: 3 });
assert_eq!(terminal_selection_text(&lines, &cjk), "界");
let mut emoji = GridSelection::new(GridPos { row: 0, col: 3 });
emoji.extend_to(GridPos { row: 0, col: 5 });
assert_eq!(terminal_selection_text(&lines, &emoji), "🙂");
}
}