T44: herdr's shortcuts work, which a user found dead: under kitty's flags a key Shift makes a symbol of goes as its own key with Shift, the symbol its alternate, as Ghostty sends it, read from the layout gpui reads on macOS and Windows; on Windows keys that type no text go as win32-input-mode records, which ConPTY asks for and herdr reads, as Windows Terminal sends them; on Linux gpui keeps no key code, which the docs say; shot learns wait, n and ]
CI / platforms (windows-latest) (push) Failing after 19s
CI / macos (push) Failing after 2m56s
CI / platforms (ubuntu-24.04) (push) Successful in 6m41s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-18 04:00:24 -04:00
1 parent 8d7e730a78
commit fc10c4d491
19 files changed
+548 -118

No files matched your search

+107 -9
View File
@@ -1,3 +1,6 @@
use std::ffi::c_void;
use anyhow::Context as _;
use block::ConcreteBlock;
use objc::{
class, msg_send,
@@ -5,13 +8,43 @@ use objc::{
sel, sel_impl,
};
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
static kTISPropertyUnicodeKeyLayoutData: *const c_void;
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut c_void;
fn TISGetInputSourceProperty(source: *mut c_void, key: *const c_void) -> *const c_void;
fn LMGetKbdType() -> u16;
fn UCKeyTranslate(
layout: *const c_void,
code: u16,
action: u16,
modifiers: u32,
keyboard: u32,
options: u32,
dead: *mut u32,
most: usize,
length: *mut usize,
text: *mut u16,
) -> u32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFDataGetBytePtr(data: *const c_void) -> *const u8;
fn CFRelease(object: *const c_void);
}
/// AppKit's event types for a key going down and coming up.
const KEY_DOWN: u64 = 10;
const KEY_UP: u64 = 11;
/// The Command key's bit in an event's modifier flags.
/// The Shift and Command keys' bits in an event's modifier flags.
const SHIFT: u64 = 1 << 17;
const COMMAND: u64 = 1 << 20;
/// Command in a layout's modifier state, as `UCKeyTranslate` takes it.
const LAYOUT_COMMAND: u32 = 1;
/// 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;
@@ -23,11 +56,13 @@ pub struct KeyFacts {
pub right_option: bool,
/// Held down; gpui loses this for keys AppKit routes through commands.
pub repeat: bool,
/// The key's own character, where gpui named the one Shift made and dropped Shift.
pub unshifted: Option<char>,
}
/// The facts of the key event AppKit is handling now.
pub fn key_facts() -> anyhow::Result<KeyFacts> {
unsafe {
/// The facts of the key event AppKit is handling now, which gpui named `name`, with `shift` or not.
pub fn key_facts(name: &str, shift: bool) -> anyhow::Result<KeyFacts> {
let (flags, repeat, code) = 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");
@@ -38,12 +73,75 @@ pub fn key_facts() -> anyhow::Result<KeyFacts> {
);
let flags: u64 = msg_send![event, modifierFlags];
let repeat: BOOL = msg_send![event, isARepeat];
Ok(KeyFacts {
left_option: flags & LEFT_OPTION != 0,
right_option: flags & RIGHT_OPTION != 0,
repeat: repeat != NO,
})
let code: u16 = msg_send![event, keyCode];
(flags, repeat != NO, code)
};
let one = name.chars().count() == 1;
let unshifted = if flags & SHIFT != 0 && !shift && one {
let own = own(code, flags)?;
log::debug!("macOS: Shift made {name:?} of key {code}, whose own is {own:?}");
own
} else {
None
};
Ok(KeyFacts {
left_option: flags & LEFT_OPTION != 0,
right_option: flags & RIGHT_OPTION != 0,
repeat,
unshifted,
})
}
/// Key `code`'s character without Shift, read as gpui reads key names: Command's layout while Command is down or the plain one types no ASCII.
fn own(code: u16, flags: u64) -> anyhow::Result<Option<char>> {
let command =
flags & COMMAND != 0 || (!typed(0, 0)?.is_ascii() && typed(0, LAYOUT_COMMAND)?.is_ascii());
let own = typed(code, if command { LAYOUT_COMMAND } else { 0 })?;
let mut chars = own.chars();
Ok(match (chars.next(), chars.next()) {
(Some(ch), None) if !ch.is_control() => Some(ch),
_ => None,
})
}
/// What key `code` types under `modifiers` in the current layout; a dead key types its own mark.
fn typed(code: u16, modifiers: u32) -> anyhow::Result<String> {
const SPACE: u16 = 49;
let mut text = [0u16; 4];
let mut length = 0;
unsafe {
let source = TISCopyCurrentKeyboardLayoutInputSource();
anyhow::ensure!(!source.is_null(), "macOS: no keyboard layout");
let data = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData);
if !data.is_null() {
let layout = CFDataGetBytePtr(data).cast::<c_void>();
let keyboard = u32::from(LMGetKbdType());
let mut dead = 0;
for key in [code, SPACE] {
UCKeyTranslate(
layout,
key,
0,
modifiers,
keyboard,
0,
&mut dead,
text.len(),
&mut length,
text.as_mut_ptr(),
);
if dead == 0 {
break;
}
}
}
CFRelease(source);
anyhow::ensure!(
!data.is_null(),
"macOS: the keyboard layout has no key table"
);
}
String::from_utf16(&text[..length]).context("macOS: the layout typed no UTF-16")
}
/// Turns off the accent menu a held key opens, so held keys repeat as in other terminals.