Files
ELYTerminal/src/term/mac.rs
T

195 lines
6.7 KiB
Rust

use std::ffi::c_void;
use anyhow::Context as _;
use block::ConcreteBlock;
use objc::{
class, msg_send,
runtime::{BOOL, NO, Object},
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 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;
/// What gpui drops from the key event AppKit is handling.
#[derive(Clone, Copy, Debug, Default)]
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 key's own character, where gpui named the one Shift made and dropped Shift.
pub unshifted: Option<char>,
/// AppKit's key code, which Shift does not change.
pub code: Option<u16>,
}
/// 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");
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];
let repeat: BOOL = msg_send![event, isARepeat];
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,
code: Some(code),
})
}
/// 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.
pub fn repeat_held_keys() {
unsafe {
let key: *mut Object = msg_send![
class!(NSString),
stringWithUTF8String: c"ApplePressAndHoldEnabled".as_ptr()
];
let off: *mut Object = msg_send![class!(NSNumber), numberWithBool: NO];
let defaults: *mut Object =
msg_send![class!(NSDictionary), dictionaryWithObject: off forKey: key];
let store: *mut Object = msg_send![class!(NSUserDefaults), standardUserDefaults];
let _: () = msg_send![store, registerDefaults: defaults];
}
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(())
}