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:
1 parent
3c8fae1089
commit
452b782149
124 files changed
+14936
-1242
No files matched your search
@@ -10,5 +10,37 @@ ghostty-unicode.workspace = true
|
||||
indexmap = "2.14.2"
|
||||
thiserror = "2.0.21"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
# Same as `[workspace.lints]` except `unsafe_code` is denied instead of
|
||||
# forbidden: the macOS Carbon keymap (`keymap_darwin.rs`) needs `unsafe` FFI.
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_lossless = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
too_many_lines = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
similar_names = "allow"
|
||||
struct_excessive_bools = "allow"
|
||||
fn_params_excessive_bools = "allow"
|
||||
match_same_arms = "allow"
|
||||
unreadable_literal = "allow"
|
||||
many_single_char_names = "allow"
|
||||
doc_markdown = "allow"
|
||||
must_use_candidate = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
used_underscore_binding = "allow"
|
||||
large_stack_arrays = "allow"
|
||||
inline_always = "allow"
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||
log = "0.4.34"
|
||||
objc2-core-foundation = "0.3.2"
|
||||
@@ -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());
|
||||
|
||||
@@ -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"");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user