mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
input: move flags to a packed struct
This commit is contained in:
@ -1605,8 +1605,7 @@ fn maybeHandleBinding(
|
||||
return .consumed;
|
||||
},
|
||||
|
||||
.action => |v| .{ v, true },
|
||||
.action_unconsumed => |v| .{ v, false },
|
||||
.leaf => |leaf| .{ leaf.action, leaf.flags.consumed },
|
||||
};
|
||||
|
||||
// We have an action, so at this point we're handling SOMETHING so
|
||||
|
@ -116,7 +116,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
while (iter.next()) |bind| {
|
||||
const action = switch (bind.value_ptr.*) {
|
||||
.leader => continue, // TODO: support this
|
||||
.action, .action_unconsumed => |action| action,
|
||||
.leaf => |leaf| leaf.action,
|
||||
};
|
||||
const key = switch (bind.key_ptr.key) {
|
||||
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
|
||||
|
@ -743,6 +743,20 @@ class: ?[:0]const u8 = null,
|
||||
/// The keybind trigger can be prefixed with some special values to change
|
||||
/// 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
|
||||
/// will consume the input, meaning that the associated encoding (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: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,
|
||||
/// `global:unconsumed:ctrl+a=reload_config` will make the keybind global
|
||||
/// 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
|
||||
/// not granted, the keybind will not work. On macOS, you can find these
|
||||
/// 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 = .{},
|
||||
|
||||
/// Horizontal window padding. This applies padding between the terminal cells
|
||||
@ -3735,11 +3737,16 @@ pub const Keybinds = struct {
|
||||
)) return false,
|
||||
|
||||
// Actions are compared by field directly
|
||||
inline .action, .action_unconsumed => |_, tag| if (!equalField(
|
||||
inputpkg.Binding.Action,
|
||||
@field(self_entry.value_ptr.*, @tagName(tag)),
|
||||
@field(other_entry.value_ptr.*, @tagName(tag)),
|
||||
)) return false,
|
||||
.leaf => {
|
||||
const self_leaf = self_entry.value_ptr.*.leaf;
|
||||
const other_leaf = other_entry.value_ptr.*.leaf;
|
||||
|
||||
if (!equalField(
|
||||
inputpkg.Binding.Set.Leaf,
|
||||
self_leaf,
|
||||
other_leaf,
|
||||
)) return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,14 +13,8 @@ trigger: Trigger,
|
||||
/// The action to take if this binding matches
|
||||
action: Action,
|
||||
|
||||
/// True if this binding should consume the input when the
|
||||
/// action is triggered.
|
||||
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,
|
||||
/// Boolean flags that can be set per binding.
|
||||
flags: Flags = .{},
|
||||
|
||||
pub const Error = error{
|
||||
InvalidFormat,
|
||||
@ -33,6 +27,10 @@ pub const Flags = packed struct {
|
||||
/// action is triggered.
|
||||
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
|
||||
/// and not just while Ghostty is focused. This may not work on all platforms.
|
||||
/// See the keybind config documentation for more information.
|
||||
@ -82,12 +80,15 @@ pub const Parser = struct {
|
||||
const prefix = input[0..idx];
|
||||
|
||||
// If the prefix is one of our flags then set it.
|
||||
if (std.mem.eql(u8, prefix, "unconsumed")) {
|
||||
if (!flags.consumed) return Error.InvalidFormat;
|
||||
flags.consumed = false;
|
||||
if (std.mem.eql(u8, prefix, "all")) {
|
||||
if (flags.all) return Error.InvalidFormat;
|
||||
flags.all = true;
|
||||
} else if (std.mem.eql(u8, prefix, "global")) {
|
||||
if (flags.global) return Error.InvalidFormat;
|
||||
flags.global = true;
|
||||
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
|
||||
if (!flags.consumed) return Error.InvalidFormat;
|
||||
flags.consumed = false;
|
||||
} else {
|
||||
// If we don't recognize the prefix then we're done.
|
||||
// There are trigger-specific prefixes like "physical:" so
|
||||
@ -114,8 +115,7 @@ pub const Parser = struct {
|
||||
return .{ .binding = .{
|
||||
.trigger = trigger,
|
||||
.action = self.action,
|
||||
.consumed = self.flags.consumed,
|
||||
.global = self.flags.global,
|
||||
.flags = self.flags,
|
||||
} };
|
||||
}
|
||||
|
||||
@ -590,10 +590,15 @@ pub const Action = union(enum) {
|
||||
/// action.
|
||||
pub fn hash(self: Action) u64 {
|
||||
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.
|
||||
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.
|
||||
switch (self) {
|
||||
@ -608,25 +613,23 @@ pub const Action = union(enum) {
|
||||
// signed zeros but these are not cases we expect for
|
||||
// our bindings.
|
||||
f32 => std.hash.autoHash(
|
||||
&hasher,
|
||||
hasher,
|
||||
@as(u32, @bitCast(field)),
|
||||
),
|
||||
f64 => std.hash.autoHash(
|
||||
&hasher,
|
||||
hasher,
|
||||
@as(u64, @bitCast(field)),
|
||||
),
|
||||
|
||||
// Everything else automatically handle.
|
||||
else => std.hash.autoHashStrat(
|
||||
&hasher,
|
||||
hasher,
|
||||
field,
|
||||
.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.
|
||||
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.binding());
|
||||
self.hashIncremental(&hasher);
|
||||
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.
|
||||
pub fn cval(self: Trigger) C {
|
||||
return .{
|
||||
@ -864,10 +872,8 @@ pub const Set = struct {
|
||||
leader: *Set,
|
||||
|
||||
/// This trigger completes a sequence and the value is the action
|
||||
/// to take. The "_unconsumed" variant is used for triggers that
|
||||
/// should not consume the input.
|
||||
action: Action,
|
||||
action_unconsumed: Action,
|
||||
/// to take along with the flags that may define binding behavior.
|
||||
leaf: Leaf,
|
||||
|
||||
/// Implements the formatter for the fmt package. This encodes the
|
||||
/// 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
|
||||
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 {
|
||||
// Clear any leaders if we have them
|
||||
var it = self.bindings.iterator();
|
||||
@ -908,7 +928,7 @@ pub const Set = struct {
|
||||
s.deinit(alloc);
|
||||
alloc.destroy(s);
|
||||
},
|
||||
.action, .action_unconsumed => {},
|
||||
.leaf => {},
|
||||
};
|
||||
|
||||
self.bindings.deinit(alloc);
|
||||
@ -980,7 +1000,7 @@ pub const Set = struct {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
},
|
||||
|
||||
.action, .action_unconsumed => {
|
||||
.leaf => {
|
||||
// Remove the existing action. Fallthrough as if
|
||||
// we don't have a leader.
|
||||
set.remove(alloc, t);
|
||||
@ -1004,11 +1024,11 @@ pub const Set = struct {
|
||||
set.remove(alloc, t);
|
||||
if (old) |entry| switch (entry) {
|
||||
.leader => unreachable, // Handled above
|
||||
inline .action, .action_unconsumed => |action, tag| set.put_(
|
||||
.leaf => |leaf| set.put_(
|
||||
alloc,
|
||||
t,
|
||||
action,
|
||||
tag == .action,
|
||||
leaf.action,
|
||||
leaf.flags,
|
||||
) catch {},
|
||||
};
|
||||
},
|
||||
@ -1023,7 +1043,7 @@ pub const Set = struct {
|
||||
return error.SequenceUnbind;
|
||||
},
|
||||
|
||||
else => if (b.consumed) {
|
||||
else => if (b.flags.consumed) {
|
||||
try set.put(alloc, b.trigger, b.action);
|
||||
} else {
|
||||
try set.putUnconsumed(alloc, b.trigger, b.action);
|
||||
@ -1040,7 +1060,7 @@ pub const Set = struct {
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
) 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
|
||||
@ -1054,7 +1074,7 @@ pub const Set = struct {
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
) Allocator.Error!void {
|
||||
try self.put_(alloc, t, action, false);
|
||||
try self.put_(alloc, t, action, .{ .consumed = false });
|
||||
}
|
||||
|
||||
fn put_(
|
||||
@ -1062,7 +1082,7 @@ pub const Set = struct {
|
||||
alloc: Allocator,
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
consumed: bool,
|
||||
flags: Flags,
|
||||
) Allocator.Error!void {
|
||||
// unbind should never go into the set, it should be handled prior
|
||||
assert(action != .unbind);
|
||||
@ -1078,7 +1098,7 @@ pub const Set = struct {
|
||||
|
||||
// If we have an existing binding for this trigger, we have to
|
||||
// update the reverse mapping to remove the old action.
|
||||
.action, .action_unconsumed => {
|
||||
.leaf => {
|
||||
const t_hash = t.hash();
|
||||
var it = self.reverse.iterator();
|
||||
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,
|
||||
} else .{
|
||||
.action_unconsumed = action,
|
||||
};
|
||||
.flags = flags,
|
||||
} };
|
||||
errdefer _ = self.bindings.remove(t);
|
||||
try self.reverse.put(alloc, action, t);
|
||||
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
|
||||
// our hash map obviously has no concept of ordering so we have to
|
||||
// choose whatever. Maybe a switch to an array hash map here.
|
||||
.action, .action_unconsumed => |action| {
|
||||
const action_hash = action.hash();
|
||||
.leaf => |leaf| {
|
||||
const action_hash = leaf.action.hash();
|
||||
|
||||
var it = self.bindings.iterator();
|
||||
while (it.next()) |it_entry| {
|
||||
switch (it_entry.value_ptr.*) {
|
||||
.leader => {},
|
||||
.action, .action_unconsumed => |action_search| {
|
||||
if (action_search.hash() == action_hash) {
|
||||
self.reverse.putAssumeCapacity(action, it_entry.key_ptr.*);
|
||||
.leaf => |leaf_search| {
|
||||
if (leaf_search.action.hash() == action_hash) {
|
||||
self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@ -1145,7 +1165,7 @@ pub const Set = struct {
|
||||
} else {
|
||||
// No over trigger points to this action so we remove
|
||||
// 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();
|
||||
while (it.next()) |entry| switch (entry.value_ptr.*) {
|
||||
// No data to clone
|
||||
.action, .action_unconsumed => {},
|
||||
.leaf => {},
|
||||
|
||||
// Must be deep cloned.
|
||||
.leader => |*s| {
|
||||
@ -1264,7 +1284,7 @@ test "parse: triggers" {
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.flags = .{ .consumed = false },
|
||||
}, try parseSingle("unconsumed:shift+a=ignore"));
|
||||
|
||||
// unconsumed physical keys
|
||||
@ -1274,7 +1294,7 @@ test "parse: triggers" {
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.flags = .{ .consumed = false },
|
||||
}, try parseSingle("unconsumed:physical:a+shift=ignore"));
|
||||
|
||||
// invalid key
|
||||
@ -1297,7 +1317,7 @@ test "parse: global triggers" {
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.global = true,
|
||||
.flags = .{ .global = true },
|
||||
}, try parseSingle("global:shift+a=ignore"));
|
||||
|
||||
// global physical keys
|
||||
@ -1307,7 +1327,7 @@ test "parse: global triggers" {
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.global = true,
|
||||
.flags = .{ .global = true },
|
||||
}, try parseSingle("global:physical:a+shift=ignore"));
|
||||
|
||||
// global unconsumed keys
|
||||
@ -1317,8 +1337,10 @@ test "parse: global triggers" {
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.flags = .{
|
||||
.global = true,
|
||||
.consumed = false,
|
||||
},
|
||||
}, try parseSingle("unconsumed:global:a+shift=ignore"));
|
||||
}
|
||||
|
||||
@ -1547,8 +1569,9 @@ test "set: parseAndPut typical binding" {
|
||||
|
||||
// Creates forward mapping
|
||||
{
|
||||
const action = s.get(.{ .key = .{ .translated = .a } }).?.action;
|
||||
try testing.expect(action == .new_window);
|
||||
const action = s.get(.{ .key = .{ .translated = .a } }).?.leaf;
|
||||
try testing.expect(action.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, action.flags);
|
||||
}
|
||||
|
||||
// Creates reverse mapping
|
||||
@ -1570,8 +1593,9 @@ test "set: parseAndPut unconsumed binding" {
|
||||
// Creates forward mapping
|
||||
{
|
||||
const trigger: Trigger = .{ .key = .{ .translated = .a } };
|
||||
const action = s.get(trigger).?.action_unconsumed;
|
||||
try testing.expect(action == .new_window);
|
||||
const action = s.get(trigger).?.leaf;
|
||||
try testing.expect(action.action == .new_window);
|
||||
try testing.expectEqual(Flags{ .consumed = false }, action.flags);
|
||||
}
|
||||
|
||||
// Creates reverse mapping
|
||||
@ -1617,8 +1641,9 @@ test "set: parseAndPut sequence" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
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 e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .c } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_tab);
|
||||
try testing.expect(e == .leaf);
|
||||
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 e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
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 e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
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);
|
||||
|
||||
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 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 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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user