mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
294 lines
9.8 KiB
Rust
294 lines
9.8 KiB
Rust
use std::str::{self, FromStr};
|
|
|
|
use compact_str::CompactString;
|
|
|
|
use super::parser::Parser;
|
|
use crate::{ParseError, Result, bail, event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, Modifiers, MouseEvent, MouseEventKind}};
|
|
|
|
impl Parser {
|
|
pub(super) fn parse_csi(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B["));
|
|
|
|
if seq.len() == 2 {
|
|
return Err(ParseError::Incomplete);
|
|
}
|
|
|
|
let first = seq[2];
|
|
let second = seq.get(3);
|
|
let last = seq[seq.len() - 1];
|
|
|
|
Ok(match first {
|
|
b'[' => match second {
|
|
None => return Err(ParseError::Incomplete),
|
|
Some(b @ b'A'..=b'E') => Event::Key(KeyCode::Fn(1 + b - b'A').into()),
|
|
Some(_) => bail!(),
|
|
},
|
|
b'D' => Event::Key(KeyCode::Left.into()),
|
|
b'C' => Event::Key(KeyCode::Right.into()),
|
|
b'A' => Event::Key(KeyCode::Up.into()),
|
|
b'B' => Event::Key(KeyCode::Down.into()),
|
|
b'H' => Event::Key(KeyCode::Home.into()),
|
|
b'F' => Event::Key(KeyCode::End.into()),
|
|
b'Z' => Event::Key(KeyEvent::new(KeyCode::Tab, Modifiers::SHIFT)),
|
|
b'M' => return self.parse_csi_normal_mouse(),
|
|
b'<' => return self.parse_csi_sgr_mouse(),
|
|
b'I' => Event::FocusIn,
|
|
b'O' => Event::FocusOut,
|
|
b';' => return self.parse_csi_modifier_key(),
|
|
// P, Q, and S for compatibility with Kitty keyboard protocol,
|
|
// as the 1 in 'CSI 1 P' etc. must be omitted if there are no
|
|
// modifiers pressed:
|
|
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
|
|
b'P' => Event::Key(KeyCode::Fn(1).into()),
|
|
b'Q' => Event::Key(KeyCode::Fn(2).into()),
|
|
b'S' => Event::Key(KeyCode::Fn(4).into()),
|
|
b'?' => match last {
|
|
b'u' | b'c' | b'n' | b'y' => return Err(ParseError::Ignored),
|
|
_ => bail!(),
|
|
},
|
|
b'>' => match seq[seq.len() - 2..] {
|
|
[b' ', b'q'] => return Err(ParseError::Ignored),
|
|
_ => bail!(),
|
|
},
|
|
b'0'..=b'9' if !(64..=126).contains(&last) => return Err(ParseError::Incomplete),
|
|
b'0'..=b'9' => match last {
|
|
b'M' => return self.parse_csi_rxvt_mouse(),
|
|
b'~' => return self.parse_csi_special_key(),
|
|
b'u' => return self.parse_csi_u_key(),
|
|
b'R' => return Err(ParseError::Ignored),
|
|
_ if self.seq.contains(&b';') => return self.parse_csi_modifier_key(),
|
|
_ => return self.parse_csi_modifier_legacy_key(),
|
|
},
|
|
_ => bail!(),
|
|
})
|
|
}
|
|
|
|
pub(super) fn parse_csi_u_key(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
|
|
debug_assert!(seq.ends_with(b"u"));
|
|
|
|
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
|
|
let mut it = s.split(';');
|
|
|
|
// In `CSI u`, this is parsed as:
|
|
//
|
|
// CSI codepoint ; modifiers u
|
|
// codepoint: ASCII Dec value
|
|
//
|
|
// The Kitty Keyboard Protocol extends this with optional components that can be
|
|
// enabled progressively. The full sequence is parsed as:
|
|
//
|
|
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ;
|
|
// text-as-codepoints u
|
|
let mut codepoints = it.next().ok_or(ParseError::Invalid)?.split(':');
|
|
|
|
let (mut code, state_from_keycode) = KeyCode::from_codepoint(parse_next(&mut codepoints)?)?;
|
|
let (mut modifiers, kind, state_from_modifiers) = parse_mks(&mut it).unwrap_or_default();
|
|
let text = parse_text(it.next().unwrap_or_default())?;
|
|
|
|
if let KeyCode::Modifier(c) = code
|
|
&& kind != KeyEventKind::Release
|
|
&& let Some(m) = c.to_modifier()
|
|
{
|
|
modifiers |= m;
|
|
}
|
|
|
|
// When the "report alternate keys" flag is enabled in the Kitty Keyboard
|
|
// Protocol and the terminal sends a keyboard event containing shift, the
|
|
// sequence will contain an additional codepoint separated by a ':' character
|
|
// which contains the shifted character according to the keyboard layout.
|
|
if modifiers.contains(Modifiers::SHIFT)
|
|
&& let Ok(Some(shifted)) = parse_next(&mut codepoints).map(char::from_u32)
|
|
&& code != KeyCode::Char(shifted)
|
|
{
|
|
code = KeyCode::Char(shifted);
|
|
modifiers.set(Modifiers::SHIFT, code.implies_shift());
|
|
}
|
|
|
|
Ok(Event::Key(KeyEvent {
|
|
code,
|
|
modifiers,
|
|
kind,
|
|
state: state_from_keycode | state_from_modifiers,
|
|
text,
|
|
}))
|
|
}
|
|
|
|
/// Parses `CSI [1;] modifier[:kind] final` — sequences that carry a
|
|
/// semicolon, e.g. `\x1B[;2A` (Shift+Up, leading 1 omitted) or `\x1B[1;2A`
|
|
/// (Shift+Up).
|
|
pub(super) fn parse_csi_modifier_key(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
|
|
|
|
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
|
|
let mut it = s.split(';');
|
|
it.next(); // skip leading "1" or empty string
|
|
|
|
let (modifiers, kind, _) = parse_mks(&mut it).unwrap_or_default();
|
|
let code = KeyCode::from_xterm_modifier(seq[seq.len() - 1])?;
|
|
Ok(Event::Key(KeyEvent { code, modifiers, kind, ..Default::default() }))
|
|
}
|
|
|
|
/// Parses legacy `CSI modifier final` - no semicolon, modifier digit
|
|
/// immediately before the final byte, e.g. `\x1B[2A` = Shift+Up.
|
|
pub(super) fn parse_csi_modifier_legacy_key(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
|
|
|
|
let modifier = seq[seq.len() - 2];
|
|
if !modifier.is_ascii_digit() {
|
|
bail!();
|
|
}
|
|
|
|
Ok(Event::Key(KeyEvent {
|
|
code: KeyCode::from_xterm_modifier(seq[seq.len() - 1])?,
|
|
modifiers: Modifiers::from_vt_mask(modifier - b'0'),
|
|
..Default::default()
|
|
}))
|
|
}
|
|
|
|
pub(super) fn parse_csi_special_key(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
|
|
debug_assert!(seq.ends_with(b"~"));
|
|
|
|
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
|
|
let mut it = s.split(';');
|
|
|
|
// This CSI sequence can be a list of semicolon-separated numbers.
|
|
let first: u8 = parse_next(&mut it)?;
|
|
|
|
let (modifiers, kind, state) = parse_mks(&mut it).unwrap_or_default();
|
|
|
|
let code = match first {
|
|
1 | 7 => KeyCode::Home,
|
|
2 => KeyCode::Insert,
|
|
3 => KeyCode::Delete,
|
|
4 | 8 => KeyCode::End,
|
|
5 => KeyCode::PageUp,
|
|
6 => KeyCode::PageDown,
|
|
v @ 11..=15 => KeyCode::Fn(v - 10),
|
|
v @ 17..=21 => KeyCode::Fn(v - 11),
|
|
v @ 23..=26 => KeyCode::Fn(v - 12),
|
|
v @ 28..=29 => KeyCode::Fn(v - 15),
|
|
v @ 31..=34 => KeyCode::Fn(v - 17),
|
|
_ => bail!(),
|
|
};
|
|
|
|
let event = Event::Key(KeyEvent { code, modifiers, kind, state, ..Default::default() });
|
|
|
|
Ok(event)
|
|
}
|
|
|
|
// Parse rxvt mouse: CSI Cb ; Cx ; Cy ; M
|
|
pub(super) fn parse_csi_rxvt_mouse(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
|
|
debug_assert!(seq.ends_with(b"M"));
|
|
|
|
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
|
|
let mut it = s.split(';');
|
|
|
|
let cb = parse_next::<u8>(&mut it)?.checked_sub(32).ok_or(ParseError::Invalid)?;
|
|
let (kind, modifiers) = MouseEventKind::from_cb(cb)?;
|
|
|
|
let column = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
|
|
let row = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
|
|
|
|
Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers }))
|
|
}
|
|
|
|
// Parse normal mouse: CSI M CB Cx Cy (6 characters only).
|
|
pub(super) fn parse_csi_normal_mouse(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B[M")); // CSI M
|
|
|
|
if seq.len() < 6 {
|
|
return Err(ParseError::Incomplete);
|
|
}
|
|
|
|
let cb = seq[3].checked_sub(32).ok_or(ParseError::Invalid)?;
|
|
let (kind, modifiers) = MouseEventKind::from_cb(cb)?;
|
|
|
|
// See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
|
// Mouse positions are encoded as (value + 32), but the upper left
|
|
// character position on the terminal is denoted as 1,1.
|
|
// So, we need to subtract 32 + 1 (33) to keep it synced with the cursor.
|
|
let column = u16::from(seq[4].checked_sub(33).ok_or(ParseError::Invalid)?);
|
|
let row = u16::from(seq[5].checked_sub(33).ok_or(ParseError::Invalid)?);
|
|
|
|
Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers }))
|
|
}
|
|
|
|
// Parse SGR mouse: CSI < Cb ; Cx ; Cy (;) (M or m)
|
|
pub(super) fn parse_csi_sgr_mouse(&self) -> Result<Event> {
|
|
let seq = &self.seq;
|
|
debug_assert!(seq.starts_with(b"\x1B[<")); // CSI <
|
|
|
|
if !seq.ends_with(b"m") && !seq.ends_with(b"M") {
|
|
return Err(ParseError::Ignored);
|
|
}
|
|
|
|
let s = str::from_utf8(&seq[3..seq.len() - 1])?;
|
|
let mut it = s.split(';');
|
|
|
|
let cb = parse_next(&mut it)?;
|
|
let (mut kind, modifiers) = MouseEventKind::from_cb(cb)?;
|
|
|
|
// See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
|
// The upper left character position on the terminal is denoted as 1,1.
|
|
// Subtract 1 to keep it synced with cursor
|
|
let column = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
|
|
let row = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
|
|
|
|
// When button 3 in Cb is used to represent mouse release, you can't tell which
|
|
// button was released. SGR mode solves this by having the sequence end with a
|
|
// lowercase m if it's a button release and an uppercase M if it's a button
|
|
// press.
|
|
//
|
|
// We've already checked that the last character is a lowercase or uppercase M
|
|
// at the start of this function, so we just need one if.
|
|
if seq.ends_with(b"m")
|
|
&& let MouseEventKind::Down(button) = kind
|
|
{
|
|
kind = MouseEventKind::Up(button);
|
|
}
|
|
|
|
Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers }))
|
|
}
|
|
}
|
|
|
|
fn parse_next<'a, T>(iter: &mut impl Iterator<Item = &'a str>) -> Result<T>
|
|
where
|
|
T: FromStr,
|
|
{
|
|
iter.next().ok_or(ParseError::Invalid)?.parse::<T>().map_err(|_| ParseError::Invalid)
|
|
}
|
|
|
|
fn parse_mks<'a, I>(mut it: I) -> Option<(Modifiers, KeyEventKind, KeyEventState)>
|
|
where
|
|
I: Iterator<Item = &'a str>,
|
|
{
|
|
let mut it = it.next()?.split(':');
|
|
let mask: u8 = it.next()?.parse().ok()?;
|
|
let code: u8 = it.next().and_then(|s| s.parse().ok()).unwrap_or(1);
|
|
|
|
Some((
|
|
Modifiers::from_vt_mask(mask),
|
|
KeyEventKind::from_vt_code(code),
|
|
KeyEventState::from_vt_mask(mask),
|
|
))
|
|
}
|
|
|
|
fn parse_text(s: &str) -> Result<CompactString> {
|
|
if s.is_empty() {
|
|
return Ok(CompactString::default());
|
|
}
|
|
|
|
s.split(':')
|
|
.map(|codepoint| char::from_u32(codepoint.parse()?).ok_or(ParseError::Invalid))
|
|
.collect()
|
|
}
|