feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)

Ports the macOS-specific Zig code behind cfg(target_os = "macos"):
- renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend
  (all renderer/metal/* files, shaders.metal verbatim, runtime library
  compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga;
  OpenGL remains the backend elsewhere.
- font: CoreText face, shaper and discovery with build.rs backend
  selection (coretext default; coretext-freetype / coretext-harfbuzz /
  coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple.
- input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes.
- os: macos, mach, cf_release_thread, NSProcessInfo args and macOS
  branches; config: macOS defaults, keybinds, Application Support paths.
- termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit
  handling, os_log logging.

Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings
clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot
run here. The embedded runtime/C API (embedded.zig) is the next step.

Also includes the cross-platform integration edits shared by the crash,
verification, Windows and macOS work (Cargo target-gating, thread
crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg
gates), a cfg fix so ghostty-config builds on wasm32, and an allow for
the intentional recursion in the crash overflow test.

Test Plan:
- cargo test --workspace (3224 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check --target aarch64-apple-darwin -p ghostty-renderer (with
  the documented cross env)
This commit is contained in:
ddidderr committed 2026-10-12 06:02:39 +02:00
1 parent 76771bed0d
commit bab49ee596
124 files changed
+14936 -1242

No files matched your search

+31 -3
View File
@@ -538,7 +538,9 @@ impl Key {
/// true if this key is one of the left or right versions of super (MacOS)
/// or ctrl.
pub fn ctrl_or_super(self) -> bool {
// Non-darwin target.
if crate::platform::is_darwin() {
return self == Key::MetaLeft || self == Key::MetaRight;
}
self == Key::ControlLeft || self == Key::ControlRight
}
@@ -558,9 +560,18 @@ impl Key {
/// Mac, we default to super (i.e. super+c for copy) and on
/// non-Mac we default to ctrl (i.e. ctrl+c for copy).
pub fn ctrl_or_super(mods: Mods) -> Mods {
ctrl_or_super_for(mods, crate::platform::is_darwin())
}
/// [`ctrl_or_super`] for an explicit platform (`darwin == true` sets super).
pub fn ctrl_or_super_for(mods: Mods, darwin: bool) -> Mods {
let mut copy = mods;
// Non-darwin target.
copy.ctrl = true;
if darwin {
copy.super_ = true;
} else {
copy.ctrl = true;
}
copy
}
@@ -604,6 +615,23 @@ mod tests {
assert!(Key::Unidentified.should_be_remappable());
}
#[test]
fn key_ctrl_or_super_follows_platform() {
crate::platform::with_darwin(true, || {
assert!(Key::MetaLeft.ctrl_or_super());
assert!(Key::MetaRight.ctrl_or_super());
assert!(!Key::ControlLeft.ctrl_or_super());
assert!(ctrl_or_super(Mods::default()).super_);
assert!(!ctrl_or_super(Mods::default()).ctrl);
});
crate::platform::with_darwin(false, || {
assert!(Key::ControlLeft.ctrl_or_super());
assert!(!Key::MetaLeft.ctrl_or_super());
assert!(ctrl_or_super(Mods::default()).ctrl);
assert!(!ctrl_or_super(Mods::default()).super_);
});
}
#[test]
fn ctrl_or_super_test() {
let m = ctrl_or_super(Mods::default());
+271 -7
View File
@@ -9,6 +9,7 @@ use crate::{
config::OptionAsAlt,
function_keys::{self, CursorMode, KeypadMode, ModifyKeys},
key::{self, Key, KeyEvent},
key_mods::ModSide,
kitty::{self as kitty_table, Entry as KittyEntry},
};
@@ -266,8 +267,16 @@ fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Resu
// Determine if the Alt modifier should be treated as an actual
// modifier (in which case it prevents associated text) or as
// the macOS Option key, which does not prevent associated text.
// (Non-macOS: always true.)
let alt_prevents_text = true;
let alt_prevents_text = if crate::platform::is_macos() {
match opts.macos_option_as_alt {
OptionAsAlt::Left => all_mods.sides.alt == ModSide::Left,
OptionAsAlt::Right => all_mods.sides.alt == ModSide::Right,
OptionAsAlt::True => true,
OptionAsAlt::False => false,
}
} else {
true
};
if !seq.mods.prevents_text(alt_prevents_text) {
seq.text = event.utf8;
@@ -373,7 +382,21 @@ fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Res
if let (Some(codepoint), None) = (it.next().map(u32::from), it.clone().next()) {
// The mods we encode for this are just the binding mods (shift, ctrl,
// super, alt unless it is actually option).
let mods = event.mods.binding();
let mods = {
let mut mods_binding = event.mods.binding();
if crate::platform::is_darwin() {
let keep_alt = match opts.macos_option_as_alt {
OptionAsAlt::False => false,
OptionAsAlt::True => true,
OptionAsAlt::Left => event.mods.sides.alt == ModSide::Left,
OptionAsAlt::Right => event.mods.sides.alt == ModSide::Right,
};
if !keep_alt {
mods_binding.alt = false;
}
}
mods_binding
};
// This copies xterm's `ModifyOtherKeys` function that returns
// whether modify other keys should be encoded for the given
@@ -465,8 +488,18 @@ fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Res
return writer.write_all(&[0x1B, byte]);
}
// (On macOS, command+keys do not encode text. On Linux we continue to
// encode text because it is typical.)
// If we are on macOS, command+keys do not encode text. It isn't
// typical for command+keys on macOS to ever encode text. They
// don't in native text inputs (i.e. TextEdit) and they also don't
// in other native terminals (Terminal.app officially but also
// iTerm2).
//
// For Linux, we continue to encode text because it is typical.
// For example on Gnome Console Super+b will encode a "b" character
// with legacy encoding.
if crate::platform::is_macos() && all_mods.super_ {
return Ok(());
}
writer.write_all(utf8.as_bytes())
}
@@ -474,7 +507,7 @@ fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Res
fn legacy_alt_prefix(
event: &KeyEvent,
binding_mods: key::Mods,
_mods: key::Mods,
mods: key::Mods,
opts: &Options,
) -> Option<u8> {
// This only takes effect with alt pressed
@@ -482,7 +515,25 @@ fn legacy_alt_prefix(
return None;
}
// (macOS option-as-alt handling does not apply on Linux.)
// On macOS, we only handle option like alt in certain
// circumstances. Otherwise, macOS does a unicode translation
// and we allow that to happen.
if crate::platform::is_macos() {
match opts.macos_option_as_alt {
OptionAsAlt::False => return None,
OptionAsAlt::Left => {
if mods.sides.alt == ModSide::Right {
return None;
}
}
OptionAsAlt::Right => {
if mods.sides.alt == ModSide::Left {
return None;
}
}
OptionAsAlt::True => {}
}
}
// Otherwise, we require utf8 to already have the byte represented.
let utf8 = event.utf8.as_bytes();
@@ -3159,3 +3210,216 @@ mod tests {
);
}
}
/// The Zig tests that only run on Darwin (`isDarwin()` / `os.tag == .macos`
/// skip otherwise). They run on every host here by overriding the platform
/// predicates (see [`crate::platform`]).
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod macos_tests {
use super::*;
use crate::{Mods, platform::with_darwin};
fn enc_legacy(ev: &KeyEvent, opts: &Options) -> Vec<u8> {
let mut w = Vec::new();
legacy(&mut w, ev, opts).unwrap();
w
}
fn enc_kitty(ev: &KeyEvent, opts: &Options) -> Vec<u8> {
let mut w = Vec::new();
kitty(&mut w, ev, opts).unwrap();
w
}
fn kitty_opts(option_as_alt: OptionAsAlt) -> Options {
Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
macos_option_as_alt: option_as_alt,
..Options::default()
}
}
#[test]
fn kitty_report_associated_with_alt_text_on_macos_with_option() {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::KeyW,
mods: Mods {
alt: true,
..Mods::default()
},
utf8: "∑",
unshifted_codepoint: 119,
..KeyEvent::default()
};
assert_eq!(
enc_kitty(&ev, &kitty_opts(OptionAsAlt::False)),
"\x1b[119;3;8721u".as_bytes()
);
});
}
#[test]
fn kitty_report_associated_with_alt_text_on_macos_with_alt() {
with_darwin(true, || {
// With Alt modifier
let ev = KeyEvent {
key: Key::KeyW,
mods: Mods {
alt: true,
..Mods::default()
},
utf8: "∑",
unshifted_codepoint: 119,
..KeyEvent::default()
};
assert_eq!(
enc_kitty(&ev, &kitty_opts(OptionAsAlt::True)),
b"\x1b[119;3u"
);
// Without Alt modifier
let ev = KeyEvent {
key: Key::KeyW,
utf8: "∑",
unshifted_codepoint: 119,
..KeyEvent::default()
};
assert_eq!(
enc_kitty(&ev, &kitty_opts(OptionAsAlt::True)),
"\x1b[119;;8721u".as_bytes()
);
});
}
#[test]
fn legacy_alt_x_macos() {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::KeyC,
utf8: "≈",
unshifted_codepoint: u32::from('c'),
mods: Mods {
alt: true,
..Mods::default()
},
..KeyEvent::default()
};
let opts = Options {
alt_esc_prefix: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
};
assert_eq!(enc_legacy(&ev, &opts), b"\x1bc");
// With option-as-alt off, macOS keeps the unicode translation.
let opts = Options {
macos_option_as_alt: OptionAsAlt::False,
..opts
};
assert_eq!(enc_legacy(&ev, &opts), "≈".as_bytes());
});
}
#[test]
fn legacy_shift_alt_period_macos() {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::Period,
utf8: ">",
unshifted_codepoint: u32::from('.'),
mods: Mods {
alt: true,
shift: true,
..Mods::default()
},
..KeyEvent::default()
};
let opts = Options {
alt_esc_prefix: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
};
assert_eq!(enc_legacy(&ev, &opts), b"\x1b>");
});
}
#[test]
fn legacy_alt_digit_with_modify_other_state_2_and_macos_option_as_alt_false() {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::Digit8,
mods: Mods {
alt: true,
..Mods::default()
},
consumed_mods: Mods {
alt: true,
..Mods::default()
},
// common translation of option+8 with European keyboard layouts
utf8: "[",
..KeyEvent::default()
};
let opts = Options {
modify_other_keys_state_2: true,
macos_option_as_alt: OptionAsAlt::False,
..Options::default()
};
assert_eq!(enc_legacy(&ev, &opts), b"[");
});
}
#[test]
fn legacy_super_only_on_macos_with_text() {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::KeyB,
utf8: "b",
mods: Mods {
super_: true,
..Mods::default()
},
..KeyEvent::default()
};
assert_eq!(enc_legacy(&ev, &Options::default()), b"");
});
// On other platforms the text is still encoded.
with_darwin(false, || {
let ev = KeyEvent {
key: Key::KeyB,
utf8: "b",
mods: Mods {
super_: true,
..Mods::default()
},
..KeyEvent::default()
};
assert_eq!(enc_legacy(&ev, &Options::default()), b"b");
});
}
#[test]
fn legacy_super_and_other_mods_on_macos_with_text() {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::KeyB,
utf8: "B",
mods: Mods {
super_: true,
shift: true,
..Mods::default()
},
..KeyEvent::default()
};
assert_eq!(enc_legacy(&ev, &Options::default()), b"");
});
}
}
+87 -12
View File
@@ -213,16 +213,33 @@ impl Mods {
/// Return the mods to use for key translation. This handles settings
/// like macos-option-as-alt. The translation mods should be used for
/// translation but never sent back in for the key callback.
///
/// Only macOS changes anything here; this port targets Linux so the
/// mods are returned unchanged.
pub fn translation(self, _option_as_alt: OptionAsAlt) -> Mods {
self
pub fn translation(self, option_as_alt: OptionAsAlt) -> Mods {
let mut result = self;
// macos-option-as-alt for darwin
if crate::platform::is_darwin() {
// Alt has to be set only on the correct side
let applies = match option_as_alt {
OptionAsAlt::False => false,
OptionAsAlt::True => true,
OptionAsAlt::Left => self.sides.alt != ModSide::Right,
OptionAsAlt::Right => self.sides.alt != ModSide::Left,
};
// Unset alt
if applies {
result.alt = false;
}
}
result
}
/// Checks to see if super is on (MacOS) or ctrl.
pub fn ctrl_or_super(self) -> bool {
// Non-darwin target.
if crate::platform::is_darwin() {
return self.super_;
}
self.ctrl
}
}
@@ -577,12 +594,70 @@ mod tests {
#[test]
fn translation_macos_option_as_alt() {
// Darwin only upstream; on Linux it is the identity.
let mods = Mods {
alt: true,
..Default::default()
};
assert_eq!(mods.translation(OptionAsAlt::True), mods);
crate::platform::with_darwin(true, || {
// Unset
{
let mods = Mods::default();
assert_eq!(mods.translation(OptionAsAlt::True), mods);
}
// Set
{
let mods = m(|x| x.alt = true);
assert_eq!(Mods::default(), mods.translation(OptionAsAlt::True));
}
// Set but disabled
{
let mods = m(|x| x.alt = true);
assert_eq!(mods.translation(OptionAsAlt::False), mods);
}
// Set wrong side
{
let mods = m(|x| {
x.alt = true;
x.sides.alt = ModSide::Right;
});
assert_eq!(mods.translation(OptionAsAlt::Left), mods);
}
{
let mods = m(|x| {
x.alt = true;
x.sides.alt = ModSide::Left;
});
assert_eq!(mods.translation(OptionAsAlt::Right), mods);
}
// Set with other mods
{
let mods = m(|x| {
x.alt = true;
x.shift = true;
});
assert_eq!(m(|x| x.shift = true), mods.translation(OptionAsAlt::True));
}
});
// Not darwin: identity.
crate::platform::with_darwin(false, || {
let mods = m(|x| x.alt = true);
assert_eq!(mods.translation(OptionAsAlt::True), mods);
});
}
#[test]
fn ctrl_or_super_follows_platform() {
let ctrl = m(|x| x.ctrl = true);
let sup = m(|x| x.super_ = true);
crate::platform::with_darwin(true, || {
assert!(sup.ctrl_or_super());
assert!(!ctrl.ctrl_or_super());
});
crate::platform::with_darwin(false, || {
assert!(ctrl.ctrl_or_super());
assert!(!sup.ctrl_or_super());
});
}
fn left(k: &str) -> Mods {
+58 -2
View File
@@ -27,19 +27,60 @@ pub struct Entry {
pub fn entries() -> &'static [Entry] {
static ENTRIES: OnceLock<Vec<Entry>> = OnceLock::new();
ENTRIES.get_or_init(|| {
// Linux/FreeBSD use the xkb column (index 2).
// Linux/FreeBSD use the xkb column (index 2), Windows the win column (3).
RAW_ENTRIES
.iter()
.map(|raw| Entry {
key: code_to_key(raw.5).unwrap_or(Key::Unidentified),
usb: raw.0,
code: raw.5,
native: raw.2,
native: native_code(raw),
})
.collect()
})
}
/// The native keycode column for the current platform (`native_idx` in Zig:
/// xkb = 2 on Linux/BSD, win = 3 on Windows, mac = 4 on macOS/iOS).
#[cfg(windows)]
fn native_code(raw: &RawEntry) -> u32 {
raw.3
}
/// macOS / iOS use the mac column.
#[cfg(target_vendor = "apple")]
fn native_code(raw: &RawEntry) -> u32 {
raw.4
}
/// See the Windows variant.
#[cfg(not(any(windows, target_vendor = "apple")))]
fn native_code(raw: &RawEntry) -> u32 {
raw.2
}
/// The columns of [`RAW_ENTRIES`] holding native keycodes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeColumn {
Xkb,
Win,
Mac,
}
/// The native keycode of the entry with the given W3C DOM `code` in a
/// specific platform column (not only the current one). The Darwin keymap
/// uses this to find the keycode of "Space".
pub fn native_for_code(code: &str, column: NativeColumn) -> Option<u32> {
RAW_ENTRIES
.iter()
.find(|raw| raw.5 == code)
.map(|raw| match column {
NativeColumn::Xkb => raw.2,
NativeColumn::Win => raw.3,
NativeColumn::Mac => raw.4,
})
}
/// A map from code to key. This isn't meant to be used at runtime
/// (though it could), so it isn't exported. It it used to build the
/// key value for Entry.
@@ -685,7 +726,22 @@ mod tests {
let a = e.iter().find(|e| e.code == "KeyA").unwrap();
assert_eq!(a.key, Key::KeyA);
assert_eq!(a.usb, 0x070004);
#[cfg(not(any(windows, target_vendor = "apple")))]
assert_eq!(a.native, 0x0026);
#[cfg(windows)]
assert_eq!(a.native, 0x001e);
#[cfg(target_vendor = "apple")]
assert_eq!(a.native, 0x0000);
}
#[test]
fn mac_native_column() {
// kVK_ANSI_A = 0x00, kVK_Space = 0x31, kVK_Return = 0x24.
assert_eq!(native_for_code("KeyA", NativeColumn::Mac), Some(0x00));
assert_eq!(native_for_code("Space", NativeColumn::Mac), Some(0x31));
assert_eq!(native_for_code("Enter", NativeColumn::Mac), Some(0x24));
assert_eq!(native_for_code("KeyA", NativeColumn::Xkb), Some(0x26));
assert_eq!(native_for_code("Nope", NativeColumn::Mac), None);
}
#[test]
+422
View File
@@ -0,0 +1,422 @@
//! Keymap is responsible for translating keyboard inputs into localized
//! chars. Port of `input/KeymapDarwin.zig`.
//!
//! For example, the physical key "S" on a US-layout keyboard might mean "O"
//! in Dvorak. On international keyboard layouts, it may require multiple
//! keystrokes to produce a single character that is otherwise a single
//! keystroke on a US-layout keyboard.
//!
//! This information is critical to know for many reasons. For keybindings,
//! if a user configures "ctrl+o" to do something, it should work with the
//! physical "ctrl+S" key on a Dvorak keyboard and so on.
//!
//! The Carbon / HIToolbox FFI only exists on macOS (`target_os = "macos"`);
//! the platform-independent pieces (Carbon modifier mapping, UTF-16
//! decoding) compile everywhere so they are unit-testable.
use crate::Mods;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
#[error("get input source failed")]
GetInputSourceFailed,
#[error("translate failed")]
TranslateFailed,
#[error("invalid utf-16 in translation")]
InvalidUtf16,
}
/// The state that has to be passed in with each call to translate.
/// The contents of this are meant to mostly be opaque and can change
/// for platform-specific reasons.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct State {
pub dead_key: u32,
}
/// The result of a translation. The result of a translation can be multiple
/// states. For example, if the user types a dead key, the result will be
/// "composing" since they're still in the process of composing a full
/// character.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Translation {
/// The translation result. If this is a dead key state, then this will
/// be pre-edit text that can be displayed but will ultimately be
/// replaced.
pub text: String,
/// Whether the text is still composing, i.e. this is a dead key state.
pub composing: bool,
/// The mods that were consumed to produce this translation
pub mods: Mods,
}
/// Map to the modifiers format used by the UCKeyTranslate function.
/// We use a u32 here because our bit arithmetic is all u32 anyways.
///
/// Layout (`packed struct(u32)` upstream): 8 padding bits, then `meta`,
/// `shift`, `caps_lock`, `alt`, `ctrl`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CarbonMods {
pub meta: bool,
pub shift: bool,
pub caps_lock: bool,
pub alt: bool,
pub ctrl: bool,
}
impl CarbonMods {
/// The packed u32 representation (`@bitCast` upstream).
pub fn to_bits(self) -> u32 {
u32::from(self.meta) << 8
| u32::from(self.shift) << 9
| u32::from(self.caps_lock) << 10
| u32::from(self.alt) << 11
| u32::from(self.ctrl) << 12
}
/// Translate NSEventModifierFlags into the format used by
/// UCKeyTranslate.
pub fn uc_key_translate(self) -> u32 {
(self.to_bits() >> 8) & 0xFF
}
}
/// Convert our mods to the Carbon API format. On macOS we strip ctrl because
/// UCKeyTranslate converts to the masked values (i.e. ctrl+c becomes 3) and
/// we don't want that behavior in Ghostty ever. This makes this file not a
/// general-purpose keymap implementation.
pub fn carbon_modifier_state(input_mods: Mods) -> (Mods, u32) {
let mut mods = input_mods;
mods.ctrl = false;
let state = CarbonMods {
alt: mods.alt,
ctrl: mods.ctrl,
meta: mods.super_,
shift: mods.shift,
caps_lock: mods.caps_lock,
}
.uc_key_translate();
(mods, state)
}
/// Convert the UTF-16 produced by UCKeyTranslate to UTF-8
/// (`std.unicode.utf16LeToUtf8`).
pub fn utf16_to_utf8(units: &[u16]) -> Result<String, Error> {
String::from_utf16(units).map_err(|_| Error::InvalidUtf16)
}
#[cfg(target_os = "macos")]
pub use darwin::Keymap;
#[cfg(target_os = "macos")]
#[allow(unsafe_code, non_snake_case, non_upper_case_globals)]
mod darwin {
use std::{ffi::c_void, ptr::NonNull};
use objc2_core_foundation::{CFData, CFRetained, CFString, CFType};
use super::{Error, State, Translation, carbon_modifier_state, utf16_to_utf8};
use crate::{
Mods,
keycodes::{NativeColumn, native_for_code},
};
// The documentation for all of these types and functions is in the macOS
// SDK: Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/
// Versions/A/Headers/TextInputSources.h
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut c_void;
fn TISGetInputSourceProperty(source: *mut c_void, key: &CFString) -> *const c_void;
fn LMGetKbdType() -> u8;
fn UCKeyTranslate(
key_layout: *const c_void,
virtual_key_code: u16,
key_action: u16,
modifier_key_state: u32,
keyboard_type: u32,
key_translate_options: u32,
dead_key_state: *mut u32,
max_string_length: std::ffi::c_ulong,
actual_string_length: *mut std::ffi::c_ulong,
unicode_string: *mut u16,
) -> i32;
static kTISPropertyUnicodeKeyLayoutData: &'static CFString;
static kTISPropertyInputSourceID: &'static CFString;
}
const K_UC_KEY_ACTION_DOWN: u16 = 0;
const K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_BIT: u32 = 0;
const K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_MASK: u32 = 1 << K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_BIT;
/// The keyboard layout translation state of the current input source.
pub struct Keymap {
/// The current input source that is selected for the keyboard. This
/// can and does change whenever the user selects a new keyboard
/// layout. This change doesn't happen automatically; the user of
/// this struct has to detect it and then call `reload` to update
/// the keymap.
source: CFRetained<CFType>,
/// The keyboard layout (`UCKeyboardLayout *`) for the current input
/// source. This doesn't need to be freed because its owned by the
/// input source.
unicode_layout: *const c_void,
}
// SAFETY: the input source is a thread-safe CF object and the layout
// data is immutable and owned by it.
unsafe impl Send for Keymap {}
impl Keymap {
pub fn new() -> Result<Keymap, Error> {
let (source, unicode_layout) = Self::load()?;
let keymap = Keymap {
source,
unicode_layout,
};
if cfg!(debug_assertions)
&& let Some(id) = keymap.source_id()
{
log::debug!("keyboard layout={id}");
}
Ok(keymap)
}
/// Reload the keymap. This must be called if the user changes their
/// keyboard layout.
pub fn reload(&mut self) -> Result<(), Error> {
let (source, unicode_layout) = Self::load()?;
self.source = source;
self.unicode_layout = unicode_layout;
Ok(())
}
fn load() -> Result<(CFRetained<CFType>, *const c_void), Error> {
// SAFETY: plain Carbon call; returns a +1 retained input source
// or null.
let raw = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
let raw = NonNull::new(raw.cast::<CFType>()).ok_or(Error::GetInputSourceFailed)?;
// SAFETY: we own the +1 retain count from the Copy function.
let source = unsafe { CFRetained::from_raw(raw) };
// This returns a CFDataRef.
// SAFETY: `source` is a valid TISInputSource and the key is a
// valid constant.
let data_raw = unsafe {
TISGetInputSourceProperty(raw.as_ptr().cast(), kTISPropertyUnicodeKeyLayoutData)
};
if data_raw.is_null() {
return Err(Error::GetInputSourceFailed);
}
// SAFETY: the property is documented to be a CFDataRef owned by
// the input source (alive as long as `source`).
let data = unsafe { &*data_raw.cast::<CFData>() };
// The CFDataRef contains a UCKeyboardLayout pointer.
Ok((source, data.byte_ptr().cast()))
}
/// Get the input source ID for the current keyboard layout. The
/// input source ID is a unique identifier for the keyboard layout
/// which is uniquely defined by Apple.
///
/// This is macOS-only. Other platforms don't have an equivalent of
/// this so this isn't expected to be generally implemented.
pub fn source_id(&self) -> Option<String> {
// SAFETY: valid source and constant key; the result is a
// CFStringRef owned by the source.
let id_raw = unsafe {
TISGetInputSourceProperty(
std::ptr::from_ref::<CFType>(&self.source).cast_mut().cast(),
kTISPropertyInputSourceID,
)
};
if id_raw.is_null() {
return None;
}
// SAFETY: documented to be a CFStringRef.
let id = unsafe { &*id_raw.cast::<CFString>() };
Some(id.to_string())
}
/// Translate a single key input into a utf8 sequence.
pub fn translate(
&self,
state: &mut State,
code: u16,
input_mods: Mods,
) -> Result<Translation, Error> {
// On macOS we strip ctrl because UCKeyTranslate converts to the
// masked values (see `carbon_modifier_state`).
let (mods, modifier_state) = carbon_modifier_state(input_mods);
// Get the keycode for the space key.
let code_space =
native_for_code("Space", NativeColumn::Mac).expect("space code not found") as u16;
// We use 4 here because the Chromium source code uses 4 and
// Chrome works pretty well. They have a todo to look into
// longer sequences but given how mature that software is I think
// this is fine.
//
// From Chromium: Per Apple docs, the buffer length can be up to
// 255 but is rarely more than 4.
// https://developer.apple.com/documentation/coreservices/1390584-uckeytranslate
let mut char_buf = [0u16; 4];
let mut char_count: std::ffi::c_ulong = 0;
// SAFETY: the layout pointer is valid while `self.source` lives;
// all out pointers reference live locals with matching sizes.
let rc = unsafe {
UCKeyTranslate(
self.unicode_layout,
code,
K_UC_KEY_ACTION_DOWN,
modifier_state,
u32::from(LMGetKbdType()),
K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_BIT,
&raw mut state.dead_key,
char_buf.len() as std::ffi::c_ulong,
&raw mut char_count,
char_buf.as_mut_ptr(),
)
};
if rc != 0 {
return Err(Error::TranslateFailed);
}
// If we got a dead key, then we translate again with "space" in
// order to get the pre-edit text.
let composing = if state.dead_key != 0 && char_count == 0 {
// We need to copy our dead key state so that it isn't
// modified.
let mut dead_key_ignore: u32 = state.dead_key;
// SAFETY: as above.
let rc = unsafe {
UCKeyTranslate(
self.unicode_layout,
code_space,
K_UC_KEY_ACTION_DOWN,
modifier_state,
u32::from(LMGetKbdType()),
K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_MASK,
&raw mut dead_key_ignore,
char_buf.len() as std::ffi::c_ulong,
&raw mut char_count,
char_buf.as_mut_ptr(),
)
};
if rc != 0 {
return Err(Error::TranslateFailed);
}
true
} else {
false
};
// Convert the utf16 to utf8
let text = utf16_to_utf8(&char_buf[..(char_count as usize).min(char_buf.len())])?;
Ok(Translation {
text,
composing,
mods,
})
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn carbon_mods_expected_values() {
// We got this from dumping the values out of another program since
// I can't find the header for this anywhere.
assert_eq!(
CarbonMods {
meta: true,
..Default::default()
}
.to_bits(),
0x100
);
assert_eq!(
CarbonMods {
shift: true,
..Default::default()
}
.to_bits(),
0x200
);
assert_eq!(
CarbonMods {
caps_lock: true,
..Default::default()
}
.to_bits(),
0x400
);
assert_eq!(
CarbonMods {
alt: true,
..Default::default()
}
.to_bits(),
0x800
);
assert_eq!(
CarbonMods {
ctrl: true,
..Default::default()
}
.to_bits(),
0x1000
);
}
#[test]
fn uc_key_translate_shifts_down_eight_bits() {
let m = CarbonMods {
meta: true,
shift: true,
..Default::default()
};
assert_eq!(m.uc_key_translate(), 0x03);
let m = CarbonMods {
alt: true,
ctrl: true,
..Default::default()
};
assert_eq!(m.uc_key_translate(), 0x18);
}
#[test]
fn modifier_state_strips_ctrl_and_maps_super_to_meta() {
let (mods, state) = carbon_modifier_state(Mods {
ctrl: true,
super_: true,
shift: true,
caps_lock: true,
alt: true,
..Mods::default()
});
assert!(!mods.ctrl);
assert!(mods.super_ && mods.shift && mods.alt && mods.caps_lock);
// meta(1) | shift(2) | caps(4) | alt(8); ctrl removed.
assert_eq!(state, 0x0F);
}
#[test]
fn utf16_decoding() {
assert_eq!(utf16_to_utf8(&[0x00E1]).unwrap(), "á");
assert_eq!(utf16_to_utf8(&[0xD83D, 0xDE00]).unwrap(), "😀");
assert_eq!(utf16_to_utf8(&[]).unwrap(), "");
assert_eq!(utf16_to_utf8(&[0xD83D]), Err(Error::InvalidUtf16));
}
}
+53
View File
@@ -0,0 +1,53 @@
//! A noop implementation of the keymap interface so that the embedded
//! library can compile on non-macOS platforms. Port of
//! `input/KeymapNoop.zig`.
use crate::Mods;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct State;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Translation {
pub text: String,
pub composing: bool,
pub mods: Mods,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Keymap;
impl Keymap {
#[allow(clippy::unnecessary_wraps)]
pub fn new() -> Result<Keymap, std::convert::Infallible> {
Ok(Keymap)
}
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
pub fn reload(&mut self) -> Result<(), std::convert::Infallible> {
Ok(())
}
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
pub fn translate(
&self,
_state: &mut State,
_code: u16,
_mods: Mods,
) -> Result<Translation, std::convert::Infallible> {
Ok(Translation::default())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn noop_translate_is_empty() {
let km = Keymap::new().unwrap();
let t = km.translate(&mut State, 0, Mods::default()).unwrap();
assert_eq!(t, Translation::default());
}
}
+9
View File
@@ -10,15 +10,24 @@ pub mod key_encode;
pub mod key_mods;
pub mod keyboard;
pub mod keycodes;
pub mod keymap_darwin;
pub mod keymap_noop;
pub mod kitty;
pub mod link;
pub mod mouse;
pub mod paste;
pub mod platform;
pub use config::OptionAsAlt;
pub use key::{Action, Key, KeyEvent, ctrl_or_super};
pub use key_mods::{Mods, RemapSet as KeyRemapSet};
pub use keyboard::Layout as KeyboardLayout;
/// The platform keymap: Carbon `UCKeyTranslate` on macOS, a no-op elsewhere
/// (Zig: `input.Keymap`).
#[cfg(target_os = "macos")]
pub use keymap_darwin::{Keymap, State as KeymapState, Translation as KeymapTranslation};
#[cfg(not(target_os = "macos"))]
pub use keymap_noop::{Keymap, State as KeymapState, Translation as KeymapTranslation};
pub use link::Link;
pub use mouse::{
Button as MouseButton,
+51
View File
@@ -0,0 +1,51 @@
//! Compile-time platform predicates used by the input code
//! (`builtin.target.os.tag.isDarwin()` / `builtin.os.tag == .macos` in Zig).
//!
//! In test builds the predicates can be overridden per thread with
//! [`with_darwin`] so the macOS-only branches (option-as-alt, super-instead-
//! of-ctrl, keycode table column) are exercised on every host.
/// `isDarwin()`: macOS and iOS.
const DARWIN: bool = cfg!(target_vendor = "apple");
/// `os.tag == .macos`.
const MACOS: bool = cfg!(target_os = "macos");
#[cfg(test)]
thread_local! {
static OVERRIDE: std::cell::Cell<Option<(bool, bool)>> =
const { std::cell::Cell::new(None) };
}
/// True when targeting Darwin (macOS or iOS).
#[inline]
pub fn is_darwin() -> bool {
#[cfg(test)]
if let Some((darwin, _)) = OVERRIDE.with(std::cell::Cell::get) {
return darwin;
}
DARWIN
}
/// True when targeting macOS proper.
#[inline]
pub fn is_macos() -> bool {
#[cfg(test)]
if let Some((_, macos)) = OVERRIDE.with(std::cell::Cell::get) {
return macos;
}
MACOS
}
/// Run `f` pretending to be macOS (`darwin == macos == true`) or not.
#[cfg(test)]
pub fn with_darwin<R>(on: bool, f: impl FnOnce() -> R) -> R {
struct Reset(Option<(bool, bool)>);
impl Drop for Reset {
fn drop(&mut self) {
OVERRIDE.with(|o| o.set(self.0));
}
}
let _reset = Reset(OVERRIDE.with(|o| o.replace(Some((on, on)))));
f()
}