Files
herdr/src/pane/input.rs
T

490 lines
20 KiB
Rust

/// Build the key event the Ghostty app would build from an OS key event, so
/// libghostty can encode it for the pane's current keyboard modes: physical
/// key, modifiers, the layout text, the unshifted codepoint, and Shift marked
/// consumed when it produced the text.
pub(super) fn ghostty_key_event_from_terminal_key(
key: &crate::input::TerminalKey,
) -> Option<crate::ghostty::KeyEvent> {
use crossterm::event::KeyCode;
let mut event = crate::ghostty::KeyEvent::new().ok()?;
event.set_action(match key.kind {
crossterm::event::KeyEventKind::Press => {
crate::ghostty::ffi::GhosttyKeyAction_GHOSTTY_KEY_ACTION_PRESS
}
crossterm::event::KeyEventKind::Release => {
crate::ghostty::ffi::GhosttyKeyAction_GHOSTTY_KEY_ACTION_RELEASE
}
crossterm::event::KeyEventKind::Repeat => {
crate::ghostty::ffi::GhosttyKeyAction_GHOSTTY_KEY_ACTION_REPEAT
}
});
let mut mods = ghostty_mods_from_key_modifiers(key.modifiers);
match key.code {
KeyCode::Char(c) => {
// Legacy input reports the produced character ("%", "D"); Kitty
// reports the unshifted key. Both map to the same physical key.
let base = unshifted_char(c);
if base != c {
mods |= crate::ghostty::MOD_SHIFT;
}
event.set_key(
ghostty_key_from_char(base)
.unwrap_or(crate::ghostty::ffi::GhosttyKey_GHOSTTY_KEY_UNIDENTIFIED),
);
event.set_unshifted_codepoint(base as u32);
let shifted = mods & crate::ghostty::MOD_SHIFT != 0;
if let Some(text) = key_text(key, base, shifted) {
if shifted && !text.starts_with(base) {
event.set_consumed_mods(crate::ghostty::MOD_SHIFT);
}
event.set_utf8(&text);
}
}
KeyCode::BackTab => {
// Ghostty represents backtab as Tab with Shift rather than a distinct key.
mods |= crate::ghostty::MOD_SHIFT;
event.set_key(crate::ghostty::ffi::GhosttyKey_GHOSTTY_KEY_TAB);
}
code => event.set_key(ghostty_key_from_key_code(code)?),
}
event.set_mods(mods);
event.set_composing(key.is_windows_dead_key());
Some(event)
}
/// Exception table for panes that negotiated no keyboard protocol. Ghostty sends
/// these chords as escape sequences that plain shells do not understand; keep
/// the classic key instead. libghostty still encodes the result (so Alt keeps
/// its ESC prefix).
pub(super) fn legacy_shell_key(key: crate::input::TerminalKey) -> crate::input::TerminalKey {
use crossterm::event::{KeyCode, KeyModifiers};
let alt = key.modifiers & KeyModifiers::ALT;
let chord = key.modifiers - KeyModifiers::ALT;
let classic = match (key.code, chord) {
(KeyCode::Enter, chord) if !chord.is_empty() => KeyCode::Enter,
(KeyCode::Char('m'), KeyModifiers::CONTROL) => KeyCode::Enter,
(KeyCode::Char('i'), KeyModifiers::CONTROL) | (KeyCode::Tab, KeyModifiers::CONTROL) => {
KeyCode::Tab
}
(KeyCode::Char('['), KeyModifiers::CONTROL) => KeyCode::Esc,
_ => return key,
};
crate::input::TerminalKey::new(classic, alt)
.with_kind(key.kind)
.with_repeat_count(key.repeat_count)
}
/// Second exception for panes that negotiated nothing: legacy encoding has no
/// Super, so Ghostty would deliver Cmd+C as a bare "c" (#3710). Encode Super
/// chords as basic Kitty (disambiguate) reports instead.
pub(super) fn legacy_super_chord(key: &crate::input::TerminalKey) -> bool {
key.modifiers
.contains(crossterm::event::KeyModifiers::SUPER)
}
/// Text the key produced on the user's layout, before Ctrl/Alt transformations.
fn key_text(key: &crate::input::TerminalKey, base: char, shifted: bool) -> Option<String> {
if let Some(text) = key
.generated_text
.as_ref()
.filter(|text| !text.is_empty() && !text.chars().any(char::is_control))
{
return Some(text.clone());
}
let produced = if shifted {
key.shifted_codepoint
.and_then(char::from_u32)
.or_else(|| us_shifted_char(base))
.unwrap_or(base)
} else {
base
};
(!produced.is_control()).then(|| produced.to_string())
}
fn unshifted_char(c: char) -> char {
if c.is_uppercase() {
let mut lower = c.to_lowercase();
if let (Some(lower), None) = (lower.next(), lower.next()) {
return lower;
}
return c;
}
ghostty_unshifted_ascii_pair(c).unwrap_or(c)
}
/// US-layout shifted character, used only when the host reported Shift without
/// the shifted alternate.
fn us_shifted_char(base: char) -> Option<char> {
if base.is_ascii_lowercase() {
return Some(base.to_ascii_uppercase());
}
Some(match base {
'1' => '!',
'2' => '@',
'3' => '#',
'4' => '$',
'5' => '%',
'6' => '^',
'7' => '&',
'8' => '*',
'9' => '(',
'0' => ')',
'-' => '_',
'=' => '+',
'[' => '{',
']' => '}',
'\\' => '|',
';' => ':',
'\'' => '"',
',' => '<',
'.' => '>',
'/' => '?',
'`' => '~',
_ => return None,
})
}
pub(super) fn ghostty_mods_from_key_modifiers(modifiers: crossterm::event::KeyModifiers) -> u16 {
let mut ghostty_mods = 0u16;
if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
ghostty_mods |= crate::ghostty::MOD_SHIFT;
}
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
ghostty_mods |= crate::ghostty::MOD_CTRL;
}
if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
ghostty_mods |= crate::ghostty::MOD_ALT;
}
if modifiers.contains(crossterm::event::KeyModifiers::SUPER) {
ghostty_mods |= crate::ghostty::MOD_SUPER;
}
ghostty_mods
}
pub(super) fn ghostty_mouse_encoder_for_terminal(
terminal: &crate::ghostty::Terminal,
position: crate::input::mouse::Position,
) -> Option<crate::ghostty::MouseEncoder> {
let mut encoder = crate::ghostty::MouseEncoder::new().ok()?;
encoder.set_from_terminal(terminal);
let cols = terminal.cols().ok()? as u32;
let rows = terminal.rows().ok()? as u32;
let sgr_pixels = terminal
.mode_get(crate::ghostty::MODE_MOUSE_SGR_PIXELS)
.ok()?;
match position {
crate::input::mouse::Position::Cell { .. } => {
if sgr_pixels {
encoder.set_format(crate::ghostty::MOUSE_FORMAT_SGR);
}
encoder.set_size(cols, rows, 1, 1);
}
crate::input::mouse::Position::Pixels { .. } => {
if sgr_pixels {
encoder.set_format(crate::ghostty::MOUSE_FORMAT_SGR_PIXELS);
}
let width_px = terminal.width_px().ok()?;
let height_px = terminal.height_px().ok()?;
if width_px == 0 || height_px == 0 || cols == 0 || rows == 0 {
return None;
}
encoder.set_size(width_px, height_px, width_px / cols, height_px / rows);
}
}
Some(encoder)
}
pub(super) fn ghostty_mouse_position_for_terminal(
position: crate::input::mouse::Position,
) -> Option<(f32, f32)> {
match position {
crate::input::mouse::Position::Pixels { x, y } => Some((x as f32, y as f32)),
crate::input::mouse::Position::Cell { column, row } => Some((column as f32, row as f32)),
}
}
pub(super) fn ghostty_mouse_event_from_button_kind(
kind: crossterm::event::MouseEventKind,
column: u16,
row: u16,
modifiers: crossterm::event::KeyModifiers,
) -> Option<crate::ghostty::MouseEvent> {
let mut event = crate::ghostty::MouseEvent::new().ok()?;
let (action, button) = match kind {
crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left) => (
crate::ghostty::MOUSE_ACTION_PRESS,
Some(crate::ghostty::MOUSE_BUTTON_LEFT),
),
crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Middle) => (
crate::ghostty::MOUSE_ACTION_PRESS,
Some(crate::ghostty::MOUSE_BUTTON_MIDDLE),
),
crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Right) => (
crate::ghostty::MOUSE_ACTION_PRESS,
Some(crate::ghostty::MOUSE_BUTTON_RIGHT),
),
crossterm::event::MouseEventKind::Up(crossterm::event::MouseButton::Left) => (
crate::ghostty::MOUSE_ACTION_RELEASE,
Some(crate::ghostty::MOUSE_BUTTON_LEFT),
),
crossterm::event::MouseEventKind::Up(crossterm::event::MouseButton::Middle) => (
crate::ghostty::MOUSE_ACTION_RELEASE,
Some(crate::ghostty::MOUSE_BUTTON_MIDDLE),
),
crossterm::event::MouseEventKind::Up(crossterm::event::MouseButton::Right) => (
crate::ghostty::MOUSE_ACTION_RELEASE,
Some(crate::ghostty::MOUSE_BUTTON_RIGHT),
),
crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Left) => (
crate::ghostty::MOUSE_ACTION_MOTION,
Some(crate::ghostty::MOUSE_BUTTON_LEFT),
),
crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Middle) => (
crate::ghostty::MOUSE_ACTION_MOTION,
Some(crate::ghostty::MOUSE_BUTTON_MIDDLE),
),
crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Right) => (
crate::ghostty::MOUSE_ACTION_MOTION,
Some(crate::ghostty::MOUSE_BUTTON_RIGHT),
),
_ => return None,
};
event.set_action(action);
if let Some(button) = button {
event.set_button(button);
} else {
event.clear_button();
}
event.set_mods(ghostty_mods_from_key_modifiers(modifiers));
event.set_position(column as f32, row as f32);
Some(event)
}
pub(super) fn ghostty_mouse_event_from_motion_kind(
kind: crossterm::event::MouseEventKind,
column: u16,
row: u16,
modifiers: crossterm::event::KeyModifiers,
) -> Option<crate::ghostty::MouseEvent> {
if kind != crossterm::event::MouseEventKind::Moved {
return None;
}
let mut event = crate::ghostty::MouseEvent::new().ok()?;
event.set_action(crate::ghostty::MOUSE_ACTION_MOTION);
event.clear_button();
event.set_mods(ghostty_mods_from_key_modifiers(modifiers));
event.set_position(column as f32, row as f32);
Some(event)
}
pub(super) fn ghostty_mouse_event_from_wheel_kind(
kind: crossterm::event::MouseEventKind,
column: u16,
row: u16,
modifiers: crossterm::event::KeyModifiers,
) -> Option<crate::ghostty::MouseEvent> {
let mut event = crate::ghostty::MouseEvent::new().ok()?;
event.set_action(crate::ghostty::MOUSE_ACTION_PRESS);
let button = match kind {
crossterm::event::MouseEventKind::ScrollUp => crate::ghostty::MOUSE_BUTTON_WHEEL_UP,
crossterm::event::MouseEventKind::ScrollDown => crate::ghostty::MOUSE_BUTTON_WHEEL_DOWN,
crossterm::event::MouseEventKind::ScrollLeft => crate::ghostty::MOUSE_BUTTON_WHEEL_LEFT,
crossterm::event::MouseEventKind::ScrollRight => crate::ghostty::MOUSE_BUTTON_WHEEL_RIGHT,
_ => return None,
};
event.set_button(button);
event.set_mods(ghostty_mods_from_key_modifiers(modifiers));
event.set_position(column as f32, row as f32);
Some(event)
}
/// Physical key for a non-character key code. Keys libghostty has no name for
/// (some media keys, Hyper/Meta, ISO level shifts) return None; the Ghostty app
/// cannot send them either.
fn ghostty_key_from_key_code(
code: crossterm::event::KeyCode,
) -> Option<crate::ghostty::ffi::GhosttyKey> {
use crate::ghostty::ffi;
use crossterm::event::{KeyCode, MediaKeyCode, ModifierKeyCode};
match code {
KeyCode::Backspace => Some(ffi::GhosttyKey_GHOSTTY_KEY_BACKSPACE),
KeyCode::Enter => Some(ffi::GhosttyKey_GHOSTTY_KEY_ENTER),
KeyCode::Left => Some(ffi::GhosttyKey_GHOSTTY_KEY_ARROW_LEFT),
KeyCode::Right => Some(ffi::GhosttyKey_GHOSTTY_KEY_ARROW_RIGHT),
KeyCode::Up => Some(ffi::GhosttyKey_GHOSTTY_KEY_ARROW_UP),
KeyCode::Down => Some(ffi::GhosttyKey_GHOSTTY_KEY_ARROW_DOWN),
KeyCode::Home => Some(ffi::GhosttyKey_GHOSTTY_KEY_HOME),
KeyCode::End => Some(ffi::GhosttyKey_GHOSTTY_KEY_END),
KeyCode::PageUp => Some(ffi::GhosttyKey_GHOSTTY_KEY_PAGE_UP),
KeyCode::PageDown => Some(ffi::GhosttyKey_GHOSTTY_KEY_PAGE_DOWN),
KeyCode::Tab | KeyCode::BackTab => Some(ffi::GhosttyKey_GHOSTTY_KEY_TAB),
KeyCode::Delete => Some(ffi::GhosttyKey_GHOSTTY_KEY_DELETE),
KeyCode::Insert => Some(ffi::GhosttyKey_GHOSTTY_KEY_INSERT),
KeyCode::Esc => Some(ffi::GhosttyKey_GHOSTTY_KEY_ESCAPE),
KeyCode::F(n) => Some(match n {
1 => ffi::GhosttyKey_GHOSTTY_KEY_F1,
2 => ffi::GhosttyKey_GHOSTTY_KEY_F2,
3 => ffi::GhosttyKey_GHOSTTY_KEY_F3,
4 => ffi::GhosttyKey_GHOSTTY_KEY_F4,
5 => ffi::GhosttyKey_GHOSTTY_KEY_F5,
6 => ffi::GhosttyKey_GHOSTTY_KEY_F6,
7 => ffi::GhosttyKey_GHOSTTY_KEY_F7,
8 => ffi::GhosttyKey_GHOSTTY_KEY_F8,
9 => ffi::GhosttyKey_GHOSTTY_KEY_F9,
10 => ffi::GhosttyKey_GHOSTTY_KEY_F10,
11 => ffi::GhosttyKey_GHOSTTY_KEY_F11,
12 => ffi::GhosttyKey_GHOSTTY_KEY_F12,
13 => ffi::GhosttyKey_GHOSTTY_KEY_F13,
14 => ffi::GhosttyKey_GHOSTTY_KEY_F14,
15 => ffi::GhosttyKey_GHOSTTY_KEY_F15,
16 => ffi::GhosttyKey_GHOSTTY_KEY_F16,
17 => ffi::GhosttyKey_GHOSTTY_KEY_F17,
18 => ffi::GhosttyKey_GHOSTTY_KEY_F18,
19 => ffi::GhosttyKey_GHOSTTY_KEY_F19,
20 => ffi::GhosttyKey_GHOSTTY_KEY_F20,
21 => ffi::GhosttyKey_GHOSTTY_KEY_F21,
22 => ffi::GhosttyKey_GHOSTTY_KEY_F22,
23 => ffi::GhosttyKey_GHOSTTY_KEY_F23,
24 => ffi::GhosttyKey_GHOSTTY_KEY_F24,
25 => ffi::GhosttyKey_GHOSTTY_KEY_F25,
_ => return None,
}),
KeyCode::CapsLock => Some(ffi::GhosttyKey_GHOSTTY_KEY_CAPS_LOCK),
KeyCode::ScrollLock => Some(ffi::GhosttyKey_GHOSTTY_KEY_SCROLL_LOCK),
KeyCode::NumLock => Some(ffi::GhosttyKey_GHOSTTY_KEY_NUM_LOCK),
KeyCode::PrintScreen => Some(ffi::GhosttyKey_GHOSTTY_KEY_PRINT_SCREEN),
KeyCode::Pause => Some(ffi::GhosttyKey_GHOSTTY_KEY_PAUSE),
KeyCode::Menu => Some(ffi::GhosttyKey_GHOSTTY_KEY_CONTEXT_MENU),
KeyCode::KeypadBegin => Some(ffi::GhosttyKey_GHOSTTY_KEY_NUMPAD_BEGIN),
KeyCode::Media(MediaKeyCode::PlayPause) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_MEDIA_PLAY_PAUSE)
}
KeyCode::Media(MediaKeyCode::Stop) => Some(ffi::GhosttyKey_GHOSTTY_KEY_MEDIA_STOP),
KeyCode::Media(MediaKeyCode::TrackNext) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_MEDIA_TRACK_NEXT)
}
KeyCode::Media(MediaKeyCode::TrackPrevious) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS)
}
KeyCode::Media(MediaKeyCode::LowerVolume) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_AUDIO_VOLUME_DOWN)
}
KeyCode::Media(MediaKeyCode::RaiseVolume) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_AUDIO_VOLUME_UP)
}
KeyCode::Media(MediaKeyCode::MuteVolume) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_AUDIO_VOLUME_MUTE)
}
KeyCode::Modifier(ModifierKeyCode::LeftShift) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_SHIFT_LEFT)
}
KeyCode::Modifier(ModifierKeyCode::RightShift) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_SHIFT_RIGHT)
}
KeyCode::Modifier(ModifierKeyCode::LeftControl) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_CONTROL_LEFT)
}
KeyCode::Modifier(ModifierKeyCode::RightControl) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_CONTROL_RIGHT)
}
KeyCode::Modifier(ModifierKeyCode::LeftAlt) => Some(ffi::GhosttyKey_GHOSTTY_KEY_ALT_LEFT),
KeyCode::Modifier(ModifierKeyCode::RightAlt) => Some(ffi::GhosttyKey_GHOSTTY_KEY_ALT_RIGHT),
KeyCode::Modifier(ModifierKeyCode::LeftSuper) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_META_LEFT)
}
KeyCode::Modifier(ModifierKeyCode::RightSuper) => {
Some(ffi::GhosttyKey_GHOSTTY_KEY_META_RIGHT)
}
_ => None,
}
}
fn ghostty_key_from_char(base: char) -> Option<crate::ghostty::ffi::GhosttyKey> {
use crate::ghostty::ffi;
match base {
'a' => Some(ffi::GhosttyKey_GHOSTTY_KEY_A),
'b' => Some(ffi::GhosttyKey_GHOSTTY_KEY_B),
'c' => Some(ffi::GhosttyKey_GHOSTTY_KEY_C),
'd' => Some(ffi::GhosttyKey_GHOSTTY_KEY_D),
'e' => Some(ffi::GhosttyKey_GHOSTTY_KEY_E),
'f' => Some(ffi::GhosttyKey_GHOSTTY_KEY_F),
'g' => Some(ffi::GhosttyKey_GHOSTTY_KEY_G),
'h' => Some(ffi::GhosttyKey_GHOSTTY_KEY_H),
'i' => Some(ffi::GhosttyKey_GHOSTTY_KEY_I),
'j' => Some(ffi::GhosttyKey_GHOSTTY_KEY_J),
'k' => Some(ffi::GhosttyKey_GHOSTTY_KEY_K),
'l' => Some(ffi::GhosttyKey_GHOSTTY_KEY_L),
'm' => Some(ffi::GhosttyKey_GHOSTTY_KEY_M),
'n' => Some(ffi::GhosttyKey_GHOSTTY_KEY_N),
'o' => Some(ffi::GhosttyKey_GHOSTTY_KEY_O),
'p' => Some(ffi::GhosttyKey_GHOSTTY_KEY_P),
'q' => Some(ffi::GhosttyKey_GHOSTTY_KEY_Q),
'r' => Some(ffi::GhosttyKey_GHOSTTY_KEY_R),
's' => Some(ffi::GhosttyKey_GHOSTTY_KEY_S),
't' => Some(ffi::GhosttyKey_GHOSTTY_KEY_T),
'u' => Some(ffi::GhosttyKey_GHOSTTY_KEY_U),
'v' => Some(ffi::GhosttyKey_GHOSTTY_KEY_V),
'w' => Some(ffi::GhosttyKey_GHOSTTY_KEY_W),
'x' => Some(ffi::GhosttyKey_GHOSTTY_KEY_X),
'y' => Some(ffi::GhosttyKey_GHOSTTY_KEY_Y),
'z' => Some(ffi::GhosttyKey_GHOSTTY_KEY_Z),
'0' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_0),
'1' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_1),
'2' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_2),
'3' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_3),
'4' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_4),
'5' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_5),
'6' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_6),
'7' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_7),
'8' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_8),
'9' => Some(ffi::GhosttyKey_GHOSTTY_KEY_DIGIT_9),
'`' => Some(ffi::GhosttyKey_GHOSTTY_KEY_BACKQUOTE),
'\\' => Some(ffi::GhosttyKey_GHOSTTY_KEY_BACKSLASH),
'[' => Some(ffi::GhosttyKey_GHOSTTY_KEY_BRACKET_LEFT),
']' => Some(ffi::GhosttyKey_GHOSTTY_KEY_BRACKET_RIGHT),
',' => Some(ffi::GhosttyKey_GHOSTTY_KEY_COMMA),
'=' => Some(ffi::GhosttyKey_GHOSTTY_KEY_EQUAL),
'-' => Some(ffi::GhosttyKey_GHOSTTY_KEY_MINUS),
'.' => Some(ffi::GhosttyKey_GHOSTTY_KEY_PERIOD),
'\'' => Some(ffi::GhosttyKey_GHOSTTY_KEY_QUOTE),
';' => Some(ffi::GhosttyKey_GHOSTTY_KEY_SEMICOLON),
'/' => Some(ffi::GhosttyKey_GHOSTTY_KEY_SLASH),
' ' => Some(ffi::GhosttyKey_GHOSTTY_KEY_SPACE),
_ => None,
}
}
fn ghostty_unshifted_ascii_pair(c: char) -> Option<char> {
Some(match c {
'!' => '1',
'@' => '2',
'#' => '3',
'$' => '4',
'%' => '5',
'^' => '6',
'&' => '7',
'*' => '8',
'(' => '9',
')' => '0',
'_' => '-',
'+' => '=',
'{' => '[',
'}' => ']',
'|' => '\\',
':' => ';',
'"' => '\'',
'<' => ',',
'>' => '.',
'?' => '/',
'~' => '`',
_ => return None,
})
}