aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/input/KeymapDarwin.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/input/KeymapDarwin.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/input/KeymapDarwin.zig')
-rw-r--r--vendor/libghostty-vt/src/input/KeymapDarwin.zig289
1 files changed, 289 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/input/KeymapDarwin.zig b/vendor/libghostty-vt/src/input/KeymapDarwin.zig
new file mode 100644
index 00000000..a8702730
--- /dev/null
+++ b/vendor/libghostty-vt/src/input/KeymapDarwin.zig
@@ -0,0 +1,289 @@
+// Keymap is responsible for translating keyboard inputs into localized chars.
+///
+/// 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.
+///
+/// This is currently only implemented for macOS.
+const Keymap = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+const macos = @import("macos");
+const codes = @import("keycodes.zig").entries;
+const Mods = @import("key.zig").Mods;
+
+/// 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: *TISInputSource,
+
+/// The keyboard layout for the current input source.
+///
+/// This doesn't need to be freed because its owned by the InputSource.
+unicode_layout: *const UCKeyboardLayout,
+
+pub const Error = error{
+ GetInputSourceFailed,
+ TranslateFailed,
+};
+
+/// 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.
+pub const State = struct {
+ dead_key: u32 = 0,
+};
+
+/// 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.
+pub const Translation = struct {
+ /// 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.
+ text: []const u8 = "",
+
+ /// Whether the text is still composing, i.e. this is a dead key state.
+ composing: bool = false,
+
+ /// The mods that were consumed to produce this translation
+ mods: Mods = .{},
+};
+
+pub fn init() !Keymap {
+ var keymap: Keymap = .{ .source = undefined, .unicode_layout = undefined };
+ try keymap.reinit();
+ return keymap;
+}
+
+pub fn deinit(self: *const Keymap) void {
+ macos.foundation.CFRelease(self.source);
+}
+
+/// Reload the keymap. This must be called if the user changes their
+/// keyboard layout.
+pub fn reload(self: *Keymap) !void {
+ macos.foundation.CFRelease(self.source);
+ try self.reinit();
+}
+
+/// 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 sourceId(self: *const Keymap, buf: []u8) Allocator.Error![]const u8 {
+ // Get the raw CFStringRef
+ const id_raw = TISGetInputSourceProperty(
+ self.source,
+ kTISPropertyInputSourceID,
+ ) orelse return error.OutOfMemory;
+
+ // Convert the CFStringRef to a C string into our buffer.
+ const id: *CFString = @ptrCast(id_raw);
+ return id.cstring(buf, .utf8) orelse error.OutOfMemory;
+}
+
+/// Reinit reinitializes the keymap. It assumes that all the memory associated
+/// with the keymap is already freed.
+fn reinit(self: *Keymap) !void {
+ self.source = TISCopyCurrentKeyboardLayoutInputSource() orelse
+ return Error.GetInputSourceFailed;
+
+ self.unicode_layout = layout: {
+ // This returns a CFDataRef
+ const data_raw = TISGetInputSourceProperty(
+ self.source,
+ kTISPropertyUnicodeKeyLayoutData,
+ ) orelse return Error.GetInputSourceFailed;
+ const data: *CFData = @ptrCast(data_raw);
+
+ // The CFDataRef contains a UCKeyboardLayout pointer
+ break :layout @ptrCast(data.getPointer());
+ };
+
+ if (comptime builtin.mode == .Debug) id: {
+ var buf: [256]u8 = undefined;
+ const id = self.sourceId(&buf) catch break :id;
+ std.log.debug("keyboard layout={s}", .{id});
+ }
+}
+
+/// Translate a single key input into a utf8 sequence.
+pub fn translate(
+ self: *const Keymap,
+ out: []u8,
+ state: *State,
+ code: u16,
+ input_mods: Mods,
+) !Translation {
+ // 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.
+ const mods: Mods = mods: {
+ var v = input_mods;
+ v.ctrl = false;
+ break :mods v;
+ };
+
+ // Get the keycode for the space key, using comptime.
+ const code_space: u16 = comptime space: for (codes) |entry| {
+ if (std.mem.eql(u8, entry.code, "Space"))
+ break :space entry.native;
+ } else @compileError("space code not found");
+
+ // Convert our mods from our format to the Carbon API format
+ const modifier_state: u32 = (CarbonMods{
+ .alt = if (mods.alt) true else false,
+ .ctrl = if (mods.ctrl) true else false,
+ .meta = if (mods.super) true else false,
+ .shift = if (mods.shift) true else false,
+ .caps_lock = if (mods.caps_lock) true else false,
+ }).ucKeyTranslate();
+
+ // 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
+ var char: [4]u16 = undefined;
+ var char_count: c_ulong = 0;
+ if (UCKeyTranslate(
+ self.unicode_layout,
+ code,
+ kUCKeyActionDown,
+ modifier_state,
+ LMGetKbdType(),
+ kUCKeyTranslateNoDeadKeysBit,
+ &state.dead_key,
+ char.len,
+ &char_count,
+ &char,
+ ) != 0) return Error.TranslateFailed;
+
+ // If we got a dead key, then we translate again with "space"
+ // in order to get the pre-edit text.
+ const composing = if (state.dead_key != 0 and char_count == 0) composing: {
+ // We need to copy our dead key state so that it isn't modified.
+ var dead_key_ignore: u32 = state.dead_key;
+ if (UCKeyTranslate(
+ self.unicode_layout,
+ code_space,
+ kUCKeyActionDown,
+ modifier_state,
+ LMGetKbdType(),
+ kUCKeyTranslateNoDeadKeysMask,
+ &dead_key_ignore,
+ char.len,
+ &char_count,
+ &char,
+ ) != 0) return Error.TranslateFailed;
+ break :composing true;
+ } else false;
+
+ // Convert the utf16 to utf8
+ const len = try std.unicode.utf16LeToUtf8(out, char[0..char_count]);
+ return .{
+ .text = out[0..len],
+ .composing = composing,
+ .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.
+const CarbonMods = packed struct(u32) {
+ _padding_start: u8 = 0,
+ meta: bool = false,
+ shift: bool = false,
+ caps_lock: bool = false,
+ alt: bool = false,
+ ctrl: bool = false,
+ _padding_end: u19 = 0,
+
+ /// Translate NSEventModifierFlags into the format used by UCKeyTranslate.
+ fn ucKeyTranslate(self: CarbonMods) u32 {
+ const int: u32 = @bitCast(self);
+ return (int >> 8) & 0xFF;
+ }
+
+ // We got this from dumping the values out of another program since
+ // I can't find the header for this anywhere. I find various modifier
+ // headers but they do not match this! 🥺
+ test "expected values" {
+ const testing = std.testing;
+ try testing.expectEqual(@as(u32, 0x100), @as(u32, @bitCast(CarbonMods{ .meta = true })));
+ try testing.expectEqual(@as(u32, 0x200), @as(u32, @bitCast(CarbonMods{ .shift = true })));
+ try testing.expectEqual(@as(u32, 0x400), @as(u32, @bitCast(CarbonMods{ .caps_lock = true })));
+ try testing.expectEqual(@as(u32, 0x800), @as(u32, @bitCast(CarbonMods{ .alt = true })));
+ try testing.expectEqual(@as(u32, 0x1000), @as(u32, @bitCast(CarbonMods{ .ctrl = true })));
+ }
+};
+
+// 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
+extern "c" fn TISCopyCurrentKeyboardLayoutInputSource() ?*TISInputSource;
+extern "c" fn TISGetInputSourceProperty(*TISInputSource, *CFString) ?*anyopaque;
+extern "c" fn LMGetKbdLast() u8;
+extern "c" fn LMGetKbdType() u8;
+extern "c" fn UCKeyTranslate(*const UCKeyboardLayout, u16, u16, u32, u32, u32, *u32, c_ulong, *c_ulong, [*]u16) i32;
+extern const kTISPropertyLocalizedName: *CFString;
+extern const kTISPropertyUnicodeKeyLayoutData: *CFString;
+extern const kTISPropertyInputSourceID: *CFString;
+const TISInputSource = opaque {};
+const UCKeyboardLayout = opaque {};
+const kUCKeyActionDown: u16 = 0;
+const kUCKeyActionUp: u16 = 1;
+const kUCKeyActionAutoKey: u16 = 2;
+const kUCKeyActionDisplay: u16 = 3;
+const kUCKeyTranslateNoDeadKeysBit: u32 = 0;
+const kUCKeyTranslateNoDeadKeysMask: u32 = 1 << kUCKeyTranslateNoDeadKeysBit;
+
+const CFData = macos.foundation.Data;
+const CFString = macos.foundation.String;
+
+test {
+ var keymap = try init();
+ defer keymap.deinit();
+
+ // These tests are all commented because they depend on the user-selected
+ // keyboard layout...
+ //
+ // // Single quote ' which is fine on US, but dead on US-International
+ // var buf: [4]u8 = undefined;
+ // var state: State = .{};
+ // {
+ // const result = try keymap.translate(&buf, &state, 0x27, .{});
+ // std.log.warn("map: text={s} dead={}", .{ result.text, result.composing });
+ // }
+ //
+ // // Then type "a" which should combine with the dead key to make á
+ // {
+ // const result = try keymap.translate(&buf, &state, 0x00, .{});
+ // std.log.warn("map: text={s} dead={}", .{ result.text, result.composing });
+ // }
+ //
+ // // Shift+1 = ! on US
+ // {
+ // const result = try keymap.translate(&buf, &state, 0x12, .{ .shift = true });
+ // std.log.warn("map: text={s} dead={}", .{ result.text, result.composing });
+ // }
+ //
+ // // Scratch space
+ // {
+ // const result = try keymap.translate(&buf, &state, 0x00, .{ .ctrl = true });
+ // std.log.warn("map: text={s} dead={}", .{ result.text, result.composing });
+ // }
+}