From 3556cf840709f1418a20035ca0bd9800adab148f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Aug 2023 11:51:10 -0700 Subject: [PATCH 1/4] input: unify binding-sensitive mods to a single func --- src/Surface.zig | 7 +------ src/input/Binding.zig | 32 +++++++++++++++++++++++++++----- src/input/key.zig | 10 ++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3462df8f0..e1b226da1 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1094,12 +1094,7 @@ pub fn keyCallback( if (action != .press and action != .repeat) return false; // Mods for bindings never include caps/num lock. - const binding_mods = mods: { - var binding_mods = mods; - binding_mods.caps_lock = false; - binding_mods.num_lock = false; - break :mods binding_mods; - }; + const binding_mods = mods.binding(); // Check if we're processing a binding first. If so, that negates // any further key processing. diff --git a/src/input/Binding.zig b/src/input/Binding.zig index fd6fa940c..6431853bd 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -293,10 +293,10 @@ pub const Trigger = struct { physical: bool = false, /// Returns a hash code that can be used to uniquely identify this trigger. - pub fn hash(self: Binding) u64 { + pub fn hash(self: Trigger) u64 { var hasher = std.hash.Wyhash.init(0); std.hash.autoHash(&hasher, self.key); - std.hash.autoHash(&hasher, self.mods); + std.hash.autoHash(&hasher, self.mods.binding()); std.hash.autoHash(&hasher, self.physical); return hasher.final(); } @@ -306,7 +306,12 @@ pub const Trigger = struct { /// The use case is that this will be called on EVERY key input to look /// for an associated action so it must be fast. pub const Set = struct { - const HashMap = std.AutoHashMapUnmanaged(Trigger, Action); + const HashMap = std.HashMapUnmanaged( + Trigger, + Action, + Context, + std.hash_map.default_max_load_percentage, + ); /// The set of bindings. bindings: HashMap = .{}, @@ -318,10 +323,14 @@ pub const Set = struct { /// Add a binding to the set. If the binding already exists then /// this will overwrite it. - pub fn put(self: *Set, alloc: Allocator, t: Trigger, action: Action) !void { + pub fn put( + self: *Set, + alloc: Allocator, + t: Trigger, + action: Action, + ) !void { // unbind should never go into the set, it should be handled prior assert(action != .unbind); - try self.bindings.put(alloc, t, action); } @@ -334,6 +343,19 @@ pub const Set = struct { pub fn remove(self: *Set, t: Trigger) void { _ = self.bindings.remove(t); } + + /// The hash map context for the set. This defines how the hash map + /// gets the hash key and checks for equality. + const Context = struct { + pub fn hash(ctx: Context, k: Trigger) u64 { + _ = ctx; + return k.hash(); + } + + pub fn eql(ctx: Context, a: Trigger, b: Trigger) bool { + return ctx.hash(a) == ctx.hash(b); + } + }; }; test "parse: triggers" { diff --git a/src/input/key.zig b/src/input/key.zig index 2d951b996..99ebeb192 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -24,6 +24,16 @@ pub const Mods = packed struct(u8) { return @as(u8, @bitCast(self)) == @as(u8, @bitCast(other)); } + /// Return mods that are only relevant for bindings. + pub fn binding(self: Mods) Mods { + return .{ + .shift = self.shift, + .ctrl = self.ctrl, + .alt = self.alt, + .super = self.super, + }; + } + /// Returns the mods without locks set. pub fn withoutLocks(self: Mods) Mods { var copy = self; From e7bb9c60b21a30e8142c96069b2fa2b2c8651641 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Aug 2023 12:31:16 -0700 Subject: [PATCH 2/4] input: expand Mods size, convert everything to use it --- src/Surface.zig | 8 ++++---- src/apprt/embedded.zig | 10 ++++++++-- src/apprt/glfw.zig | 14 ++++++++++++-- src/input/key.zig | 36 +++++++++++++++++++++++++++++------- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index e1b226da1..80d18f47e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1149,8 +1149,8 @@ pub fn keyCallback( .set_other => if (!modify_other_keys) continue, } - const mods_int: u8 = @bitCast(binding_mods); - const entry_mods_int: u8 = @bitCast(entry.mods); + const mods_int = binding_mods.int(); + const entry_mods_int = entry.mods.int(); if (entry_mods_int == 0) { if (mods_int != 0 and !entry.mods_empty_is_any) continue; // mods are either empty, or empty means any so we allow it. @@ -1186,8 +1186,8 @@ pub fn keyCallback( // Handle non-printables const char: u8 = char: { - const mods_int: u8 = @bitCast(unalt_mods); - const ctrl_only: u8 = @bitCast(input.Mods{ .ctrl = true }); + const mods_int = unalt_mods.int(); + const ctrl_only = (input.Mods{ .ctrl = true }).int(); // If we're only pressing control, check if this is a character // we convert to a non-printable. The best table I've found for diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index efd1480af..7c2f0514a 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -660,7 +660,10 @@ pub const CAPI = struct { surface.keyCallback( action, keycode, - @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(c_mods))))), + @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(c_mods))), + )), ) catch |err| { log.err("error processing key event err={}", .{err}); return; @@ -684,7 +687,10 @@ pub const CAPI = struct { surface.mouseButtonCallback( action, button, - @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(mods))))), + @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(mods))), + )), ); } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 90d08dfba..bb00efafe 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -618,7 +618,12 @@ pub const Surface = struct { const core_win = window.getUserPointer(CoreSurface) orelse return; // Convert our glfw types into our input types - const mods: input.Mods = @bitCast(glfw_mods); + const mods: input.Mods = .{ + .shift = glfw_mods.shift, + .ctrl = glfw_mods.control, + .alt = glfw_mods.alt, + .super = glfw_mods.super, + }; const action: input.Action = switch (glfw_action) { .release => .release, .press => .press, @@ -843,7 +848,12 @@ pub const Surface = struct { const core_win = window.getUserPointer(CoreSurface) orelse return; // Convert glfw button to input button - const mods: input.Mods = @bitCast(glfw_mods); + const mods: input.Mods = .{ + .shift = glfw_mods.shift, + .ctrl = glfw_mods.control, + .alt = glfw_mods.alt, + .super = glfw_mods.super, + }; const button: input.MouseButton = switch (glfw_button) { .left => .left, .right => .right, diff --git a/src/input/key.zig b/src/input/key.zig index 99ebeb192..8ec485c9a 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -5,23 +5,45 @@ const Allocator = std.mem.Allocator; /// GLFW representation, but we use this generically. /// /// IMPORTANT: Any changes here update include/ghostty.h -pub const Mods = packed struct(u8) { +pub const Mods = packed struct(Mods.Backing) { + pub const Backing = u16; + shift: bool = false, ctrl: bool = false, alt: bool = false, super: bool = false, caps_lock: bool = false, num_lock: bool = false, - _padding: u2 = 0, + sides: side = .{}, + _padding: u6 = 0, + + /// Tracks the side that is active for any given modifier. Note + /// that this doesn't confirm a modifier is pressed; you must check + /// the bool for that in addition to this. + /// + /// Not all platforms support this, check apprt for more info. + pub const side = packed struct(u4) { + shift: Side = .left, + ctrl: Side = .left, + alt: Side = .left, + super: Side = .left, + }; + + pub const Side = enum { left, right }; + + /// Integer value of this struct. + pub fn int(self: Mods) Backing { + return @bitCast(self); + } /// Returns true if no modifiers are set. pub fn empty(self: Mods) bool { - return @as(u8, @bitCast(self)) == 0; + return self.int() == 0; } /// Returns true if two mods are equal. pub fn equal(self: Mods, other: Mods) bool { - return @as(u8, @bitCast(self)) == @as(u8, @bitCast(other)); + return self.int() == other.int(); } /// Return mods that are only relevant for bindings. @@ -45,10 +67,10 @@ pub const Mods = packed struct(u8) { // For our own understanding test { const testing = std.testing; - try testing.expectEqual(@as(u8, @bitCast(Mods{})), @as(u8, 0b0)); + try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0)); try testing.expectEqual( - @as(u8, @bitCast(Mods{ .shift = true })), - @as(u8, 0b0000_0001), + @as(Backing, @bitCast(Mods{ .shift = true })), + @as(Backing, 0b0000_0001), ); } }; From 721087be7624ce2fd0def7ff3fba91905072c29e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Aug 2023 12:40:48 -0700 Subject: [PATCH 3/4] macos: send the left/right status of modifier keys --- include/ghostty.h | 4 ++++ macos/Sources/Ghostty/SurfaceView.swift | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index d2d74ea6c..af3aa1fa7 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -83,6 +83,10 @@ typedef enum { GHOSTTY_MODS_SUPER = 1 << 3, GHOSTTY_MODS_CAPS = 1 << 4, GHOSTTY_MODS_NUM = 1 << 5, + GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6, + GHOSTTY_MODS_CTRL_RIGHT = 1 << 7, + GHOSTTY_MODS_ALT_RIGHT = 1 << 8, + GHOSTTY_MODS_SUPER_RIGHT = 1 << 9, } ghostty_input_mods_e; typedef enum { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 178885976..cad0fdbd5 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -480,12 +480,21 @@ extension Ghostty { private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue + if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue } if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue } if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue } if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue } if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } + // Handle sided input. We can't tell that both are pressed in the + // Ghostty structure but thats okay -- we don't use that information. + let rawFlags = flags.rawValue + if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } + if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } + if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue } + if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue } + return ghostty_input_mods_e(mods) } From cbd6a325e95e8fe492d5a4ed054ab81fd03b3e48 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Aug 2023 12:50:21 -0700 Subject: [PATCH 4/4] config: macos-option-as-alt now accepts "left", "right" --- src/Surface.zig | 10 ++++++---- src/apprt/embedded.zig | 12 ++++++++++-- src/config.zig | 10 +++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 80d18f47e..c4516700d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -142,7 +142,7 @@ const DerivedConfig = struct { confirm_close_surface: bool, mouse_interval: u64, macos_non_native_fullscreen: bool, - macos_option_as_alt: bool, + macos_option_as_alt: configpkg.OptionAsAlt, pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig { var arena = ArenaAllocator.init(alloc_gpa); @@ -1042,9 +1042,11 @@ pub fn charCallback( // On macOS, we have to opt-in to using alt because option // by default is a unicode character sequence. if (comptime builtin.target.isDarwin()) { - if (!self.config.macos_option_as_alt) { - log.debug("macos_option_as_alt disabled, not sending esc prefix", .{}); - break :alt; + switch (self.config.macos_option_as_alt) { + .false => break :alt, + .true => {}, + .left => if (mods.sides.alt != .left) break :alt, + .right => if (mods.sides.alt != .right) break :alt, } } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 7c2f0514a..e9f2a403c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -394,8 +394,16 @@ pub const Surface = struct { // then we strip the alt modifier from the mods for translation. const translate_mods = translate_mods: { var translate_mods = mods; - if (self.app.config.@"macos-option-as-alt") - translate_mods.alt = false; + switch (self.app.config.@"macos-option-as-alt") { + .false => {}, + .true => translate_mods.alt = false, + .left => if (mods.sides.alt == .left) { + translate_mods.alt = false; + }, + .right => if (mods.sides.alt == .right) { + translate_mods.alt = false; + }, + } break :translate_mods translate_mods; }; diff --git a/src/config.zig b/src/config.zig index b1e847153..773017a9a 100644 --- a/src/config.zig +++ b/src/config.zig @@ -249,7 +249,7 @@ pub const Config = struct { /// (i.e. alt+ctrl+a). /// /// This does not work with GLFW builds. - @"macos-option-as-alt": bool = false, + @"macos-option-as-alt": OptionAsAlt = .false, /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, @@ -1036,6 +1036,14 @@ fn equal(comptime T: type, old: T, new: T) bool { } } +/// Valid values for macos-option-as-alt. +pub const OptionAsAlt = enum { + false, + true, + left, + right, +}; + /// Color represents a color using RGB. pub const Color = struct { r: u8,