T08: Tab and F6 pass Ely's focus keys; each kitty flag reshapes only what it names; Cmd is Super; repeats and Command key-ups come from AppKit; IME commits stay UTF-8, as in kitty and Ghostty; key.zig out; the line rule reads every text file
This commit is contained in:
14 files changed
+505
-1147
No files matched your search
+4
-1
@@ -56,7 +56,10 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
|
||||
ShellIntegration::init(cx)?;
|
||||
input::bind_keys(cx);
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::repeat_held_keys();
|
||||
{
|
||||
mac::repeat_held_keys();
|
||||
mac::deliver_command_key_ups()?;
|
||||
}
|
||||
let family = &SettingsStore::get(cx).font_family;
|
||||
anyhow::ensure!(
|
||||
cx.text_system().all_font_names().contains(family),
|
||||
|
||||
+53
-44
@@ -3,7 +3,7 @@ use std::ops::Range;
|
||||
use alacritty_terminal::{grid::Scroll, term::TermMode};
|
||||
use gpui::{
|
||||
App, Bounds, Context, EntityInputHandler, KeyBinding, KeyDownEvent, KeyUpEvent, Keystroke,
|
||||
Pixels, Point, UTF16Selection, Window, actions,
|
||||
NoAction, Pixels, Point, UTF16Selection, Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -11,17 +11,15 @@ use super::{
|
||||
view::TerminalView,
|
||||
};
|
||||
|
||||
actions!(terminal, [SendTab, SendBackTab]);
|
||||
|
||||
/// The key context a terminal's root sets.
|
||||
pub const CONTEXT: &str = "Terminal";
|
||||
|
||||
/// Tab and Shift-Tab reach the terminal before Ely moves focus with them.
|
||||
/// Ely's focus keys go to the terminal's listeners instead.
|
||||
pub fn bind_keys(cx: &mut App) {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", SendTab, Some(CONTEXT)),
|
||||
KeyBinding::new("shift-tab", SendBackTab, Some(CONTEXT)),
|
||||
]);
|
||||
cx.bind_keys(
|
||||
["tab", "shift-tab", "f6", "shift-f6"]
|
||||
.map(|key| KeyBinding::new(key, NoAction, Some(CONTEXT))),
|
||||
);
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
@@ -41,41 +39,34 @@ impl TerminalView {
|
||||
if event.prefer_character_input {
|
||||
return;
|
||||
}
|
||||
let action = if event.is_held {
|
||||
let facts = self.facts();
|
||||
let action = if event.is_held || facts.repeat {
|
||||
Action::Repeat
|
||||
} else {
|
||||
Action::Press
|
||||
};
|
||||
if self.send(&event.keystroke, action, cx) {
|
||||
if self.send(&event.keystroke, action, facts, cx) {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn key_up(&mut self, event: &KeyUpEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.send(&event.keystroke, Action::Release, cx) {
|
||||
let facts = self.facts();
|
||||
if self.send(&event.keystroke, Action::Release, facts, cx) {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tab(&mut self, shift: bool, cx: &mut Context<Self>) {
|
||||
let key = Key {
|
||||
name: "tab",
|
||||
text: if shift { "" } else { "\t" },
|
||||
shift,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
action: Action::Press,
|
||||
};
|
||||
self.encode(&key, cx);
|
||||
}
|
||||
|
||||
/// Sends what `keystroke` means to the program; false when it is text the input method types.
|
||||
fn send(&mut self, keystroke: &Keystroke, action: Action, cx: &mut Context<Self>) -> bool {
|
||||
fn send(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
action: Action,
|
||||
facts: Facts,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let modifiers = keystroke.modifiers;
|
||||
if modifiers.platform {
|
||||
return false;
|
||||
}
|
||||
let alt = modifiers.alt && self.alt_is_meta(cx);
|
||||
let alt = modifiers.alt && Self::alt_is_meta(facts, cx);
|
||||
let text = match &keystroke.key_char {
|
||||
Some(_) if alt && cfg!(target_os = "macos") => "",
|
||||
Some(text) => text.as_str(),
|
||||
@@ -87,12 +78,9 @@ impl TerminalView {
|
||||
shift: modifiers.shift,
|
||||
ctrl: modifiers.control,
|
||||
alt,
|
||||
platform: modifiers.platform,
|
||||
action,
|
||||
};
|
||||
self.encode(&key, cx)
|
||||
}
|
||||
|
||||
fn encode(&mut self, key: &Key, cx: &mut Context<Self>) -> bool {
|
||||
let mode = *self.pty.term.lock().mode();
|
||||
let kitty = Kitty {
|
||||
disambiguate: mode.contains(TermMode::DISAMBIGUATE_ESC_CODES),
|
||||
@@ -101,7 +89,7 @@ impl TerminalView {
|
||||
all: mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC),
|
||||
text: mode.contains(TermMode::REPORT_ASSOCIATED_TEXT),
|
||||
};
|
||||
let Some(bytes) = keys::encode(key, mode.contains(TermMode::APP_CURSOR), kitty) else {
|
||||
let Some(bytes) = keys::encode(&key, mode.contains(TermMode::APP_CURSOR), kitty) else {
|
||||
return false;
|
||||
};
|
||||
log::trace!("terminal {}: {key:?} sends {bytes:?}", self.pty.pid);
|
||||
@@ -109,31 +97,52 @@ impl TerminalView {
|
||||
true
|
||||
}
|
||||
|
||||
/// AppKit's facts of the key; logged and none when unreadable.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn facts(&self) -> Facts {
|
||||
super::mac::key_facts().unwrap_or_else(|error| {
|
||||
log::error!("terminal {}: {error:#}", self.pty.pid);
|
||||
Facts {
|
||||
left_option: false,
|
||||
right_option: false,
|
||||
repeat: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn facts(&self) -> Facts {
|
||||
Facts::default()
|
||||
}
|
||||
|
||||
/// Whether a held Alt is a modifier; on macOS only the Option keys the settings name.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn alt_is_meta(&self, cx: &App) -> bool {
|
||||
fn alt_is_meta(facts: Facts, cx: &App) -> bool {
|
||||
use crate::settings::{OptionAsAlt, SettingsStore};
|
||||
let sides = || match super::mac::option_sides() {
|
||||
Ok(sides) => Some(sides),
|
||||
Err(error) => {
|
||||
log::error!("terminal {}: {error:#}; Option composes", self.pty.pid);
|
||||
None
|
||||
}
|
||||
};
|
||||
match SettingsStore::get(cx).option_as_alt {
|
||||
OptionAsAlt::Off => false,
|
||||
OptionAsAlt::Both => true,
|
||||
OptionAsAlt::Left => sides().is_some_and(|(left, _)| left),
|
||||
OptionAsAlt::Right => sides().is_some_and(|(_, right)| right),
|
||||
OptionAsAlt::Left => facts.left_option,
|
||||
OptionAsAlt::Right => facts.right_option,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn alt_is_meta(&self, _: &App) -> bool {
|
||||
fn alt_is_meta(_: Facts, _: &App) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
type Facts = super::mac::KeyFacts;
|
||||
|
||||
/// Other platforms tell gpui all it needs.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Facts {
|
||||
repeat: bool,
|
||||
}
|
||||
|
||||
impl EntityInputHandler for TerminalView {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
|
||||
+115
-203
@@ -1,3 +1,7 @@
|
||||
mod legacy;
|
||||
|
||||
use legacy::{FUNCTION_TILDES, legacy, printable_key};
|
||||
|
||||
/// Whether a key went down, repeated, or came up.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
@@ -17,6 +21,8 @@ pub struct Key<'a> {
|
||||
pub ctrl: bool,
|
||||
/// Alt as a modifier, not as the key that composes characters.
|
||||
pub alt: bool,
|
||||
/// Cmd, the Windows key or Super: kitty's super.
|
||||
pub platform: bool,
|
||||
pub action: Action,
|
||||
}
|
||||
|
||||
@@ -33,16 +39,24 @@ pub struct Kitty {
|
||||
/// The bytes a key sends; none when it sends nothing or its text is the input method's to type.
|
||||
pub fn encode(key: &Key, app_cursor: bool, kitty: Kitty) -> Option<Vec<u8>> {
|
||||
let on = kitty.disambiguate || kitty.events || kitty.alternates || kitty.all || kitty.text;
|
||||
if on {
|
||||
encode_kitty(key, kitty)
|
||||
} else {
|
||||
legacy(key, app_cursor)
|
||||
if !on {
|
||||
return legacy(key, app_cursor);
|
||||
}
|
||||
if key.action == Action::Release && !kitty.events {
|
||||
return None;
|
||||
}
|
||||
match functional(key.name) {
|
||||
Some((number, end)) => named(key, number, end, app_cursor, kitty),
|
||||
None => typed(key, printable(key.name)?, kitty),
|
||||
}
|
||||
}
|
||||
|
||||
/// xterm's modifier code: one plus shift 1, alt 2, ctrl 4.
|
||||
/// xterm's modifier code: one plus shift 1, alt 2, ctrl 4, super 8.
|
||||
fn code(key: &Key) -> u32 {
|
||||
1 + u32::from(key.shift) + 2 * u32::from(key.alt) + 4 * u32::from(key.ctrl)
|
||||
1 + u32::from(key.shift)
|
||||
+ 2 * u32::from(key.alt)
|
||||
+ 4 * u32::from(key.ctrl)
|
||||
+ 8 * u32::from(key.platform)
|
||||
}
|
||||
|
||||
/// The character a key name stands for, `space` included.
|
||||
@@ -54,152 +68,20 @@ fn printable(name: &str) -> Option<char> {
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy(key: &Key, app_cursor: bool) -> Option<Vec<u8>> {
|
||||
if key.action == Action::Release {
|
||||
return None;
|
||||
}
|
||||
if let Some(sequence) = function(key, app_cursor) {
|
||||
return Some(sequence);
|
||||
}
|
||||
let ch = printable(key.name)?;
|
||||
if key.ctrl {
|
||||
if let Some(byte) = control(ch, key.shift) {
|
||||
return Some(if key.alt {
|
||||
vec![0x1B, byte]
|
||||
} else {
|
||||
vec![byte]
|
||||
});
|
||||
}
|
||||
return Some(format!("\x1b[{};{}u", ch as u32, code(key)).into_bytes());
|
||||
}
|
||||
if key.alt {
|
||||
let mut bytes = vec![0x1B];
|
||||
if key.text.is_empty() {
|
||||
let typed = if key.shift {
|
||||
ch.to_ascii_uppercase()
|
||||
} else {
|
||||
ch
|
||||
};
|
||||
bytes.extend(typed.to_string().as_bytes());
|
||||
} else {
|
||||
bytes.extend(key.text.as_bytes());
|
||||
}
|
||||
return Some(bytes);
|
||||
}
|
||||
None
|
||||
/// Whether the key types its text: some, none of it control, and no modifier that stops text.
|
||||
fn types(key: &Key) -> bool {
|
||||
let control = key.text.chars().any(char::is_control);
|
||||
!key.text.is_empty() && !control && !key.ctrl && !key.alt && !key.platform
|
||||
}
|
||||
|
||||
/// xterm's bytes for a function, cursor or editing key.
|
||||
fn function(key: &Key, app_cursor: bool) -> Option<Vec<u8>> {
|
||||
let mods = code(key);
|
||||
let modified = mods > 1;
|
||||
let cursor = |end: char| match (modified, app_cursor) {
|
||||
(true, _) => format!("\x1b[1;{mods}{end}"),
|
||||
(false, true) => format!("\x1bO{end}"),
|
||||
(false, false) => format!("\x1b[{end}"),
|
||||
};
|
||||
let tilde = |n: u32| {
|
||||
if modified {
|
||||
format!("\x1b[{n};{mods}~")
|
||||
} else {
|
||||
format!("\x1b[{n}~")
|
||||
}
|
||||
};
|
||||
let ss3 = |end: char| {
|
||||
if modified {
|
||||
format!("\x1b[1;{mods}{end}")
|
||||
} else {
|
||||
format!("\x1bO{end}")
|
||||
}
|
||||
};
|
||||
let only = (key.shift, key.alt, key.ctrl);
|
||||
let sequence = match key.name {
|
||||
"up" => cursor('A'),
|
||||
"down" => cursor('B'),
|
||||
"right" => cursor('C'),
|
||||
"left" => cursor('D'),
|
||||
"home" => cursor('H'),
|
||||
"end" => cursor('F'),
|
||||
"insert" => tilde(2),
|
||||
"delete" => tilde(3),
|
||||
"pageup" => tilde(5),
|
||||
"pagedown" => tilde(6),
|
||||
"f1" => ss3('P'),
|
||||
"f2" => ss3('Q'),
|
||||
"f3" if modified => tilde(13),
|
||||
"f3" => ss3('R'),
|
||||
"f4" => ss3('S'),
|
||||
"enter" => match only {
|
||||
(false, false, false) => "\r".into(),
|
||||
(false, true, false) => "\x1b\r".into(),
|
||||
_ => format!("\x1b[27;{mods};13~"),
|
||||
},
|
||||
"tab" => match only {
|
||||
(false, false, false) => "\t".into(),
|
||||
(true, false, false) => "\x1b[Z".into(),
|
||||
(false, true, false) => "\x1b\t".into(),
|
||||
_ => format!("\x1b[27;{mods};9~"),
|
||||
},
|
||||
"escape" => match only {
|
||||
(false, false, false) => "\x1b".into(),
|
||||
(false, true, false) => "\x1b\x1b".into(),
|
||||
_ => format!("\x1b[27;{mods};27~"),
|
||||
},
|
||||
"backspace" => {
|
||||
let byte = if key.ctrl { 0x08 } else { 0x7F };
|
||||
return Some(if key.alt {
|
||||
vec![0x1B, byte]
|
||||
} else {
|
||||
vec![byte]
|
||||
});
|
||||
}
|
||||
name => tilde(FUNCTION_TILDES.iter().find(|(f, _)| *f == name)?.1),
|
||||
};
|
||||
Some(sequence.into_bytes())
|
||||
}
|
||||
|
||||
/// F5 to F25 and the numbers xterm sends them with.
|
||||
const FUNCTION_TILDES: [(&str, u32); 21] = [
|
||||
("f5", 15),
|
||||
("f6", 17),
|
||||
("f7", 18),
|
||||
("f8", 19),
|
||||
("f9", 20),
|
||||
("f10", 21),
|
||||
("f11", 23),
|
||||
("f12", 24),
|
||||
("f13", 25),
|
||||
("f14", 26),
|
||||
("f15", 28),
|
||||
("f16", 29),
|
||||
("f17", 31),
|
||||
("f18", 32),
|
||||
("f19", 33),
|
||||
("f20", 34),
|
||||
("f21", 42),
|
||||
("f22", 43),
|
||||
("f23", 44),
|
||||
("f24", 45),
|
||||
("f25", 46),
|
||||
];
|
||||
|
||||
/// The C0 byte Ctrl turns a key into, as kitty and Ghostty do; `[`, `i` and `m` go as CSI u.
|
||||
fn control(ch: char, shift: bool) -> Option<u8> {
|
||||
if shift && ch != '@' {
|
||||
return None;
|
||||
}
|
||||
Some(match ch {
|
||||
' ' | '2' | '@' => 0,
|
||||
'a'..='h' | 'j' | 'k' | 'l' | 'n'..='z' => ch as u8 - b'a' + 1,
|
||||
'3' => 27,
|
||||
'4' | '\\' => 28,
|
||||
'5' | ']' => 29,
|
||||
'6' | '^' | '~' => 30,
|
||||
'7' | '/' | '_' => 31,
|
||||
'8' | '?' => 127,
|
||||
'0' | '1' | '9' => ch as u8,
|
||||
_ => return None,
|
||||
})
|
||||
/// The text's code points joined by colons, control codes left out.
|
||||
fn code_points(text: &str) -> String {
|
||||
let points: Vec<String> = text
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_control())
|
||||
.map(|ch| (ch as u32).to_string())
|
||||
.collect();
|
||||
points.join(":")
|
||||
}
|
||||
|
||||
/// kitty's functional keys: the number and the final byte.
|
||||
@@ -230,68 +112,98 @@ fn functional(name: &str) -> Option<(u32, char)> {
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_kitty(key: &Key, kitty: Kitty) -> Option<Vec<u8>> {
|
||||
let control_text = key.text.chars().any(char::is_control);
|
||||
if key.action == Action::Release {
|
||||
let legacy = matches!(key.name, "enter" | "tab" | "backspace");
|
||||
if !kitty.events || (legacy && !kitty.all) {
|
||||
/// kitty's event field: 2 for a repeat, 3 for a release, none for a press or without the flag.
|
||||
fn event(key: &Key, kitty: Kitty) -> Option<u32> {
|
||||
match key.action {
|
||||
Action::Press => None,
|
||||
_ if !kitty.events => None,
|
||||
Action::Repeat => Some(2),
|
||||
Action::Release => Some(3),
|
||||
}
|
||||
}
|
||||
|
||||
/// A functional key: legacy bytes until a flag that reshapes it is on.
|
||||
fn named(key: &Key, number: u32, end: char, app_cursor: bool, kitty: Kitty) -> Option<Vec<u8>> {
|
||||
if !kitty.events && !kitty.disambiguate && !kitty.all {
|
||||
return legacy(key, app_cursor);
|
||||
}
|
||||
let lone = matches!(key.name, "enter" | "tab" | "backspace");
|
||||
if key.action == Action::Release && lone && !kitty.all {
|
||||
return None;
|
||||
}
|
||||
let plain = code(key) == 1 && key.action != Action::Release && !kitty.all;
|
||||
match key.name {
|
||||
"enter" if plain => return Some(b"\r".to_vec()),
|
||||
"tab" if plain => return Some(b"\t".to_vec()),
|
||||
"backspace" if plain => return Some(vec![0x7F]),
|
||||
"escape" if plain && !kitty.disambiguate => return Some(b"\x1b".to_vec()),
|
||||
_ => {}
|
||||
}
|
||||
let mods = code(key);
|
||||
let event = event(key, kitty);
|
||||
let fields = match (mods, event) {
|
||||
(_, Some(event)) => format!("{mods}:{event}"),
|
||||
(1, None) => String::new(),
|
||||
(_, None) => mods.to_string(),
|
||||
};
|
||||
let sequence = match (end, fields.is_empty()) {
|
||||
('u' | '~', true) => format!("\x1b[{number}{end}"),
|
||||
('u' | '~', false) => format!("\x1b[{number};{fields}{end}"),
|
||||
(_, true) => format!("\x1b[{end}"),
|
||||
(_, false) => format!("\x1b[1;{fields}{end}"),
|
||||
};
|
||||
Some(sequence.into_bytes())
|
||||
}
|
||||
|
||||
/// A printable key: text, then legacy bytes, then CSI u once a flag has more to say.
|
||||
fn typed(key: &Key, ch: char, kitty: Kitty) -> Option<Vec<u8>> {
|
||||
let typing = types(key);
|
||||
if typing && !kitty.all && key.action != Action::Release {
|
||||
return None;
|
||||
}
|
||||
let shifted = key
|
||||
.text
|
||||
.chars()
|
||||
.next()
|
||||
.filter(|&typed| typing && typed != ch && !typed.is_control())
|
||||
.or_else(|| (key.shift && ch.is_ascii_lowercase()).then(|| ch.to_ascii_uppercase()))
|
||||
.filter(|_| key.shift && kitty.alternates);
|
||||
let event = event(key, kitty);
|
||||
let text = typing && kitty.text && key.action != Action::Release;
|
||||
let mods = code(key);
|
||||
if shifted.is_none() && event.is_none() && !text {
|
||||
if mods == 1 && !kitty.all {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let shift_typed = key.shift && !key.text.is_empty() && !control_text;
|
||||
let bound = key.ctrl || key.alt || (key.shift && !shift_typed);
|
||||
if !kitty.all && !bound && key.action != Action::Release {
|
||||
match key.name {
|
||||
"enter" => return Some(b"\r".to_vec()),
|
||||
"tab" => return Some(b"\t".to_vec()),
|
||||
"backspace" => return Some(vec![0x7F]),
|
||||
_ if !key.text.is_empty() && !control_text => return None,
|
||||
_ => {}
|
||||
if mods > 1
|
||||
&& !kitty.disambiguate
|
||||
&& !kitty.all
|
||||
&& let Some(bytes) = printable_key(key, ch)
|
||||
{
|
||||
return Some(bytes);
|
||||
}
|
||||
}
|
||||
let (number, end) = functional(key.name)
|
||||
.or_else(|| printable(key.name).map(|ch| (ch.to_ascii_lowercase() as u32, 'u')))?;
|
||||
let mods = code(key);
|
||||
let event = match key.action {
|
||||
Action::Press => 1,
|
||||
Action::Repeat => 2,
|
||||
Action::Release => 3,
|
||||
};
|
||||
let event = if kitty.events { event } else { 1 };
|
||||
let mut out = String::from("\x1b[");
|
||||
if end != 'u' && end != '~' {
|
||||
if event > 1 {
|
||||
out.push_str(&format!("1;{mods}:{event}"));
|
||||
} else if mods > 1 {
|
||||
out.push_str(&format!("1;{mods}"));
|
||||
}
|
||||
out.push(end);
|
||||
return Some(out.into_bytes());
|
||||
}
|
||||
out.push_str(&number.to_string());
|
||||
let shifted = key.text.chars().next().filter(|&typed| {
|
||||
kitty.alternates && key.shift && typed as u32 != number && !typed.is_control()
|
||||
});
|
||||
let mut out = format!("\x1b[{}", ch.to_ascii_lowercase() as u32);
|
||||
if let Some(shifted) = shifted {
|
||||
out.push_str(&format!(":{}", shifted as u32));
|
||||
}
|
||||
let wrote_mods = event > 1 || mods > 1;
|
||||
if event > 1 {
|
||||
out.push_str(&format!(";{mods}:{event}"));
|
||||
} else if mods > 1 {
|
||||
out.push_str(&format!(";{mods}"));
|
||||
let second = match (mods, event) {
|
||||
(_, Some(event)) => format!("{mods}:{event}"),
|
||||
(1, None) => String::new(),
|
||||
(_, None) => mods.to_string(),
|
||||
};
|
||||
let third = if text {
|
||||
code_points(key.text)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if !second.is_empty() || !third.is_empty() {
|
||||
out.push_str(&format!(";{second}"));
|
||||
}
|
||||
let text: Vec<String> = key
|
||||
.text
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_control())
|
||||
.map(|ch| (ch as u32).to_string())
|
||||
.collect();
|
||||
if kitty.text && key.action != Action::Release && !key.ctrl && !key.alt && !text.is_empty() {
|
||||
out.push_str(if wrote_mods { ";" } else { ";;" });
|
||||
out.push_str(&text.join(":"));
|
||||
if !third.is_empty() {
|
||||
out.push_str(&format!(";{third}"));
|
||||
}
|
||||
out.push(end);
|
||||
out.push('u');
|
||||
Some(out.into_bytes())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
use super::{Action, Key, code, printable};
|
||||
|
||||
/// xterm's encoding, as Ghostty sends it with no kitty flags set.
|
||||
pub(super) fn legacy(key: &Key, app_cursor: bool) -> Option<Vec<u8>> {
|
||||
if key.action == Action::Release {
|
||||
return None;
|
||||
}
|
||||
if let Some(sequence) = function(key, app_cursor) {
|
||||
return Some(sequence);
|
||||
}
|
||||
printable_key(key, printable(key.name)?)
|
||||
}
|
||||
|
||||
/// What Ctrl or Alt makes of a printable key; none for text and for Cmd.
|
||||
pub(super) fn printable_key(key: &Key, ch: char) -> Option<Vec<u8>> {
|
||||
if key.platform {
|
||||
return None;
|
||||
}
|
||||
if key.ctrl {
|
||||
if let Some(byte) = control(ch, key.shift) {
|
||||
return Some(if key.alt {
|
||||
vec![0x1B, byte]
|
||||
} else {
|
||||
vec![byte]
|
||||
});
|
||||
}
|
||||
return Some(format!("\x1b[{};{}u", ch as u32, code(key)).into_bytes());
|
||||
}
|
||||
if !key.alt {
|
||||
return None;
|
||||
}
|
||||
let mut bytes = vec![0x1B];
|
||||
if key.text.is_empty() {
|
||||
let typed = if key.shift {
|
||||
ch.to_ascii_uppercase()
|
||||
} else {
|
||||
ch
|
||||
};
|
||||
bytes.extend(typed.to_string().as_bytes());
|
||||
} else {
|
||||
bytes.extend(key.text.as_bytes());
|
||||
}
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
/// xterm's bytes for a function, cursor or editing key.
|
||||
pub(super) fn function(key: &Key, app_cursor: bool) -> Option<Vec<u8>> {
|
||||
let mods = code(key);
|
||||
let modified = mods > 1;
|
||||
let cursor = |end: char| match (modified, app_cursor) {
|
||||
(true, _) => format!("\x1b[1;{mods}{end}"),
|
||||
(false, true) => format!("\x1bO{end}"),
|
||||
(false, false) => format!("\x1b[{end}"),
|
||||
};
|
||||
let tilde = |n: u32| {
|
||||
if modified {
|
||||
format!("\x1b[{n};{mods}~")
|
||||
} else {
|
||||
format!("\x1b[{n}~")
|
||||
}
|
||||
};
|
||||
let ss3 = |end: char| {
|
||||
if modified {
|
||||
format!("\x1b[1;{mods}{end}")
|
||||
} else {
|
||||
format!("\x1bO{end}")
|
||||
}
|
||||
};
|
||||
let alt_only = mods == 3;
|
||||
let sequence = match key.name {
|
||||
"up" => cursor('A'),
|
||||
"down" => cursor('B'),
|
||||
"right" => cursor('C'),
|
||||
"left" => cursor('D'),
|
||||
"home" => cursor('H'),
|
||||
"end" => cursor('F'),
|
||||
"insert" => tilde(2),
|
||||
"delete" => tilde(3),
|
||||
"pageup" => tilde(5),
|
||||
"pagedown" => tilde(6),
|
||||
"f1" => ss3('P'),
|
||||
"f2" => ss3('Q'),
|
||||
"f3" if modified => tilde(13),
|
||||
"f3" => ss3('R'),
|
||||
"f4" => ss3('S'),
|
||||
"enter" if !modified => "\r".into(),
|
||||
"enter" if alt_only => "\x1b\r".into(),
|
||||
"enter" => format!("\x1b[27;{mods};13~"),
|
||||
"tab" if !modified => "\t".into(),
|
||||
"tab" if mods == 2 => "\x1b[Z".into(),
|
||||
"tab" if alt_only => "\x1b\t".into(),
|
||||
"tab" => format!("\x1b[27;{mods};9~"),
|
||||
"escape" if !modified => "\x1b".into(),
|
||||
"escape" if alt_only => "\x1b\x1b".into(),
|
||||
"escape" => format!("\x1b[27;{mods};27~"),
|
||||
"backspace" => {
|
||||
let byte = if key.ctrl { 0x08 } else { 0x7F };
|
||||
return Some(if key.alt {
|
||||
vec![0x1B, byte]
|
||||
} else {
|
||||
vec![byte]
|
||||
});
|
||||
}
|
||||
name => tilde(FUNCTION_TILDES.iter().find(|(f, _)| *f == name)?.1),
|
||||
};
|
||||
Some(sequence.into_bytes())
|
||||
}
|
||||
|
||||
/// F5 to F25 and the numbers xterm sends them with.
|
||||
pub(super) const FUNCTION_TILDES: [(&str, u32); 21] = [
|
||||
("f5", 15),
|
||||
("f6", 17),
|
||||
("f7", 18),
|
||||
("f8", 19),
|
||||
("f9", 20),
|
||||
("f10", 21),
|
||||
("f11", 23),
|
||||
("f12", 24),
|
||||
("f13", 25),
|
||||
("f14", 26),
|
||||
("f15", 28),
|
||||
("f16", 29),
|
||||
("f17", 31),
|
||||
("f18", 32),
|
||||
("f19", 33),
|
||||
("f20", 34),
|
||||
("f21", 42),
|
||||
("f22", 43),
|
||||
("f23", 44),
|
||||
("f24", 45),
|
||||
("f25", 46),
|
||||
];
|
||||
|
||||
/// The C0 byte Ctrl turns a key into, as kitty and Ghostty do; `[`, `i` and `m` go as CSI u.
|
||||
fn control(ch: char, shift: bool) -> Option<u8> {
|
||||
if shift && ch != '@' {
|
||||
return None;
|
||||
}
|
||||
Some(match ch {
|
||||
' ' | '2' | '@' => 0,
|
||||
'a'..='h' | 'j' | 'k' | 'l' | 'n'..='z' => ch as u8 - b'a' + 1,
|
||||
'3' => 27,
|
||||
'4' | '\\' => 28,
|
||||
'5' | ']' => 29,
|
||||
'6' | '^' | '~' => 30,
|
||||
'7' | '/' | '_' => 31,
|
||||
'8' | '?' => 127,
|
||||
'0' | '1' | '9' => ch as u8,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -10,6 +10,7 @@ fn key(spec: &'static str, text: &'static str) -> Key<'static> {
|
||||
shift: parts.contains(&"shift"),
|
||||
ctrl: parts.contains(&"ctrl"),
|
||||
alt: parts.contains(&"alt"),
|
||||
platform: parts.contains(&"cmd"),
|
||||
action: Action::Press,
|
||||
}
|
||||
}
|
||||
@@ -258,3 +259,65 @@ fn kitty_report_all_alternates_and_text() {
|
||||
};
|
||||
assert_eq!(kitty(released, everything).as_deref(), Some("\x1b[13;1:3u"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_that_only_add_fields_keep_legacy_keys() {
|
||||
let alternates = Kitty {
|
||||
alternates: true,
|
||||
..Kitty::default()
|
||||
};
|
||||
let bytes = |spec, flags, app_cursor| encode(&key(spec, ""), app_cursor, flags);
|
||||
assert_eq!(bytes("ctrl+c", alternates, false), Some(vec![0x03]));
|
||||
assert_eq!(bytes("escape", alternates, false), Some(b"\x1b".to_vec()));
|
||||
assert_eq!(bytes("up", alternates, true), Some(b"\x1bOA".to_vec()));
|
||||
assert_eq!(bytes("f1", alternates, false), Some(b"\x1bOP".to_vec()));
|
||||
assert_eq!(
|
||||
bytes("ctrl+shift+a", alternates, false),
|
||||
Some(b"\x1b[97:65;6u".to_vec())
|
||||
);
|
||||
let events = Kitty {
|
||||
events: true,
|
||||
..Kitty::default()
|
||||
};
|
||||
let released = |spec| Key {
|
||||
action: Action::Release,
|
||||
..key(spec, "")
|
||||
};
|
||||
assert_eq!(bytes("ctrl+c", events, false), Some(vec![0x03]));
|
||||
assert_eq!(
|
||||
encode(&released("ctrl+c"), false, events),
|
||||
Some(b"\x1b[99;5:3u".to_vec())
|
||||
);
|
||||
assert_eq!(bytes("f1", events, false), Some(b"\x1b[P".to_vec()));
|
||||
assert_eq!(bytes("escape", events, false), Some(b"\x1b".to_vec()));
|
||||
assert_eq!(
|
||||
encode(&released("escape"), false, events),
|
||||
Some(b"\x1b[27;1:3u".to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn super_is_kittys_eighth_bit() {
|
||||
let events = Kitty {
|
||||
events: true,
|
||||
..DISAMBIGUATE
|
||||
};
|
||||
assert_eq!(
|
||||
kitty(key("cmd+ctrl+c", ""), DISAMBIGUATE).as_deref(),
|
||||
Some("\x1b[99;13u")
|
||||
);
|
||||
let released = Key {
|
||||
action: Action::Release,
|
||||
..key("cmd+ctrl+c", "")
|
||||
};
|
||||
assert_eq!(kitty(released, events).as_deref(), Some("\x1b[99;13:3u"));
|
||||
assert_eq!(legacy(key("cmd+c", ""), false), None, "Cmd types nothing");
|
||||
assert_eq!(
|
||||
legacy(key("cmd+up", ""), false).as_deref(),
|
||||
Some("\x1b[1;9A")
|
||||
);
|
||||
assert_eq!(
|
||||
legacy(key("cmd+enter", ""), false).as_deref(),
|
||||
Some("\x1b[27;9;13~")
|
||||
);
|
||||
}
|
||||
+53
-5
@@ -1,6 +1,7 @@
|
||||
use block::ConcreteBlock;
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{NO, Object},
|
||||
runtime::{BOOL, NO, Object},
|
||||
sel, sel_impl,
|
||||
};
|
||||
|
||||
@@ -8,23 +9,40 @@ use objc::{
|
||||
const KEY_DOWN: u64 = 10;
|
||||
const KEY_UP: u64 = 11;
|
||||
|
||||
/// The Command key's bit in an event's modifier flags.
|
||||
const COMMAND: u64 = 1 << 20;
|
||||
|
||||
/// The device bits of the left and right Option keys in an event's modifier flags.
|
||||
const LEFT_OPTION: u64 = 0x20;
|
||||
const RIGHT_OPTION: u64 = 0x40;
|
||||
|
||||
/// Which Option keys are down in the key event AppKit is handling: left, then right.
|
||||
pub fn option_sides() -> anyhow::Result<(bool, bool)> {
|
||||
/// What gpui drops from the key event AppKit is handling.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct KeyFacts {
|
||||
pub left_option: bool,
|
||||
pub right_option: bool,
|
||||
/// Held down; gpui loses this for keys AppKit routes through commands.
|
||||
pub repeat: bool,
|
||||
}
|
||||
|
||||
/// The facts of the key event AppKit is handling now.
|
||||
pub fn key_facts() -> anyhow::Result<KeyFacts> {
|
||||
unsafe {
|
||||
let app: *mut Object = msg_send![class!(NSApplication), sharedApplication];
|
||||
let event: *mut Object = msg_send![app, currentEvent];
|
||||
anyhow::ensure!(!event.is_null(), "macOS: no key event to read Option from");
|
||||
anyhow::ensure!(!event.is_null(), "macOS: no key event to read");
|
||||
let kind: u64 = msg_send![event, type];
|
||||
anyhow::ensure!(
|
||||
kind == KEY_DOWN || kind == KEY_UP,
|
||||
"macOS: the current event is type {kind}, not a key"
|
||||
);
|
||||
let flags: u64 = msg_send![event, modifierFlags];
|
||||
Ok((flags & LEFT_OPTION != 0, flags & RIGHT_OPTION != 0))
|
||||
let repeat: BOOL = msg_send![event, isARepeat];
|
||||
Ok(KeyFacts {
|
||||
left_option: flags & LEFT_OPTION != 0,
|
||||
right_option: flags & RIGHT_OPTION != 0,
|
||||
repeat: repeat != NO,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,3 +61,33 @@ pub fn repeat_held_keys() {
|
||||
}
|
||||
log::info!("macOS: held keys repeat");
|
||||
}
|
||||
|
||||
/// Hands the focused view the Command key-ups AppKit swallows, as Ghostty does.
|
||||
pub fn deliver_command_key_ups() -> anyhow::Result<()> {
|
||||
let handler = ConcreteBlock::new(|event: *mut Object| -> *mut Object {
|
||||
unsafe {
|
||||
let flags: u64 = msg_send![event, modifierFlags];
|
||||
let window: *mut Object = msg_send![event, window];
|
||||
if flags & COMMAND == 0 || window.is_null() {
|
||||
return event;
|
||||
}
|
||||
let view: *mut Object = msg_send![window, firstResponder];
|
||||
let _: () = msg_send![view, keyUp: event];
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
})
|
||||
.copy();
|
||||
unsafe {
|
||||
let monitor: *mut Object = msg_send![
|
||||
class!(NSEvent),
|
||||
addLocalMonitorForEventsMatchingMask: 1u64 << KEY_UP
|
||||
handler: &*handler
|
||||
];
|
||||
anyhow::ensure!(
|
||||
!monitor.is_null(),
|
||||
"macOS: AppKit refused the key-up monitor"
|
||||
);
|
||||
}
|
||||
log::info!("macOS: Command key-ups reach the focused view");
|
||||
Ok(())
|
||||
}
|
||||
+1
-3
@@ -11,7 +11,7 @@ use gpui::{
|
||||
use super::{
|
||||
frame::frame,
|
||||
ink::Ink,
|
||||
input::{self, SendBackTab, SendTab},
|
||||
input,
|
||||
paint::{self, Look},
|
||||
pty::{Events, Pty, PtyEvent, config, fit},
|
||||
};
|
||||
@@ -261,8 +261,6 @@ impl Render for TerminalView {
|
||||
.track_focus(&self.focus)
|
||||
.on_key_down(cx.listener(Self::key_down))
|
||||
.on_key_up(cx.listener(Self::key_up))
|
||||
.on_action(cx.listener(|view, _: &SendTab, _, cx| view.tab(false, cx)))
|
||||
.on_action(cx.listener(|view, _: &SendBackTab, _, cx| view.tab(true, cx)))
|
||||
.size_full()
|
||||
.px(px(settings.padding_x))
|
||||
.py(px(settings.padding_y))
|
||||
|
||||
Reference in new issue
Block a user