From 67cbabd605b064100c1588d0ec63fafa1764186c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Aug 2023 14:33:56 -0700 Subject: [PATCH 1/5] make keyboard modifiers left/right-aware throughout core --- include/ghostty.h | 16 +++-- macos/Sources/Ghostty/SurfaceView.swift | 14 ++-- src/Surface.zig | 20 +++--- src/apprt/embedded.zig | 6 +- src/apprt/glfw.zig | 15 ++++- src/config.zig | 88 ++++++++++++------------- src/input/Binding.zig | 30 ++++++--- src/input/key.zig | 37 ++++++++--- src/input/mouse.zig | 10 +-- 9 files changed, 145 insertions(+), 91 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 5b75ea1ad..1c7d18d37 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -72,12 +72,16 @@ typedef int ghostty_input_scroll_mods_t; typedef enum { GHOSTTY_MODS_NONE = 0, - GHOSTTY_MODS_SHIFT = 1 << 0, - GHOSTTY_MODS_CTRL = 1 << 1, - GHOSTTY_MODS_ALT = 1 << 2, - GHOSTTY_MODS_SUPER = 1 << 3, - GHOSTTY_MODS_CAPS = 1 << 4, - GHOSTTY_MODS_NUM = 1 << 5, + GHOSTTY_MODS_LEFT_SHIFT = 1 << 0, + GHOSTTY_MODS_RIGHT_SHIFT = 1 << 1, + GHOSTTY_MODS_LEFT_CTRL = 1 << 2, + GHOSTTY_MODS_RIGHT_CTRL = 1 << 3, + GHOSTTY_MODS_LEFT_ALT = 1 << 4, + GHOSTTY_MODS_RIGHT_ALT = 1 << 5, + GHOSTTY_MODS_LEFT_SUPER = 1 << 6, + GHOSTTY_MODS_RIGHT_SUPER = 1 << 7, + GHOSTTY_MODS_CAPS = 1 << 8, + GHOSTTY_MODS_NUM = 1 << 9, } ghostty_input_mods_e; typedef enum { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 2bb9c5e8d..03e5f333f 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -491,10 +491,16 @@ 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 } + + let rawFlags = flags.rawValue + if (rawFlags & UInt(NX_DEVICELSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_LEFT_SHIFT.rawValue } + if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_RIGHT_SHIFT.rawValue } + if (rawFlags & UInt(NX_DEVICELCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_LEFT_CTRL.rawValue } + if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_RIGHT_CTRL.rawValue } + if (rawFlags & UInt(NX_DEVICELALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_LEFT_ALT.rawValue } + if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_RIGHT_ALT.rawValue } + if (rawFlags & UInt(NX_DEVICELCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_LEFT_SUPER.rawValue } + if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_RIGHT_SUPER.rawValue } if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } return ghostty_input_mods_e(mods) diff --git a/src/Surface.zig b/src/Surface.zig index bf8bd3649..25cfdfec8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -981,12 +981,12 @@ pub fn keyCallback( // Handle non-printables const char: u8 = char: { - const mods_int: u8 = @bitCast(mods); - const ctrl_only: u8 = @bitCast(input.Mods{ .ctrl = true }); + const mods_int: input.Mods.Int = @bitCast(mods); + const ctrl_only: input.Mods.Int = @bitCast(input.Mods{ .ctrl = .both }); // If we're only pressing control, check if this is a character // we convert to a non-printable. - if (mods_int == ctrl_only) { + if (mods_int & ctrl_only > 0) { const val: u8 = switch (key) { .left_bracket => 0x1B, .backslash => 0x1C, @@ -1324,9 +1324,9 @@ fn mouseReport( // X10 doesn't have modifiers if (self.io.terminal.modes.mouse_event != .x10) { - if (mods.shift) acc += 4; - if (mods.super) acc += 8; - if (mods.ctrl) acc += 16; + if (mods.shift.pressed()) acc += 4; + if (mods.super.pressed()) acc += 8; + if (mods.ctrl.pressed()) acc += 16; } // Motion adds another bit @@ -1478,13 +1478,13 @@ pub fn mouseButtonCallback( // Always record our latest mouse state self.mouse.click_state[@intCast(@intFromEnum(button))] = action; - self.mouse.mods = @bitCast(mods); + self.mouse.mods = mods; // Shift-click continues the previous mouse state if we have a selection. // cursorPosCallback will also do a mouse report so we don't need to do any // of the logic below. if (button == .left and action == .press) { - if (mods.shift and self.mouse.left_click_count > 0) { + if (mods.shift.pressed() and self.mouse.left_click_count > 0) { // Checking for selection requires the renderer state mutex which // sucks but this should be pretty rare of an event so it won't // cause a ton of contention. @@ -1508,7 +1508,7 @@ pub fn mouseButtonCallback( // Report mouse events if enabled if (self.io.terminal.modes.mouse_event != .none) report: { // Shift overrides mouse "grabbing" in the window, taken from Kitty. - if (mods.shift) break :report; + if (mods.shift.pressed()) break :report; // In any other mouse button scenario without shift pressed we // clear the selection since the underlying application can handle @@ -1634,7 +1634,7 @@ pub fn cursorPosCallback( // Do a mouse report if (self.io.terminal.modes.mouse_event != .none) report: { // Shift overrides mouse "grabbing" in the window, taken from Kitty. - if (self.mouse.mods.shift) break :report; + if (self.mouse.mods.shift.pressed()) break :report; // We use the first mouse button we find pressed in order to report // since the spec (afaict) does not say... diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index e793360c6..53f6b2d76 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -508,7 +508,7 @@ pub const CAPI = struct { action, key, unmapped_key, - @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(mods))))), + @bitCast(@as(input.Mods.Int, @truncate(@as(c_uint, @bitCast(mods))))), ); } @@ -527,7 +527,7 @@ pub const CAPI = struct { surface.mouseButtonCallback( action, button, - @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(mods))))), + @bitCast(@as(input.Mods.Int, @truncate(@as(c_uint, @bitCast(mods))))), ); } @@ -545,7 +545,7 @@ pub const CAPI = struct { surface.scrollCallback( x, y, - @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(scroll_mods))))), + @bitCast(@as(input.ScrollMods.Int, @truncate(@as(c_uint, @bitCast(scroll_mods))))), ); } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 4863e5511..c3cb6d683 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -564,7 +564,7 @@ pub const Surface = struct { defer tracy.end(); // Convert our glfw types into our input types - const mods: input.Mods = @bitCast(glfw_mods); + const mods = convertMods(glfw_mods); const action: input.Action = switch (glfw_action) { .release => .release, .press => .press, @@ -784,7 +784,7 @@ 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 = convertMods(glfw_mods); const button: input.MouseButton = switch (glfw_button) { .left => .left, .right => .right, @@ -806,4 +806,15 @@ pub const Surface = struct { return; }; } + + fn convertMods(mods: glfw.Mods) input.Mods { + return .{ + .shift = if (mods.shift) .both else .none, + .ctrl = if (mods.control) .both else .none, + .alt = if (mods.alt) .both else .none, + .super = if (mods.super) .both else .none, + .caps_lock = mods.caps_lock, + .num_lock = mods.num_lock, + }; + } }; diff --git a/src/config.zig b/src/config.zig index e812cbec8..961a67252 100644 --- a/src/config.zig +++ b/src/config.zig @@ -300,7 +300,7 @@ pub const Config = struct { // Add our default keybindings try result.keybind.set.put( alloc, - .{ .key = .space, .mods = .{ .super = true, .alt = true, .ctrl = true } }, + .{ .key = .space, .mods = .{ .super = .both, .alt = .both, .ctrl = .both } }, .{ .reload_config = {} }, ); @@ -308,9 +308,9 @@ pub const Config = struct { // On macOS we default to super but Linux ctrl+shift since // ctrl+c is to kill the process. const mods: inputpkg.Mods = if (builtin.target.isDarwin()) - .{ .super = true } + .{ .super = .both } else - .{ .ctrl = true, .shift = true }; + .{ .ctrl = .both, .shift = .both }; try result.keybind.set.put( alloc, @@ -393,13 +393,13 @@ pub const Config = struct { // Dev Mode try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .shift = true, .super = true } }, + .{ .key = .down, .mods = .{ .shift = .both, .super = .both } }, .{ .toggle_dev_mode = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .j, .mods = ctrlOrSuper(.{ .shift = true }) }, + .{ .key = .j, .mods = ctrlOrSuper(.{ .shift = .both }) }, .{ .write_scrollback_file = {} }, ); @@ -407,89 +407,89 @@ pub const Config = struct { if (comptime !builtin.target.isDarwin()) { try result.keybind.set.put( alloc, - .{ .key = .n, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .n, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .new_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .w, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .w, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .close_surface = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .q, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .q, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .quit = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .f4, .mods = .{ .alt = true } }, + .{ .key = .f4, .mods = .{ .alt = .both } }, .{ .close_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .t, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .t, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .new_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .left, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .previous_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .right, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .next_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .o, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .o, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .new_split = .right }, ); try result.keybind.set.put( alloc, - .{ .key = .e, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .e, .mods = .{ .ctrl = .both, .shift = .both } }, .{ .new_split = .down }, ); try result.keybind.set.put( alloc, - .{ .key = .left_bracket, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .left_bracket, .mods = .{ .ctrl = .both, .super = .both } }, .{ .goto_split = .previous }, ); try result.keybind.set.put( alloc, - .{ .key = .right_bracket, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .right_bracket, .mods = .{ .ctrl = .both, .super = .both } }, .{ .goto_split = .next }, ); try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .up, .mods = .{ .ctrl = .both, .alt = .both } }, .{ .goto_split = .top }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .down, .mods = .{ .ctrl = .both, .alt = .both } }, .{ .goto_split = .bottom }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .left, .mods = .{ .ctrl = .both, .alt = .both } }, .{ .goto_split = .left }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .right, .mods = .{ .ctrl = .both, .alt = .both } }, .{ .goto_split = .right }, ); // Semantic prompts try result.keybind.set.put( alloc, - .{ .key = .page_up, .mods = .{ .shift = true } }, + .{ .key = .page_up, .mods = .{ .shift = .both } }, .{ .jump_to_prompt = -1 }, ); try result.keybind.set.put( alloc, - .{ .key = .page_down, .mods = .{ .shift = true } }, + .{ .key = .page_down, .mods = .{ .shift = .both } }, .{ .jump_to_prompt = 1 }, ); } @@ -502,9 +502,9 @@ pub const Config = struct { // On macOS we default to super but everywhere else // is alt. const mods: inputpkg.Mods = if (builtin.target.isDarwin()) - .{ .super = true } + .{ .super = .both } else - .{ .alt = true }; + .{ .alt = .both }; try result.keybind.set.put( alloc, @@ -525,97 +525,97 @@ pub const Config = struct { if (comptime builtin.target.isDarwin()) { try result.keybind.set.put( alloc, - .{ .key = .q, .mods = .{ .super = true } }, + .{ .key = .q, .mods = .{ .super = .both } }, .{ .quit = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .k, .mods = .{ .super = true } }, + .{ .key = .k, .mods = .{ .super = .both } }, .{ .clear_screen = {} }, ); // Semantic prompts try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .super = true, .shift = true } }, + .{ .key = .up, .mods = .{ .super = .both, .shift = .both } }, .{ .jump_to_prompt = -1 }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .super = true, .shift = true } }, + .{ .key = .down, .mods = .{ .super = .both, .shift = .both } }, .{ .jump_to_prompt = 1 }, ); // Mac windowing try result.keybind.set.put( alloc, - .{ .key = .n, .mods = .{ .super = true } }, + .{ .key = .n, .mods = .{ .super = .both } }, .{ .new_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .w, .mods = .{ .super = true } }, + .{ .key = .w, .mods = .{ .super = .both } }, .{ .close_surface = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .w, .mods = .{ .super = true, .shift = true } }, + .{ .key = .w, .mods = .{ .super = .both, .shift = .both } }, .{ .close_window = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .t, .mods = .{ .super = true } }, + .{ .key = .t, .mods = .{ .super = .both } }, .{ .new_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .left_bracket, .mods = .{ .super = true, .shift = true } }, + .{ .key = .left_bracket, .mods = .{ .super = .both, .shift = .both } }, .{ .previous_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .right_bracket, .mods = .{ .super = true, .shift = true } }, + .{ .key = .right_bracket, .mods = .{ .super = .both, .shift = .both } }, .{ .next_tab = {} }, ); try result.keybind.set.put( alloc, - .{ .key = .d, .mods = .{ .super = true } }, + .{ .key = .d, .mods = .{ .super = .both } }, .{ .new_split = .right }, ); try result.keybind.set.put( alloc, - .{ .key = .d, .mods = .{ .super = true, .shift = true } }, + .{ .key = .d, .mods = .{ .super = .both, .shift = .both } }, .{ .new_split = .down }, ); try result.keybind.set.put( alloc, - .{ .key = .left_bracket, .mods = .{ .super = true } }, + .{ .key = .left_bracket, .mods = .{ .super = .both } }, .{ .goto_split = .previous }, ); try result.keybind.set.put( alloc, - .{ .key = .right_bracket, .mods = .{ .super = true } }, + .{ .key = .right_bracket, .mods = .{ .super = .both } }, .{ .goto_split = .next }, ); try result.keybind.set.put( alloc, - .{ .key = .up, .mods = .{ .super = true, .alt = true } }, + .{ .key = .up, .mods = .{ .super = .both, .alt = .both } }, .{ .goto_split = .top }, ); try result.keybind.set.put( alloc, - .{ .key = .down, .mods = .{ .super = true, .alt = true } }, + .{ .key = .down, .mods = .{ .super = .both, .alt = .both } }, .{ .goto_split = .bottom }, ); try result.keybind.set.put( alloc, - .{ .key = .left, .mods = .{ .super = true, .alt = true } }, + .{ .key = .left, .mods = .{ .super = .both, .alt = .both } }, .{ .goto_split = .left }, ); try result.keybind.set.put( alloc, - .{ .key = .right, .mods = .{ .super = true, .alt = true } }, + .{ .key = .right, .mods = .{ .super = .both, .alt = .both } }, .{ .goto_split = .right }, ); } @@ -630,9 +630,9 @@ pub const Config = struct { fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods { var copy = mods; if (comptime builtin.target.isDarwin()) { - copy.super = true; + copy.super = .both; } else { - copy.ctrl = true; + copy.ctrl = .both; } return copy; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 4dec02d6f..2484ee075 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -42,12 +42,24 @@ pub fn parse(input: []const u8) !Binding { // Check if its a modifier const modsInfo = @typeInfo(key.Mods).Struct; inline for (modsInfo.fields) |field| { - if (field.type == bool) { + if (field.name[0] != '_') { if (std.mem.eql(u8, part, field.name)) { - // Repeat not allowed - if (@field(result.mods, field.name)) return Error.InvalidFormat; + switch (field.type) { + bool => { + if (@field(result.mods, field.name)) + return Error.InvalidFormat; + @field(result.mods, field.name) = true; + }, + + key.Mods.Side => { + if (@field(result.mods, field.name).pressed()) + return Error.InvalidFormat; + @field(result.mods, field.name) = .both; + }, + + else => @compileError("invalid type"), + } - @field(result.mods, field.name) = true; continue :loop; } } @@ -336,14 +348,14 @@ test "parse: triggers" { // single modifier try testing.expectEqual(Binding{ .trigger = .{ - .mods = .{ .shift = true }, + .mods = .{ .shift = .both }, .key = .a, }, .action = .{ .ignore = {} }, }, try parse("shift+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ - .mods = .{ .ctrl = true }, + .mods = .{ .ctrl = .both }, .key = .a, }, .action = .{ .ignore = {} }, @@ -352,7 +364,7 @@ test "parse: triggers" { // multiple modifier try testing.expectEqual(Binding{ .trigger = .{ - .mods = .{ .shift = true, .ctrl = true }, + .mods = .{ .shift = .both, .ctrl = .both }, .key = .a, }, .action = .{ .ignore = {} }, @@ -361,7 +373,7 @@ test "parse: triggers" { // key can come before modifier try testing.expectEqual(Binding{ .trigger = .{ - .mods = .{ .shift = true }, + .mods = .{ .shift = .both }, .key = .a, }, .action = .{ .ignore = {} }, @@ -370,7 +382,7 @@ test "parse: triggers" { // unmapped keys try testing.expectEqual(Binding{ .trigger = .{ - .mods = .{ .shift = true }, + .mods = .{ .shift = .both }, .key = .a, .unmapped = true, }, diff --git a/src/input/key.zig b/src/input/key.zig index f7a8a244a..5e91f8bcf 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -5,22 +5,41 @@ 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) { - shift: bool = false, - ctrl: bool = false, - alt: bool = false, - super: bool = false, +pub const Mods = packed struct(Mods.Int) { + pub const Int = u10; + + shift: Side = .none, + ctrl: Side = .none, + alt: Side = .none, + super: Side = .none, caps_lock: bool = false, num_lock: bool = false, - _padding: u2 = 0, + + /// Keeps track of left/right press. A packed struct makes it easy + /// to set as a bitmask and then check the individual values. + pub const Side = enum(u2) { + none = 0, + left = 1, + right = 2, + + /// Note that while this should only be set for BOTH being set, + /// this is semantically used to mean "any" for the purposes of + /// keybindings. We do not allow keybindings to map to "both". + both = 3, + + /// Returns true if the key is pressed at all. + pub fn pressed(self: Side) bool { + return @intFromEnum(self) != 0; + } + }; // For our own understanding test { const testing = std.testing; - try testing.expectEqual(@as(u8, @bitCast(Mods{})), @as(u8, 0b0)); + try testing.expectEqual(@as(Int, @bitCast(Mods{})), @as(Int, 0b0)); try testing.expectEqual( - @as(u8, @bitCast(Mods{ .shift = true })), - @as(u8, 0b0000_0001), + @as(Int, @bitCast(Mods{ .shift = .left })), + @as(Int, 0b0000_0001), ); } }; diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 2233c4494..df4fe8db3 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -64,7 +64,9 @@ pub const MouseMomentum = enum(u3) { }; /// The bitmask for mods for scroll events. -pub const ScrollMods = packed struct(u8) { +pub const ScrollMods = packed struct(ScrollMods.Int) { + pub const Int = u8; + /// True if this is a high-precision scroll event. For example, Apple /// devices such as Magic Mouse, trackpads, etc. are high-precision /// and send very detailed scroll events. @@ -79,10 +81,10 @@ pub const ScrollMods = packed struct(u8) { // For our own understanding test { const testing = std.testing; - try testing.expectEqual(@as(u8, @bitCast(ScrollMods{})), @as(u8, 0b0)); + try testing.expectEqual(@as(Int, @bitCast(ScrollMods{})), @as(Int, 0b0)); try testing.expectEqual( - @as(u8, @bitCast(ScrollMods{ .precision = true })), - @as(u8, 0b0000_0001), + @as(Int, @bitCast(ScrollMods{ .precision = true })), + @as(Int, 0b0000_0001), ); } }; From eca5955e6568dd7144881617bd90d5415f1de895 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Aug 2023 14:35:52 -0700 Subject: [PATCH 2/5] apprt/gtk: new input mods format --- src/apprt/gtk.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 2871e7f06..4c3471979 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1230,10 +1230,10 @@ fn translateMouseButton(button: c.guint) input.MouseButton { fn translateMods(state: c.GdkModifierType) input.Mods { var mods: input.Mods = .{}; - if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true; - if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = true; - if (state & c.GDK_ALT_MASK != 0) mods.alt = true; - if (state & c.GDK_SUPER_MASK != 0) mods.super = true; + if (state & c.GDK_SHIFT_MASK != 0) mods.shift = .both; + if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = .both; + if (state & c.GDK_ALT_MASK != 0) mods.alt = .both; + if (state & c.GDK_SUPER_MASK != 0) mods.super = .both; // Lock is dependent on the X settings but we just assume caps lock. if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true; From 47bed511773853b691034762291e993382b22a4b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Aug 2023 14:43:44 -0700 Subject: [PATCH 3/5] bindings can contain directional modifiers: left_shift+a --- src/input/Binding.zig | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 2484ee075..ca51a72a2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -43,18 +43,38 @@ pub fn parse(input: []const u8) !Binding { const modsInfo = @typeInfo(key.Mods).Struct; inline for (modsInfo.fields) |field| { if (field.name[0] != '_') { - if (std.mem.eql(u8, part, field.name)) { + if (std.mem.endsWith(u8, part, field.name)) { + // Parse the directional modifier if it exists + const side: key.Mods.Side = side: { + if (std.mem.eql(u8, part, field.name)) + break :side .both; + if (std.mem.eql(u8, part, "left_" ++ field.name)) + break :side .left; + if (std.mem.eql(u8, part, "right_" ++ field.name)) + break :side .right; + + return Error.InvalidFormat; + }; + switch (field.type) { bool => { + // Can only be set once if (@field(result.mods, field.name)) return Error.InvalidFormat; + + // Can not be directional + if (side != .both) + return Error.InvalidFormat; + @field(result.mods, field.name) = true; }, key.Mods.Side => { + // Can only be set once if (@field(result.mods, field.name).pressed()) return Error.InvalidFormat; - @field(result.mods, field.name) = .both; + + @field(result.mods, field.name) = side; }, else => @compileError("invalid type"), @@ -361,6 +381,15 @@ test "parse: triggers" { .action = .{ .ignore = {} }, }, try parse("ctrl+a=ignore")); + // directional modifier + try testing.expectEqual(Binding{ + .trigger = .{ + .mods = .{ .shift = .left }, + .key = .a, + }, + .action = .{ .ignore = {} }, + }, try parse("left_shift+a=ignore")); + // multiple modifier try testing.expectEqual(Binding{ .trigger = .{ @@ -395,6 +424,9 @@ test "parse: triggers" { // repeated control try testing.expectError(Error.InvalidFormat, parse("shift+shift+a=ignore")); + // conflicting sides + try testing.expectError(Error.InvalidFormat, parse("left_shift+right_shift+a=ignore")); + // multiple character try testing.expectError(Error.InvalidFormat, parse("a+b=ignore")); } From 32eb226fa3de2d3909872439005fbb6f9cbea864 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Aug 2023 14:52:20 -0700 Subject: [PATCH 4/5] non-macos doesn't support directional bindings --- src/config.zig | 14 +++++++++++++- src/input/Binding.zig | 1 + src/input/key.zig | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/config.zig b/src/config.zig index 961a67252..ef56f3d42 100644 --- a/src/config.zig +++ b/src/config.zig @@ -3,6 +3,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; +const apprt = @import("apprt.zig"); const inputpkg = @import("input.zig"); const terminal = @import("terminal/main.zig"); const internal_os = @import("os/main.zig"); @@ -1206,7 +1207,18 @@ pub const Keybinds = struct { }; errdefer if (copy) |v| alloc.free(v); - const binding = try inputpkg.Binding.parse(value); + const binding = binding: { + var binding = try inputpkg.Binding.parse(value); + + // Unless we're on native macOS, we don't allow directional + // keys, so we just remap them to "both". + if (comptime !(builtin.target.isDarwin() and apprt.runtime == apprt.embedded)) { + binding.trigger.mods = binding.trigger.mods.removeDirection(); + } + + break :binding binding; + }; + switch (binding.action) { .unbind => self.set.remove(binding.trigger), else => try self.set.put(alloc, binding.trigger, binding.action), diff --git a/src/input/Binding.zig b/src/input/Binding.zig index ca51a72a2..c4090b804 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -3,6 +3,7 @@ const Binding = @This(); const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const key = @import("key.zig"); diff --git a/src/input/key.zig b/src/input/key.zig index 5e91f8bcf..6e0c1c234 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -33,6 +33,19 @@ pub const Mods = packed struct(Mods.Int) { } }; + /// Return the identical mods but with all directional configuration + /// removed and all of it set to "both". + pub fn removeDirection(self: Mods) Mods { + return Mods{ + .shift = if (self.shift.pressed()) .both else .none, + .ctrl = if (self.ctrl.pressed()) .both else .none, + .alt = if (self.alt.pressed()) .both else .none, + .super = if (self.super.pressed()) .both else .none, + .caps_lock = self.caps_lock, + .num_lock = self.num_lock, + }; + } + // For our own understanding test { const testing = std.testing; From 274f934e88c55df6e655f330fdcf5477fdbce5c6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Aug 2023 15:39:08 -0700 Subject: [PATCH 5/5] key: fix wrong comment --- src/input/key.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/key.zig b/src/input/key.zig index 6e0c1c234..681b934fc 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -131,7 +131,7 @@ pub const Key = enum(c_int) { equal, left_bracket, // [ right_bracket, // ] - backslash, // / + backslash, // \ // control up,