input: move flags to a packed struct

This commit is contained in:
Mitchell Hashimoto
2024-09-23 10:16:35 -07:00
parent 0394c8e2df
commit 66143a33ef
4 changed files with 129 additions and 91 deletions

View File

@ -1605,8 +1605,7 @@ fn maybeHandleBinding(
return .consumed; return .consumed;
}, },
.action => |v| .{ v, true }, .leaf => |leaf| .{ leaf.action, leaf.flags.consumed },
.action_unconsumed => |v| .{ v, false },
}; };
// We have an action, so at this point we're handling SOMETHING so // We have an action, so at this point we're handling SOMETHING so

View File

@ -116,7 +116,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
while (iter.next()) |bind| { while (iter.next()) |bind| {
const action = switch (bind.value_ptr.*) { const action = switch (bind.value_ptr.*) {
.leader => continue, // TODO: support this .leader => continue, // TODO: support this
.action, .action_unconsumed => |action| action, .leaf => |leaf| leaf.action,
}; };
const key = switch (bind.key_ptr.key) { const key = switch (bind.key_ptr.key) {
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}), .translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),

View File

@ -743,6 +743,20 @@ class: ?[:0]const u8 = null,
/// The keybind trigger can be prefixed with some special values to change /// The keybind trigger can be prefixed with some special values to change
/// the behavior of the keybind. These are: /// the behavior of the keybind. These are:
/// ///
/// * `all:` - Make the keybind apply to all terminal surfaces. By default,
/// keybinds only apply to the focused terminal surface. If this is true,
/// then the keybind will be sent to all terminal surfaces. This only
/// applies to actions that are surface-specific. For actions that
/// are already global (i.e. `quit`), this prefix has no effect.
///
/// * `global:` - Make the keybind global. By default, keybinds only work
/// within Ghostty and under the right conditions (application focused,
/// sometimes terminal focused, etc.). If you want a keybind to work
/// globally across your system (i.e. even when Ghostty is not focused),
/// specify this prefix. This prefix implies `all:`. Note: this does not
/// work in all environments; see the additional notes below for more
/// information.
///
/// * `unconsumed:` - Do not consume the input. By default, a keybind /// * `unconsumed:` - Do not consume the input. By default, a keybind
/// will consume the input, meaning that the associated encoding (if /// will consume the input, meaning that the associated encoding (if
/// any) will not be sent to the running program in the terminal. If /// any) will not be sent to the running program in the terminal. If
@ -750,13 +764,6 @@ class: ?[:0]const u8 = null,
/// `unconsumed:` prefix before the entire keybind. For example: /// `unconsumed:` prefix before the entire keybind. For example:
/// `unconsumed:ctrl+a=reload_config` /// `unconsumed:ctrl+a=reload_config`
/// ///
/// * `global:` - Make the keybind global. By default, keybinds only work
/// within Ghostty and under the right conditions (application focused,
/// sometimes terminal focused, etc.). If you want a keybind to work
/// globally across your system (i.e. even when Ghostty is not focused),
/// specify this prefix. Note: this does not work in all environments;
/// see the additional notes below for more information.
///
/// Multiple prefixes can be specified. For example, /// Multiple prefixes can be specified. For example,
/// `global:unconsumed:ctrl+a=reload_config` will make the keybind global /// `global:unconsumed:ctrl+a=reload_config` will make the keybind global
/// and not consume the input to reload the config. /// and not consume the input to reload the config.
@ -767,11 +774,6 @@ class: ?[:0]const u8 = null,
/// Ghostty will attempt to request these permissions. If the permissions are /// Ghostty will attempt to request these permissions. If the permissions are
/// not granted, the keybind will not work. On macOS, you can find these /// not granted, the keybind will not work. On macOS, you can find these
/// permissions in System Preferences -> Privacy & Security -> Accessibility. /// permissions in System Preferences -> Privacy & Security -> Accessibility.
///
/// Additionally, `global:` keybinds associated with actions that affect
/// a specific terminal surface will target the last focused terminal surface
/// within Ghostty. There is not a way to target a specific terminal surface
/// with a `global:` keybind.
keybind: Keybinds = .{}, keybind: Keybinds = .{},
/// Horizontal window padding. This applies padding between the terminal cells /// Horizontal window padding. This applies padding between the terminal cells
@ -3735,11 +3737,16 @@ pub const Keybinds = struct {
)) return false, )) return false,
// Actions are compared by field directly // Actions are compared by field directly
inline .action, .action_unconsumed => |_, tag| if (!equalField( .leaf => {
inputpkg.Binding.Action, const self_leaf = self_entry.value_ptr.*.leaf;
@field(self_entry.value_ptr.*, @tagName(tag)), const other_leaf = other_entry.value_ptr.*.leaf;
@field(other_entry.value_ptr.*, @tagName(tag)),
)) return false, if (!equalField(
inputpkg.Binding.Set.Leaf,
self_leaf,
other_leaf,
)) return false;
},
} }
} }

View File

@ -13,14 +13,8 @@ trigger: Trigger,
/// The action to take if this binding matches /// The action to take if this binding matches
action: Action, action: Action,
/// True if this binding should consume the input when the /// Boolean flags that can be set per binding.
/// action is triggered. flags: Flags = .{},
consumed: bool = true,
/// True if this binding is global. Global bindings should work system-wide
/// and not just while Ghostty is focused. This may not work on all platforms.
/// See the keybind config documentation for more information.
global: bool = false,
pub const Error = error{ pub const Error = error{
InvalidFormat, InvalidFormat,
@ -33,6 +27,10 @@ pub const Flags = packed struct {
/// action is triggered. /// action is triggered.
consumed: bool = true, consumed: bool = true,
/// True if this binding should be forwarded to all active surfaces
/// in the application.
all: bool = false,
/// True if this binding is global. Global bindings should work system-wide /// True if this binding is global. Global bindings should work system-wide
/// and not just while Ghostty is focused. This may not work on all platforms. /// and not just while Ghostty is focused. This may not work on all platforms.
/// See the keybind config documentation for more information. /// See the keybind config documentation for more information.
@ -82,12 +80,15 @@ pub const Parser = struct {
const prefix = input[0..idx]; const prefix = input[0..idx];
// If the prefix is one of our flags then set it. // If the prefix is one of our flags then set it.
if (std.mem.eql(u8, prefix, "unconsumed")) { if (std.mem.eql(u8, prefix, "all")) {
if (!flags.consumed) return Error.InvalidFormat; if (flags.all) return Error.InvalidFormat;
flags.consumed = false; flags.all = true;
} else if (std.mem.eql(u8, prefix, "global")) { } else if (std.mem.eql(u8, prefix, "global")) {
if (flags.global) return Error.InvalidFormat; if (flags.global) return Error.InvalidFormat;
flags.global = true; flags.global = true;
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
if (!flags.consumed) return Error.InvalidFormat;
flags.consumed = false;
} else { } else {
// If we don't recognize the prefix then we're done. // If we don't recognize the prefix then we're done.
// There are trigger-specific prefixes like "physical:" so // There are trigger-specific prefixes like "physical:" so
@ -114,8 +115,7 @@ pub const Parser = struct {
return .{ .binding = .{ return .{ .binding = .{
.trigger = trigger, .trigger = trigger,
.action = self.action, .action = self.action,
.consumed = self.flags.consumed, .flags = self.flags,
.global = self.flags.global,
} }; } };
} }
@ -590,10 +590,15 @@ pub const Action = union(enum) {
/// action. /// action.
pub fn hash(self: Action) u64 { pub fn hash(self: Action) u64 {
var hasher = std.hash.Wyhash.init(0); var hasher = std.hash.Wyhash.init(0);
self.hashIncremental(&hasher);
return hasher.final();
}
/// Hash the action into the given hasher.
fn hashIncremental(self: Action, hasher: anytype) void {
// Always has the active tag. // Always has the active tag.
const Tag = @typeInfo(Action).Union.tag_type.?; const Tag = @typeInfo(Action).Union.tag_type.?;
std.hash.autoHash(&hasher, @as(Tag, self)); std.hash.autoHash(hasher, @as(Tag, self));
// Hash the value of the field. // Hash the value of the field.
switch (self) { switch (self) {
@ -608,25 +613,23 @@ pub const Action = union(enum) {
// signed zeros but these are not cases we expect for // signed zeros but these are not cases we expect for
// our bindings. // our bindings.
f32 => std.hash.autoHash( f32 => std.hash.autoHash(
&hasher, hasher,
@as(u32, @bitCast(field)), @as(u32, @bitCast(field)),
), ),
f64 => std.hash.autoHash( f64 => std.hash.autoHash(
&hasher, hasher,
@as(u64, @bitCast(field)), @as(u64, @bitCast(field)),
), ),
// Everything else automatically handle. // Everything else automatically handle.
else => std.hash.autoHashStrat( else => std.hash.autoHashStrat(
&hasher, hasher,
field, field,
.DeepRecursive, .DeepRecursive,
), ),
} }
}, },
} }
return hasher.final();
} }
}; };
@ -783,11 +786,16 @@ pub const Trigger = struct {
/// 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: Trigger) 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); self.hashIncremental(&hasher);
std.hash.autoHash(&hasher, self.mods.binding());
return hasher.final(); return hasher.final();
} }
/// Hash the trigger into the given hasher.
fn hashIncremental(self: Trigger, hasher: anytype) void {
std.hash.autoHash(hasher, self.key);
std.hash.autoHash(hasher, self.mods.binding());
}
/// Convert the trigger to a C API compatible trigger. /// Convert the trigger to a C API compatible trigger.
pub fn cval(self: Trigger) C { pub fn cval(self: Trigger) C {
return .{ return .{
@ -864,10 +872,8 @@ pub const Set = struct {
leader: *Set, leader: *Set,
/// This trigger completes a sequence and the value is the action /// This trigger completes a sequence and the value is the action
/// to take. The "_unconsumed" variant is used for triggers that /// to take along with the flags that may define binding behavior.
/// should not consume the input. leaf: Leaf,
action: Action,
action_unconsumed: Action,
/// Implements the formatter for the fmt package. This encodes the /// Implements the formatter for the fmt package. This encodes the
/// action back into the format used by parse. /// action back into the format used by parse.
@ -892,14 +898,28 @@ pub const Set = struct {
} }
}, },
.action, .action_unconsumed => |action| { .leaf => |leaf| {
// action implements the format // action implements the format
try writer.print("={s}", .{action}); try writer.print("={s}", .{leaf.action});
}, },
} }
} }
}; };
/// Leaf node of a set is an action to trigger. This is a "leaf" compared
/// to the inner nodes which are "leaders" for sequences.
pub const Leaf = struct {
action: Action,
flags: Flags,
pub fn hash(self: Leaf) u64 {
var hasher = std.hash.Wyhash.init(0);
self.action.hash(&hasher);
std.hash.autoHash(&hasher, self.flags);
return hasher.final();
}
};
pub fn deinit(self: *Set, alloc: Allocator) void { pub fn deinit(self: *Set, alloc: Allocator) void {
// Clear any leaders if we have them // Clear any leaders if we have them
var it = self.bindings.iterator(); var it = self.bindings.iterator();
@ -908,7 +928,7 @@ pub const Set = struct {
s.deinit(alloc); s.deinit(alloc);
alloc.destroy(s); alloc.destroy(s);
}, },
.action, .action_unconsumed => {}, .leaf => {},
}; };
self.bindings.deinit(alloc); self.bindings.deinit(alloc);
@ -980,7 +1000,7 @@ pub const Set = struct {
error.OutOfMemory => return error.OutOfMemory, error.OutOfMemory => return error.OutOfMemory,
}, },
.action, .action_unconsumed => { .leaf => {
// Remove the existing action. Fallthrough as if // Remove the existing action. Fallthrough as if
// we don't have a leader. // we don't have a leader.
set.remove(alloc, t); set.remove(alloc, t);
@ -1004,11 +1024,11 @@ pub const Set = struct {
set.remove(alloc, t); set.remove(alloc, t);
if (old) |entry| switch (entry) { if (old) |entry| switch (entry) {
.leader => unreachable, // Handled above .leader => unreachable, // Handled above
inline .action, .action_unconsumed => |action, tag| set.put_( .leaf => |leaf| set.put_(
alloc, alloc,
t, t,
action, leaf.action,
tag == .action, leaf.flags,
) catch {}, ) catch {},
}; };
}, },
@ -1023,7 +1043,7 @@ pub const Set = struct {
return error.SequenceUnbind; return error.SequenceUnbind;
}, },
else => if (b.consumed) { else => if (b.flags.consumed) {
try set.put(alloc, b.trigger, b.action); try set.put(alloc, b.trigger, b.action);
} else { } else {
try set.putUnconsumed(alloc, b.trigger, b.action); try set.putUnconsumed(alloc, b.trigger, b.action);
@ -1040,7 +1060,7 @@ pub const Set = struct {
t: Trigger, t: Trigger,
action: Action, action: Action,
) Allocator.Error!void { ) Allocator.Error!void {
try self.put_(alloc, t, action, true); try self.put_(alloc, t, action, .{});
} }
/// Same as put but marks the trigger as unconsumed. An unconsumed /// Same as put but marks the trigger as unconsumed. An unconsumed
@ -1054,7 +1074,7 @@ pub const Set = struct {
t: Trigger, t: Trigger,
action: Action, action: Action,
) Allocator.Error!void { ) Allocator.Error!void {
try self.put_(alloc, t, action, false); try self.put_(alloc, t, action, .{ .consumed = false });
} }
fn put_( fn put_(
@ -1062,7 +1082,7 @@ pub const Set = struct {
alloc: Allocator, alloc: Allocator,
t: Trigger, t: Trigger,
action: Action, action: Action,
consumed: bool, flags: Flags,
) Allocator.Error!void { ) Allocator.Error!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);
@ -1078,7 +1098,7 @@ pub const Set = struct {
// If we have an existing binding for this trigger, we have to // If we have an existing binding for this trigger, we have to
// update the reverse mapping to remove the old action. // update the reverse mapping to remove the old action.
.action, .action_unconsumed => { .leaf => {
const t_hash = t.hash(); const t_hash = t.hash();
var it = self.reverse.iterator(); var it = self.reverse.iterator();
while (it.next()) |reverse_entry| it: { while (it.next()) |reverse_entry| it: {
@ -1090,11 +1110,10 @@ pub const Set = struct {
}, },
}; };
gop.value_ptr.* = if (consumed) .{ gop.value_ptr.* = .{ .leaf = .{
.action = action, .action = action,
} else .{ .flags = flags,
.action_unconsumed = action, } };
};
errdefer _ = self.bindings.remove(t); errdefer _ = self.bindings.remove(t);
try self.reverse.put(alloc, action, t); try self.reverse.put(alloc, action, t);
errdefer _ = self.reverse.remove(action); errdefer _ = self.reverse.remove(action);
@ -1129,15 +1148,16 @@ pub const Set = struct {
// Note: we'd LIKE to replace this with the most recent binding but // Note: we'd LIKE to replace this with the most recent binding but
// our hash map obviously has no concept of ordering so we have to // our hash map obviously has no concept of ordering so we have to
// choose whatever. Maybe a switch to an array hash map here. // choose whatever. Maybe a switch to an array hash map here.
.action, .action_unconsumed => |action| { .leaf => |leaf| {
const action_hash = action.hash(); const action_hash = leaf.action.hash();
var it = self.bindings.iterator(); var it = self.bindings.iterator();
while (it.next()) |it_entry| { while (it.next()) |it_entry| {
switch (it_entry.value_ptr.*) { switch (it_entry.value_ptr.*) {
.leader => {}, .leader => {},
.action, .action_unconsumed => |action_search| { .leaf => |leaf_search| {
if (action_search.hash() == action_hash) { if (leaf_search.action.hash() == action_hash) {
self.reverse.putAssumeCapacity(action, it_entry.key_ptr.*); self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*);
break; break;
} }
}, },
@ -1145,7 +1165,7 @@ pub const Set = struct {
} else { } else {
// No over trigger points to this action so we remove // No over trigger points to this action so we remove
// the reverse mapping completely. // the reverse mapping completely.
_ = self.reverse.remove(action); _ = self.reverse.remove(leaf.action);
} }
}, },
} }
@ -1162,7 +1182,7 @@ pub const Set = struct {
var it = result.bindings.iterator(); var it = result.bindings.iterator();
while (it.next()) |entry| switch (entry.value_ptr.*) { while (it.next()) |entry| switch (entry.value_ptr.*) {
// No data to clone // No data to clone
.action, .action_unconsumed => {}, .leaf => {},
// Must be deep cloned. // Must be deep cloned.
.leader => |*s| { .leader => |*s| {
@ -1264,7 +1284,7 @@ test "parse: triggers" {
.key = .{ .translated = .a }, .key = .{ .translated = .a },
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
.consumed = false, .flags = .{ .consumed = false },
}, try parseSingle("unconsumed:shift+a=ignore")); }, try parseSingle("unconsumed:shift+a=ignore"));
// unconsumed physical keys // unconsumed physical keys
@ -1274,7 +1294,7 @@ test "parse: triggers" {
.key = .{ .physical = .a }, .key = .{ .physical = .a },
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
.consumed = false, .flags = .{ .consumed = false },
}, try parseSingle("unconsumed:physical:a+shift=ignore")); }, try parseSingle("unconsumed:physical:a+shift=ignore"));
// invalid key // invalid key
@ -1297,7 +1317,7 @@ test "parse: global triggers" {
.key = .{ .translated = .a }, .key = .{ .translated = .a },
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
.global = true, .flags = .{ .global = true },
}, try parseSingle("global:shift+a=ignore")); }, try parseSingle("global:shift+a=ignore"));
// global physical keys // global physical keys
@ -1307,7 +1327,7 @@ test "parse: global triggers" {
.key = .{ .physical = .a }, .key = .{ .physical = .a },
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
.global = true, .flags = .{ .global = true },
}, try parseSingle("global:physical:a+shift=ignore")); }, try parseSingle("global:physical:a+shift=ignore"));
// global unconsumed keys // global unconsumed keys
@ -1317,8 +1337,10 @@ test "parse: global triggers" {
.key = .{ .translated = .a }, .key = .{ .translated = .a },
}, },
.action = .{ .ignore = {} }, .action = .{ .ignore = {} },
.consumed = false, .flags = .{
.global = true, .global = true,
.consumed = false,
},
}, try parseSingle("unconsumed:global:a+shift=ignore")); }, try parseSingle("unconsumed:global:a+shift=ignore"));
} }
@ -1547,8 +1569,9 @@ test "set: parseAndPut typical binding" {
// Creates forward mapping // Creates forward mapping
{ {
const action = s.get(.{ .key = .{ .translated = .a } }).?.action; const action = s.get(.{ .key = .{ .translated = .a } }).?.leaf;
try testing.expect(action == .new_window); try testing.expect(action.action == .new_window);
try testing.expectEqual(Flags{}, action.flags);
} }
// Creates reverse mapping // Creates reverse mapping
@ -1570,8 +1593,9 @@ test "set: parseAndPut unconsumed binding" {
// Creates forward mapping // Creates forward mapping
{ {
const trigger: Trigger = .{ .key = .{ .translated = .a } }; const trigger: Trigger = .{ .key = .{ .translated = .a } };
const action = s.get(trigger).?.action_unconsumed; const action = s.get(trigger).?.leaf;
try testing.expect(action == .new_window); try testing.expect(action.action == .new_window);
try testing.expectEqual(Flags{ .consumed = false }, action.flags);
} }
// Creates reverse mapping // Creates reverse mapping
@ -1617,8 +1641,9 @@ test "set: parseAndPut sequence" {
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?;
try testing.expect(e == .action); try testing.expect(e == .leaf);
try testing.expect(e.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags);
} }
} }
@ -1641,14 +1666,16 @@ test "set: parseAndPut sequence with two actions" {
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?;
try testing.expect(e == .action); try testing.expect(e == .leaf);
try testing.expect(e.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags);
} }
{ {
const t: Trigger = .{ .key = .{ .translated = .c } }; const t: Trigger = .{ .key = .{ .translated = .c } };
const e = current.get(t).?; const e = current.get(t).?;
try testing.expect(e == .action); try testing.expect(e == .leaf);
try testing.expect(e.action == .new_tab); try testing.expect(e.leaf.action == .new_tab);
try testing.expectEqual(Flags{}, e.leaf.flags);
} }
} }
@ -1671,8 +1698,9 @@ test "set: parseAndPut overwrite sequence" {
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?;
try testing.expect(e == .action); try testing.expect(e == .leaf);
try testing.expect(e.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags);
} }
} }
@ -1695,8 +1723,9 @@ test "set: parseAndPut overwrite leader" {
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?;
try testing.expect(e == .action); try testing.expect(e == .leaf);
try testing.expect(e.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags);
} }
} }
@ -1825,11 +1854,14 @@ test "set: consumed state" {
defer s.deinit(alloc); defer s.deinit(alloc);
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
try s.putUnconsumed(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); try s.putUnconsumed(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action_unconsumed); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
} }