Merge pull request #244 from mitchellh/alt-as-esc

Make Ghostty aware of left/right modifier keys
This commit is contained in:
Mitchell Hashimoto
2023-08-07 16:01:31 -07:00
committed by GitHub
10 changed files with 210 additions and 98 deletions

View File

@ -72,12 +72,16 @@ typedef int ghostty_input_scroll_mods_t;
typedef enum { typedef enum {
GHOSTTY_MODS_NONE = 0, GHOSTTY_MODS_NONE = 0,
GHOSTTY_MODS_SHIFT = 1 << 0, GHOSTTY_MODS_LEFT_SHIFT = 1 << 0,
GHOSTTY_MODS_CTRL = 1 << 1, GHOSTTY_MODS_RIGHT_SHIFT = 1 << 1,
GHOSTTY_MODS_ALT = 1 << 2, GHOSTTY_MODS_LEFT_CTRL = 1 << 2,
GHOSTTY_MODS_SUPER = 1 << 3, GHOSTTY_MODS_RIGHT_CTRL = 1 << 3,
GHOSTTY_MODS_CAPS = 1 << 4, GHOSTTY_MODS_LEFT_ALT = 1 << 4,
GHOSTTY_MODS_NUM = 1 << 5, 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; } ghostty_input_mods_e;
typedef enum { typedef enum {

View File

@ -491,10 +491,16 @@ extension Ghostty {
private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue 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 } let rawFlags = flags.rawValue
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue } if (rawFlags & UInt(NX_DEVICELSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_LEFT_SHIFT.rawValue }
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.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 } if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue }
return ghostty_input_mods_e(mods) return ghostty_input_mods_e(mods)

View File

@ -981,12 +981,12 @@ pub fn keyCallback(
// Handle non-printables // Handle non-printables
const char: u8 = char: { const char: u8 = char: {
const mods_int: u8 = @bitCast(mods); const mods_int: input.Mods.Int = @bitCast(mods);
const ctrl_only: u8 = @bitCast(input.Mods{ .ctrl = true }); const ctrl_only: input.Mods.Int = @bitCast(input.Mods{ .ctrl = .both });
// If we're only pressing control, check if this is a character // If we're only pressing control, check if this is a character
// we convert to a non-printable. // we convert to a non-printable.
if (mods_int == ctrl_only) { if (mods_int & ctrl_only > 0) {
const val: u8 = switch (key) { const val: u8 = switch (key) {
.left_bracket => 0x1B, .left_bracket => 0x1B,
.backslash => 0x1C, .backslash => 0x1C,
@ -1324,9 +1324,9 @@ fn mouseReport(
// X10 doesn't have modifiers // X10 doesn't have modifiers
if (self.io.terminal.modes.mouse_event != .x10) { if (self.io.terminal.modes.mouse_event != .x10) {
if (mods.shift) acc += 4; if (mods.shift.pressed()) acc += 4;
if (mods.super) acc += 8; if (mods.super.pressed()) acc += 8;
if (mods.ctrl) acc += 16; if (mods.ctrl.pressed()) acc += 16;
} }
// Motion adds another bit // Motion adds another bit
@ -1478,13 +1478,13 @@ pub fn mouseButtonCallback(
// Always record our latest mouse state // Always record our latest mouse state
self.mouse.click_state[@intCast(@intFromEnum(button))] = action; 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. // 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 // cursorPosCallback will also do a mouse report so we don't need to do any
// of the logic below. // of the logic below.
if (button == .left and action == .press) { 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 // Checking for selection requires the renderer state mutex which
// sucks but this should be pretty rare of an event so it won't // sucks but this should be pretty rare of an event so it won't
// cause a ton of contention. // cause a ton of contention.
@ -1508,7 +1508,7 @@ pub fn mouseButtonCallback(
// Report mouse events if enabled // Report mouse events if enabled
if (self.io.terminal.modes.mouse_event != .none) report: { if (self.io.terminal.modes.mouse_event != .none) report: {
// Shift overrides mouse "grabbing" in the window, taken from Kitty. // 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 // In any other mouse button scenario without shift pressed we
// clear the selection since the underlying application can handle // clear the selection since the underlying application can handle
@ -1634,7 +1634,7 @@ pub fn cursorPosCallback(
// Do a mouse report // Do a mouse report
if (self.io.terminal.modes.mouse_event != .none) report: { if (self.io.terminal.modes.mouse_event != .none) report: {
// Shift overrides mouse "grabbing" in the window, taken from Kitty. // 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 // We use the first mouse button we find pressed in order to report
// since the spec (afaict) does not say... // since the spec (afaict) does not say...

View File

@ -508,7 +508,7 @@ pub const CAPI = struct {
action, action,
key, key,
unmapped_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( surface.mouseButtonCallback(
action, action,
button, 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( surface.scrollCallback(
x, x,
y, y,
@bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(scroll_mods))))), @bitCast(@as(input.ScrollMods.Int, @truncate(@as(c_uint, @bitCast(scroll_mods))))),
); );
} }

View File

@ -564,7 +564,7 @@ pub const Surface = struct {
defer tracy.end(); defer tracy.end();
// Convert our glfw types into our input types // 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) { const action: input.Action = switch (glfw_action) {
.release => .release, .release => .release,
.press => .press, .press => .press,
@ -784,7 +784,7 @@ pub const Surface = struct {
const core_win = window.getUserPointer(CoreSurface) orelse return; const core_win = window.getUserPointer(CoreSurface) orelse return;
// Convert glfw button to input button // 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) { const button: input.MouseButton = switch (glfw_button) {
.left => .left, .left => .left,
.right => .right, .right => .right,
@ -806,4 +806,15 @@ pub const Surface = struct {
return; 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,
};
}
}; };

View File

@ -1230,10 +1230,10 @@ fn translateMouseButton(button: c.guint) input.MouseButton {
fn translateMods(state: c.GdkModifierType) input.Mods { fn translateMods(state: c.GdkModifierType) input.Mods {
var mods: input.Mods = .{}; var mods: input.Mods = .{};
if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true; if (state & c.GDK_SHIFT_MASK != 0) mods.shift = .both;
if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = true; if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = .both;
if (state & c.GDK_ALT_MASK != 0) mods.alt = true; if (state & c.GDK_ALT_MASK != 0) mods.alt = .both;
if (state & c.GDK_SUPER_MASK != 0) mods.super = true; if (state & c.GDK_SUPER_MASK != 0) mods.super = .both;
// Lock is dependent on the X settings but we just assume caps lock. // Lock is dependent on the X settings but we just assume caps lock.
if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true; if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true;

View File

@ -3,6 +3,7 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const apprt = @import("apprt.zig");
const inputpkg = @import("input.zig"); const inputpkg = @import("input.zig");
const terminal = @import("terminal/main.zig"); const terminal = @import("terminal/main.zig");
const internal_os = @import("os/main.zig"); const internal_os = @import("os/main.zig");
@ -300,7 +301,7 @@ pub const Config = struct {
// Add our default keybindings // Add our default keybindings
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .space, .mods = .{ .super = true, .alt = true, .ctrl = true } }, .{ .key = .space, .mods = .{ .super = .both, .alt = .both, .ctrl = .both } },
.{ .reload_config = {} }, .{ .reload_config = {} },
); );
@ -308,9 +309,9 @@ pub const Config = struct {
// On macOS we default to super but Linux ctrl+shift since // On macOS we default to super but Linux ctrl+shift since
// ctrl+c is to kill the process. // ctrl+c is to kill the process.
const mods: inputpkg.Mods = if (builtin.target.isDarwin()) const mods: inputpkg.Mods = if (builtin.target.isDarwin())
.{ .super = true } .{ .super = .both }
else else
.{ .ctrl = true, .shift = true }; .{ .ctrl = .both, .shift = .both };
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
@ -393,13 +394,13 @@ pub const Config = struct {
// Dev Mode // Dev Mode
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .down, .mods = .{ .shift = true, .super = true } }, .{ .key = .down, .mods = .{ .shift = .both, .super = .both } },
.{ .toggle_dev_mode = {} }, .{ .toggle_dev_mode = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .j, .mods = ctrlOrSuper(.{ .shift = true }) }, .{ .key = .j, .mods = ctrlOrSuper(.{ .shift = .both }) },
.{ .write_scrollback_file = {} }, .{ .write_scrollback_file = {} },
); );
@ -407,89 +408,89 @@ pub const Config = struct {
if (comptime !builtin.target.isDarwin()) { if (comptime !builtin.target.isDarwin()) {
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .n, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .n, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .new_window = {} }, .{ .new_window = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .w, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .w, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .close_surface = {} }, .{ .close_surface = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .q, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .q, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .quit = {} }, .{ .quit = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .f4, .mods = .{ .alt = true } }, .{ .key = .f4, .mods = .{ .alt = .both } },
.{ .close_window = {} }, .{ .close_window = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .t, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .t, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .new_tab = {} }, .{ .new_tab = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .left, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .left, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .previous_tab = {} }, .{ .previous_tab = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .right, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .right, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .next_tab = {} }, .{ .next_tab = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .o, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .o, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .new_split = .right }, .{ .new_split = .right },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .e, .mods = .{ .ctrl = true, .shift = true } }, .{ .key = .e, .mods = .{ .ctrl = .both, .shift = .both } },
.{ .new_split = .down }, .{ .new_split = .down },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .left_bracket, .mods = .{ .ctrl = true, .super = true } }, .{ .key = .left_bracket, .mods = .{ .ctrl = .both, .super = .both } },
.{ .goto_split = .previous }, .{ .goto_split = .previous },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .right_bracket, .mods = .{ .ctrl = true, .super = true } }, .{ .key = .right_bracket, .mods = .{ .ctrl = .both, .super = .both } },
.{ .goto_split = .next }, .{ .goto_split = .next },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .up, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .up, .mods = .{ .ctrl = .both, .alt = .both } },
.{ .goto_split = .top }, .{ .goto_split = .top },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .down, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .down, .mods = .{ .ctrl = .both, .alt = .both } },
.{ .goto_split = .bottom }, .{ .goto_split = .bottom },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .left, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .left, .mods = .{ .ctrl = .both, .alt = .both } },
.{ .goto_split = .left }, .{ .goto_split = .left },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .right, .mods = .{ .ctrl = true, .alt = true } }, .{ .key = .right, .mods = .{ .ctrl = .both, .alt = .both } },
.{ .goto_split = .right }, .{ .goto_split = .right },
); );
// Semantic prompts // Semantic prompts
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .page_up, .mods = .{ .shift = true } }, .{ .key = .page_up, .mods = .{ .shift = .both } },
.{ .jump_to_prompt = -1 }, .{ .jump_to_prompt = -1 },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .page_down, .mods = .{ .shift = true } }, .{ .key = .page_down, .mods = .{ .shift = .both } },
.{ .jump_to_prompt = 1 }, .{ .jump_to_prompt = 1 },
); );
} }
@ -502,9 +503,9 @@ pub const Config = struct {
// On macOS we default to super but everywhere else // On macOS we default to super but everywhere else
// is alt. // is alt.
const mods: inputpkg.Mods = if (builtin.target.isDarwin()) const mods: inputpkg.Mods = if (builtin.target.isDarwin())
.{ .super = true } .{ .super = .both }
else else
.{ .alt = true }; .{ .alt = .both };
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
@ -525,97 +526,97 @@ pub const Config = struct {
if (comptime builtin.target.isDarwin()) { if (comptime builtin.target.isDarwin()) {
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .q, .mods = .{ .super = true } }, .{ .key = .q, .mods = .{ .super = .both } },
.{ .quit = {} }, .{ .quit = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .k, .mods = .{ .super = true } }, .{ .key = .k, .mods = .{ .super = .both } },
.{ .clear_screen = {} }, .{ .clear_screen = {} },
); );
// Semantic prompts // Semantic prompts
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .up, .mods = .{ .super = true, .shift = true } }, .{ .key = .up, .mods = .{ .super = .both, .shift = .both } },
.{ .jump_to_prompt = -1 }, .{ .jump_to_prompt = -1 },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .down, .mods = .{ .super = true, .shift = true } }, .{ .key = .down, .mods = .{ .super = .both, .shift = .both } },
.{ .jump_to_prompt = 1 }, .{ .jump_to_prompt = 1 },
); );
// Mac windowing // Mac windowing
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .n, .mods = .{ .super = true } }, .{ .key = .n, .mods = .{ .super = .both } },
.{ .new_window = {} }, .{ .new_window = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .w, .mods = .{ .super = true } }, .{ .key = .w, .mods = .{ .super = .both } },
.{ .close_surface = {} }, .{ .close_surface = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .w, .mods = .{ .super = true, .shift = true } }, .{ .key = .w, .mods = .{ .super = .both, .shift = .both } },
.{ .close_window = {} }, .{ .close_window = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .t, .mods = .{ .super = true } }, .{ .key = .t, .mods = .{ .super = .both } },
.{ .new_tab = {} }, .{ .new_tab = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .left_bracket, .mods = .{ .super = true, .shift = true } }, .{ .key = .left_bracket, .mods = .{ .super = .both, .shift = .both } },
.{ .previous_tab = {} }, .{ .previous_tab = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .right_bracket, .mods = .{ .super = true, .shift = true } }, .{ .key = .right_bracket, .mods = .{ .super = .both, .shift = .both } },
.{ .next_tab = {} }, .{ .next_tab = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .d, .mods = .{ .super = true } }, .{ .key = .d, .mods = .{ .super = .both } },
.{ .new_split = .right }, .{ .new_split = .right },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .d, .mods = .{ .super = true, .shift = true } }, .{ .key = .d, .mods = .{ .super = .both, .shift = .both } },
.{ .new_split = .down }, .{ .new_split = .down },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .left_bracket, .mods = .{ .super = true } }, .{ .key = .left_bracket, .mods = .{ .super = .both } },
.{ .goto_split = .previous }, .{ .goto_split = .previous },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .right_bracket, .mods = .{ .super = true } }, .{ .key = .right_bracket, .mods = .{ .super = .both } },
.{ .goto_split = .next }, .{ .goto_split = .next },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .up, .mods = .{ .super = true, .alt = true } }, .{ .key = .up, .mods = .{ .super = .both, .alt = .both } },
.{ .goto_split = .top }, .{ .goto_split = .top },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .down, .mods = .{ .super = true, .alt = true } }, .{ .key = .down, .mods = .{ .super = .both, .alt = .both } },
.{ .goto_split = .bottom }, .{ .goto_split = .bottom },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .left, .mods = .{ .super = true, .alt = true } }, .{ .key = .left, .mods = .{ .super = .both, .alt = .both } },
.{ .goto_split = .left }, .{ .goto_split = .left },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .right, .mods = .{ .super = true, .alt = true } }, .{ .key = .right, .mods = .{ .super = .both, .alt = .both } },
.{ .goto_split = .right }, .{ .goto_split = .right },
); );
} }
@ -630,9 +631,9 @@ pub const Config = struct {
fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods { fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods {
var copy = mods; var copy = mods;
if (comptime builtin.target.isDarwin()) { if (comptime builtin.target.isDarwin()) {
copy.super = true; copy.super = .both;
} else { } else {
copy.ctrl = true; copy.ctrl = .both;
} }
return copy; return copy;
@ -1206,7 +1207,18 @@ pub const Keybinds = struct {
}; };
errdefer if (copy) |v| alloc.free(v); 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) { switch (binding.action) {
.unbind => self.set.remove(binding.trigger), .unbind => self.set.remove(binding.trigger),
else => try self.set.put(alloc, binding.trigger, binding.action), else => try self.set.put(alloc, binding.trigger, binding.action),

View File

@ -3,6 +3,7 @@
const Binding = @This(); const Binding = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const assert = std.debug.assert; const assert = std.debug.assert;
const key = @import("key.zig"); const key = @import("key.zig");
@ -42,12 +43,44 @@ pub fn parse(input: []const u8) !Binding {
// Check if its a modifier // Check if its a modifier
const modsInfo = @typeInfo(key.Mods).Struct; const modsInfo = @typeInfo(key.Mods).Struct;
inline for (modsInfo.fields) |field| { inline for (modsInfo.fields) |field| {
if (field.type == bool) { if (field.name[0] != '_') {
if (std.mem.eql(u8, part, field.name)) { if (std.mem.endsWith(u8, part, field.name)) {
// Repeat not allowed // Parse the directional modifier if it exists
if (@field(result.mods, field.name)) return Error.InvalidFormat; 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) = side;
},
else => @compileError("invalid type"),
}
@field(result.mods, field.name) = true;
continue :loop; continue :loop;
} }
} }
@ -336,23 +369,32 @@ test "parse: triggers" {
// single modifier // single modifier
try testing.expectEqual(Binding{ try testing.expectEqual(Binding{
.trigger = .{ .trigger = .{
.mods = .{ .shift = true }, .mods = .{ .shift = .both },
.key = .a, .key = .a,
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
}, try parse("shift+a=ignore")); }, try parse("shift+a=ignore"));
try testing.expectEqual(Binding{ try testing.expectEqual(Binding{
.trigger = .{ .trigger = .{
.mods = .{ .ctrl = true }, .mods = .{ .ctrl = .both },
.key = .a, .key = .a,
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
}, try parse("ctrl+a=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 // multiple modifier
try testing.expectEqual(Binding{ try testing.expectEqual(Binding{
.trigger = .{ .trigger = .{
.mods = .{ .shift = true, .ctrl = true }, .mods = .{ .shift = .both, .ctrl = .both },
.key = .a, .key = .a,
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
@ -361,7 +403,7 @@ test "parse: triggers" {
// key can come before modifier // key can come before modifier
try testing.expectEqual(Binding{ try testing.expectEqual(Binding{
.trigger = .{ .trigger = .{
.mods = .{ .shift = true }, .mods = .{ .shift = .both },
.key = .a, .key = .a,
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
@ -370,7 +412,7 @@ test "parse: triggers" {
// unmapped keys // unmapped keys
try testing.expectEqual(Binding{ try testing.expectEqual(Binding{
.trigger = .{ .trigger = .{
.mods = .{ .shift = true }, .mods = .{ .shift = .both },
.key = .a, .key = .a,
.unmapped = true, .unmapped = true,
}, },
@ -383,6 +425,9 @@ test "parse: triggers" {
// repeated control // repeated control
try testing.expectError(Error.InvalidFormat, parse("shift+shift+a=ignore")); 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 // multiple character
try testing.expectError(Error.InvalidFormat, parse("a+b=ignore")); try testing.expectError(Error.InvalidFormat, parse("a+b=ignore"));
} }

View File

@ -5,22 +5,54 @@ const Allocator = std.mem.Allocator;
/// GLFW representation, but we use this generically. /// GLFW representation, but we use this generically.
/// ///
/// IMPORTANT: Any changes here update include/ghostty.h /// IMPORTANT: Any changes here update include/ghostty.h
pub const Mods = packed struct(u8) { pub const Mods = packed struct(Mods.Int) {
shift: bool = false, pub const Int = u10;
ctrl: bool = false,
alt: bool = false, shift: Side = .none,
super: bool = false, ctrl: Side = .none,
alt: Side = .none,
super: Side = .none,
caps_lock: bool = false, caps_lock: bool = false,
num_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;
}
};
/// 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 // For our own understanding
test { test {
const testing = std.testing; 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( try testing.expectEqual(
@as(u8, @bitCast(Mods{ .shift = true })), @as(Int, @bitCast(Mods{ .shift = .left })),
@as(u8, 0b0000_0001), @as(Int, 0b0000_0001),
); );
} }
}; };
@ -99,7 +131,7 @@ pub const Key = enum(c_int) {
equal, equal,
left_bracket, // [ left_bracket, // [
right_bracket, // ] right_bracket, // ]
backslash, // / backslash, // \
// control // control
up, up,

View File

@ -64,7 +64,9 @@ pub const MouseMomentum = enum(u3) {
}; };
/// The bitmask for mods for scroll events. /// 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 /// True if this is a high-precision scroll event. For example, Apple
/// devices such as Magic Mouse, trackpads, etc. are high-precision /// devices such as Magic Mouse, trackpads, etc. are high-precision
/// and send very detailed scroll events. /// and send very detailed scroll events.
@ -79,10 +81,10 @@ pub const ScrollMods = packed struct(u8) {
// For our own understanding // For our own understanding
test { test {
const testing = std.testing; 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( try testing.expectEqual(
@as(u8, @bitCast(ScrollMods{ .precision = true })), @as(Int, @bitCast(ScrollMods{ .precision = true })),
@as(u8, 0b0000_0001), @as(Int, 0b0000_0001),
); );
} }
}; };