Merge pull request #285 from mitchellh/left-right-alt

macos: option-as-alt can specify left or right option key
This commit is contained in:
Mitchell Hashimoto
2023-08-14 13:20:46 -07:00
committed by GitHub
8 changed files with 129 additions and 33 deletions

View File

@ -83,6 +83,10 @@ typedef enum {
GHOSTTY_MODS_SUPER = 1 << 3, GHOSTTY_MODS_SUPER = 1 << 3,
GHOSTTY_MODS_CAPS = 1 << 4, GHOSTTY_MODS_CAPS = 1 << 4,
GHOSTTY_MODS_NUM = 1 << 5, 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; } ghostty_input_mods_e;
typedef enum { typedef enum {

View File

@ -480,12 +480,21 @@ 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(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue } if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue }
if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue } if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue }
if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue } if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue }
if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.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) return ghostty_input_mods_e(mods)
} }

View File

@ -142,7 +142,7 @@ const DerivedConfig = struct {
confirm_close_surface: bool, confirm_close_surface: bool,
mouse_interval: u64, mouse_interval: u64,
macos_non_native_fullscreen: bool, 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 { pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
var arena = ArenaAllocator.init(alloc_gpa); 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 // On macOS, we have to opt-in to using alt because option
// by default is a unicode character sequence. // by default is a unicode character sequence.
if (comptime builtin.target.isDarwin()) { if (comptime builtin.target.isDarwin()) {
if (!self.config.macos_option_as_alt) { switch (self.config.macos_option_as_alt) {
log.debug("macos_option_as_alt disabled, not sending esc prefix", .{}); .false => break :alt,
break :alt; .true => {},
.left => if (mods.sides.alt != .left) break :alt,
.right => if (mods.sides.alt != .right) break :alt,
} }
} }
@ -1094,12 +1096,7 @@ pub fn keyCallback(
if (action != .press and action != .repeat) return false; if (action != .press and action != .repeat) return false;
// Mods for bindings never include caps/num lock. // Mods for bindings never include caps/num lock.
const binding_mods = mods: { const binding_mods = mods.binding();
var binding_mods = mods;
binding_mods.caps_lock = false;
binding_mods.num_lock = false;
break :mods binding_mods;
};
// Check if we're processing a binding first. If so, that negates // Check if we're processing a binding first. If so, that negates
// any further key processing. // any further key processing.
@ -1154,8 +1151,8 @@ pub fn keyCallback(
.set_other => if (!modify_other_keys) continue, .set_other => if (!modify_other_keys) continue,
} }
const mods_int: u8 = @bitCast(binding_mods); const mods_int = binding_mods.int();
const entry_mods_int: u8 = @bitCast(entry.mods); const entry_mods_int = entry.mods.int();
if (entry_mods_int == 0) { if (entry_mods_int == 0) {
if (mods_int != 0 and !entry.mods_empty_is_any) continue; if (mods_int != 0 and !entry.mods_empty_is_any) continue;
// mods are either empty, or empty means any so we allow it. // mods are either empty, or empty means any so we allow it.
@ -1191,8 +1188,8 @@ pub fn keyCallback(
// Handle non-printables // Handle non-printables
const char: u8 = char: { const char: u8 = char: {
const mods_int: u8 = @bitCast(unalt_mods); const mods_int = unalt_mods.int();
const ctrl_only: u8 = @bitCast(input.Mods{ .ctrl = true }); const ctrl_only = (input.Mods{ .ctrl = true }).int();
// 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. The best table I've found for // we convert to a non-printable. The best table I've found for

View File

@ -394,8 +394,16 @@ pub const Surface = struct {
// then we strip the alt modifier from the mods for translation. // then we strip the alt modifier from the mods for translation.
const translate_mods = translate_mods: { const translate_mods = translate_mods: {
var translate_mods = mods; var translate_mods = mods;
if (self.app.config.@"macos-option-as-alt") switch (self.app.config.@"macos-option-as-alt") {
translate_mods.alt = false; .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; break :translate_mods translate_mods;
}; };
@ -660,7 +668,10 @@ pub const CAPI = struct {
surface.keyCallback( surface.keyCallback(
action, action,
keycode, 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| { ) catch |err| {
log.err("error processing key event err={}", .{err}); log.err("error processing key event err={}", .{err});
return; return;
@ -684,7 +695,10 @@ 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.Backing,
@truncate(@as(c_uint, @bitCast(mods))),
)),
); );
} }

View File

@ -618,7 +618,12 @@ pub const Surface = struct {
const core_win = window.getUserPointer(CoreSurface) orelse return; const core_win = window.getUserPointer(CoreSurface) orelse return;
// 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: 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) { const action: input.Action = switch (glfw_action) {
.release => .release, .release => .release,
.press => .press, .press => .press,
@ -843,7 +848,12 @@ 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: 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) { const button: input.MouseButton = switch (glfw_button) {
.left => .left, .left => .left,
.right => .right, .right => .right,

View File

@ -249,7 +249,7 @@ pub const Config = struct {
/// (i.e. alt+ctrl+a). /// (i.e. alt+ctrl+a).
/// ///
/// This does not work with GLFW builds. /// 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. /// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null, _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. /// Color represents a color using RGB.
pub const Color = struct { pub const Color = struct {
r: u8, r: u8,

View File

@ -293,10 +293,10 @@ pub const Trigger = struct {
physical: bool = false, physical: bool = false,
/// Returns a hash code that can be used to uniquely identify this trigger. /// 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); var hasher = std.hash.Wyhash.init(0);
std.hash.autoHash(&hasher, self.key); 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); std.hash.autoHash(&hasher, self.physical);
return hasher.final(); 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 /// The use case is that this will be called on EVERY key input to look
/// for an associated action so it must be fast. /// for an associated action so it must be fast.
pub const Set = struct { 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. /// The set of bindings.
bindings: HashMap = .{}, bindings: HashMap = .{},
@ -318,10 +323,14 @@ pub const Set = struct {
/// Add a binding to the set. If the binding already exists then /// Add a binding to the set. If the binding already exists then
/// this will overwrite it. /// 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 // unbind should never go into the set, it should be handled prior
assert(action != .unbind); assert(action != .unbind);
try self.bindings.put(alloc, t, action); try self.bindings.put(alloc, t, action);
} }
@ -334,6 +343,19 @@ pub const Set = struct {
pub fn remove(self: *Set, t: Trigger) void { pub fn remove(self: *Set, t: Trigger) void {
_ = self.bindings.remove(t); _ = 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" { test "parse: triggers" {

View File

@ -5,23 +5,55 @@ 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.Backing) {
pub const Backing = u16;
shift: bool = false, shift: bool = false,
ctrl: bool = false, ctrl: bool = false,
alt: bool = false, alt: bool = false,
super: bool = false, super: bool = false,
caps_lock: bool = false, caps_lock: bool = false,
num_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. /// Returns true if no modifiers are set.
pub fn empty(self: Mods) bool { pub fn empty(self: Mods) bool {
return @as(u8, @bitCast(self)) == 0; return self.int() == 0;
} }
/// Returns true if two mods are equal. /// Returns true if two mods are equal.
pub fn equal(self: Mods, other: Mods) bool { 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.
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. /// Returns the mods without locks set.
@ -35,10 +67,10 @@ pub const Mods = 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(Mods{})), @as(u8, 0b0)); try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0));
try testing.expectEqual( try testing.expectEqual(
@as(u8, @bitCast(Mods{ .shift = true })), @as(Backing, @bitCast(Mods{ .shift = true })),
@as(u8, 0b0000_0001), @as(Backing, 0b0000_0001),
); );
} }
}; };