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_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 {

View File

@ -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)
}

View File

@ -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,
}
}
@ -1094,12 +1096,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.
@ -1154,8 +1151,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.
@ -1191,8 +1188,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

View File

@ -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;
};
@ -660,7 +668,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 +695,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))),
)),
);
}

View File

@ -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,

View File

@ -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,

View File

@ -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" {

View File

@ -5,23 +5,55 @@ 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.
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.
@ -35,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),
);
}
};