summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Surface.zig18
-rw-r--r--src/apprt/embedded.zig38
-rw-r--r--src/config/Config.zig35
-rw-r--r--src/input.zig2
-rw-r--r--src/input/KeyEncoder.zig4
-rw-r--r--src/input/KeymapDarwin.zig26
-rw-r--r--src/input/keyboard.zig58
7 files changed, 166 insertions, 15 deletions
diff --git a/src/Surface.zig b/src/Surface.zig
index 3e7300d08..9fc5b1d90 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -245,7 +245,7 @@ const DerivedConfig = struct {
mouse_scroll_multiplier: f64,
mouse_shift_capture: configpkg.MouseShiftCapture,
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
- macos_option_as_alt: configpkg.OptionAsAlt,
+ macos_option_as_alt: ?configpkg.OptionAsAlt,
vt_kam_allowed: bool,
window_padding_top: u32,
window_padding_bottom: u32,
@@ -1990,12 +1990,26 @@ fn encodeKey(
// inputs there are many keybindings that result in no encoding
// whatsoever.
const enc: input.KeyEncoder = enc: {
+ const option_as_alt: configpkg.OptionAsAlt = self.config.macos_option_as_alt orelse detect: {
+ // Non-macOS doesn't use this value so ignore.
+ if (comptime builtin.os.tag != .macos) break :detect .false;
+
+ // If we don't have alt pressed, it doesn't matter what this
+ // config is so we can just say "false" and break out and avoid
+ // more expensive checks below.
+ if (!event.mods.alt) break :detect .false;
+
+ // Alt is pressed, we're on macOS. We break some encapsulation
+ // here and assume libghostty for ease...
+ break :detect self.rt_app.keyboardLayout().detectOptionAsAlt();
+ };
+
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const t = &self.io.terminal;
break :enc .{
.event = event,
- .macos_option_as_alt = self.config.macos_option_as_alt,
+ .macos_option_as_alt = option_as_alt,
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
.cursor_key_application = t.modes.get(.cursor_keys),
.keypad_key_application = t.modes.get(.keypad_keys),
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 6a4411a85..451605af7 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -105,11 +105,14 @@ pub const App = struct {
var config_clone = try config.clone(alloc);
errdefer config_clone.deinit();
+ var keymap = try input.Keymap.init();
+ errdefer keymap.deinit();
+
return .{
.core_app = core_app,
.config = config_clone,
.opts = opts,
- .keymap = try input.Keymap.init(),
+ .keymap = keymap,
.keymap_state = .{},
};
}
@@ -161,8 +164,15 @@ pub const App = struct {
// then we strip the alt modifier from the mods for translation.
const translate_mods = translate_mods: {
var translate_mods = mods;
- if (comptime builtin.target.isDarwin()) {
- const strip = switch (self.config.@"macos-option-as-alt") {
+ if ((comptime builtin.target.isDarwin()) and translate_mods.alt) {
+ // Note: the keyboardLayout() function is not super cheap
+ // so we only want to run it if alt is already pressed hence
+ // the above condition.
+ const option_as_alt: configpkg.OptionAsAlt =
+ self.config.@"macos-option-as-alt" orelse
+ self.keyboardLayout().detectOptionAsAlt();
+
+ const strip = switch (option_as_alt) {
.false => false,
.true => mods.alt,
.left => mods.sides.alt == .left,
@@ -382,6 +392,25 @@ pub const App = struct {
}
}
+ /// Loads the keyboard layout.
+ ///
+ /// Kind of expensive so this should be avoided if possible. When I say
+ /// "kind of expensive" I mean that its not something you probably want
+ /// to run on every keypress.
+ pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
+ // We only support keyboard layout detection on macOS.
+ if (comptime builtin.os.tag != .macos) return .unknown;
+
+ // Any layout larger than this is not something we can handle.
+ var buf: [256]u8 = undefined;
+ const id = self.keymap.sourceId(&buf) catch |err| {
+ comptime assert(@TypeOf(err) == error{OutOfMemory});
+ return .unknown;
+ };
+
+ return input.KeyboardLayout.mapAppleId(id) orelse .unknown;
+ }
+
pub fn wakeup(self: *const App) void {
self.opts.wakeup(self.opts.userdata);
}
@@ -1551,7 +1580,8 @@ pub const CAPI = struct {
@truncate(@as(c_uint, @bitCast(mods_raw))),
));
const result = mods.translation(
- surface.core_surface.config.macos_option_as_alt,
+ surface.core_surface.config.macos_option_as_alt orelse
+ surface.app.keyboardLayout().detectOptionAsAlt(),
);
return @intCast(@as(input.Mods.Backing, @bitCast(result)));
}
diff --git a/src/config/Config.zig b/src/config/Config.zig
index fa531dc7e..7771a60ec 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1574,20 +1574,41 @@ keybind: Keybinds = .{},
/// editor, etc.
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
+/// macOS doesn't have a distinct "alt" key and instead has the "option"
+/// key which behaves slightly differently. On macOS by default, the
+/// option key plus a character will sometimes produces a Unicode character.
+/// For example, on US standard layouts option-b produces "∫". This may be
+/// undesirable if you want to use "option" as an "alt" key for keybindings
+/// in terminal programs or shells.
+///
+/// This configuration lets you change the behavior so that option is treated
+/// as alt.
+///
+/// The default behavior (unset) will depend on your active keyboard
+/// layout. If your keyboard layout is one of the keyboard layouts listed
+/// below, then the default value is "true". Otherwise, the default
+/// value is "false". Keyboard layouts with a default value of "true" are:
+///
+/// - U.S. Standard
+/// - U.S. International
+///
+/// Note that if an *Option*-sequence doesn't produce a printable character, it
+/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
+///
+/// Explicit values that can be set:
+///
/// If `true`, the *Option* key will be treated as *Alt*. This makes terminal
/// sequences expecting *Alt* to work properly, but will break Unicode input
-/// sequences on macOS if you use them via the *Alt* key. You may set this to
-/// `false` to restore the macOS *Alt* key unicode sequences but this will break
-/// terminal sequences expecting *Alt* to work.
+/// sequences on macOS if you use them via the *Alt* key.
+///
+/// You may set this to `false` to restore the macOS *Alt* key unicode
+/// sequences but this will break terminal sequences expecting *Alt* to work.
///
/// The values `left` or `right` enable this for the left or right *Option*
/// key, respectively.
///
-/// Note that if an *Option*-sequence doesn't produce a printable character, it
-/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
-///
/// This does not work with GLFW builds.
-@"macos-option-as-alt": OptionAsAlt = .false,
+@"macos-option-as-alt": ?OptionAsAlt = null,
/// Whether to enable the macOS window shadow. The default value is true.
/// With some window managers and window transparency settings, you may
diff --git a/src/input.zig b/src/input.zig
index 9e3997d97..83be38d3d 100644
--- a/src/input.zig
+++ b/src/input.zig
@@ -3,6 +3,7 @@ const builtin = @import("builtin");
const mouse = @import("input/mouse.zig");
const key = @import("input/key.zig");
+const keyboard = @import("input/keyboard.zig");
pub const function_keys = @import("input/function_keys.zig");
pub const keycodes = @import("input/keycodes.zig");
@@ -13,6 +14,7 @@ pub const Action = key.Action;
pub const Binding = @import("input/Binding.zig");
pub const Link = @import("input/Link.zig");
pub const Key = key.Key;
+pub const KeyboardLayout = keyboard.Layout;
pub const KeyEncoder = @import("input/KeyEncoder.zig");
pub const KeyEvent = key.KeyEvent;
pub const InspectorMode = Binding.Action.InspectorMode;
diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig
index 25d85e78d..4bac7ee6b 100644
--- a/src/input/KeyEncoder.zig
+++ b/src/input/KeyEncoder.zig
@@ -208,7 +208,7 @@ fn kitty(
// 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.
- const alt_prevents_text = if (comptime builtin.target.isDarwin())
+ const alt_prevents_text = if (comptime builtin.os.tag == .macos)
switch (self.macos_option_as_alt) {
.left => all_mods.sides.alt == .left,
.right => all_mods.sides.alt == .right,
@@ -422,7 +422,7 @@ fn legacyAltPrefix(
// On macOS, we only handle option like alt in certain
// circumstances. Otherwise, macOS does a unicode translation
// and we allow that to happen.
- if (comptime builtin.target.isDarwin()) {
+ if (comptime builtin.os.tag == .macos) {
switch (self.macos_option_as_alt) {
.false => return null,
.left => if (mods.sides.alt == .right) return null,
diff --git a/src/input/KeymapDarwin.zig b/src/input/KeymapDarwin.zig
index 5ba7c6440..3d81b0f4b 100644
--- a/src/input/KeymapDarwin.zig
+++ b/src/input/KeymapDarwin.zig
@@ -14,6 +14,7 @@ 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 Key = @import("key.zig").Key;
@@ -72,6 +73,24 @@ pub fn reload(self: *Keymap) !void {
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 {
@@ -89,6 +108,12 @@ fn reinit(self: *Keymap) !void {
// 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.
@@ -200,6 +225,7 @@ 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;
diff --git a/src/input/keyboard.zig b/src/input/keyboard.zig
new file mode 100644
index 000000000..73674df2c
--- /dev/null
+++ b/src/input/keyboard.zig
@@ -0,0 +1,58 @@
+const std = @import("std");
+const OptionAsAlt = @import("../config.zig").OptionAsAlt;
+
+/// Keyboard layouts.
+///
+/// These aren't heavily used in Ghostty and having a fully comprehensive
+/// list is not important. We only need to distinguish between a few
+/// different layouts for some nice-to-have features, such as setting a default
+/// value for "macos-option-as-alt".
+pub const Layout = enum {
+ // Unknown, unmapped layout. Ghostty should not make any assumptions
+ // about the layout of the keyboard.
+ unknown,
+
+ // The remaining should be fairly self-explanatory:
+ us_standard,
+ us_international,
+
+ /// Map an Apple keyboard layout ID to a value in this enum. The layout
+ /// ID can be retrieved using Carbon's TIKeyboardLayoutGetInputSourceProperty
+ /// function.
+ ///
+ /// Even though our layout supports "unknown", we return null if we don't
+ /// recognize the layout ID so callers can detect this scenario.
+ pub fn mapAppleId(id: []const u8) ?Layout {
+ if (std.mem.eql(u8, id, "com.apple.keylayout.US")) {
+ return .us_standard;
+ } else if (std.mem.eql(u8, id, "com.apple.keylayout.USInternational")) {
+ return .us_international;
+ }
+
+ return null;
+ }
+
+ /// Returns the default macos-option-as-alt value for this layout.
+ ///
+ /// We apply some heuristics to change the default based on the keyboard
+ /// layout if "macos-option-as-alt" is unset. We do this because on some
+ /// keyboard layouts such as US standard layouts, users generally expect
+ /// an input such as option-b to map to alt-b but macOS by default will
+ /// convert it to the codepoint "∫".
+ ///
+ /// This behavior however is desired on international layout where the
+ /// option key is used for important, regularly used inputs.
+ pub fn detectOptionAsAlt(self: Layout) OptionAsAlt {
+ return switch (self) {
+ // On US standard, the option key is typically used as alt
+ // and not as a modifier for other codepoints. For example,
+ // option-B = ∫ but usually the user wants alt-B.
+ .us_standard,
+ .us_international,
+ => .true,
+
+ .unknown,
+ => .false,
+ };
+ }
+};