mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 19:45:49 +00:00
input: Disallow table/chain= and make chain apply to the most recent table (#10954)
Fixes #10039 (Context is all there)
This commit is contained in:
@@ -1858,6 +1858,12 @@ class: ?[:0]const u8 = null,
|
||||
/// If an invalid key is pressed, the sequence ends but the table remains
|
||||
/// active.
|
||||
///
|
||||
/// * Chain actions work within tables, the `chain` keyword applies to
|
||||
/// the most recently defined binding in the table. e.g. if you set
|
||||
/// `table/ctrl+a=new_window` you can chain by using `chain=text:hello`.
|
||||
/// Important: chain itself doesn't get prefixed with the table name,
|
||||
/// since it applies to the most recent binding in any table.
|
||||
///
|
||||
/// * Prefixes like `global:` work within tables:
|
||||
/// `foo/global:ctrl+a=new_window`.
|
||||
///
|
||||
@@ -6213,6 +6219,15 @@ pub const Keybinds = struct {
|
||||
/// which allows all table names to be available without reservation.
|
||||
tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty,
|
||||
|
||||
/// The most recent binding target for `chain=` additions.
|
||||
///
|
||||
/// This is intentionally tracked at the Keybinds level so that chains can
|
||||
/// apply across table boundaries according to parse order.
|
||||
chain_target: union(enum) {
|
||||
root,
|
||||
table: []const u8,
|
||||
} = .root,
|
||||
|
||||
pub fn init(self: *Keybinds, alloc: Allocator) !void {
|
||||
// We don't clear the memory because it's in the arena and unlikely
|
||||
// to be free-able anyways (since arenas can only clear the last
|
||||
@@ -6220,6 +6235,7 @@ pub const Keybinds = struct {
|
||||
// will be freed when the config is freed.
|
||||
self.set = .{};
|
||||
self.tables = .empty;
|
||||
self.chain_target = .root;
|
||||
|
||||
// keybinds for opening and reloading config
|
||||
try self.set.put(
|
||||
@@ -7002,6 +7018,7 @@ pub const Keybinds = struct {
|
||||
log.info("config has 'keybind = clear', all keybinds cleared", .{});
|
||||
self.set = .{};
|
||||
self.tables = .empty;
|
||||
self.chain_target = .root;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7039,16 +7056,39 @@ pub const Keybinds = struct {
|
||||
if (binding.len == 0) {
|
||||
log.debug("config has 'keybind = {s}/', table cleared", .{table_name});
|
||||
gop.value_ptr.* = .{};
|
||||
self.chain_target = .root;
|
||||
return;
|
||||
}
|
||||
|
||||
// Chains are only allowed at the root level. Their target is
|
||||
// tracked globally by parse order in `self.chain_target`.
|
||||
if (std.mem.startsWith(u8, binding, "chain=")) {
|
||||
return error.InvalidFormat;
|
||||
}
|
||||
|
||||
// Parse and add the binding to the table
|
||||
try gop.value_ptr.parseAndPut(alloc, binding);
|
||||
self.chain_target = .{ .table = gop.key_ptr.* };
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, value, "chain=")) {
|
||||
switch (self.chain_target) {
|
||||
.root => try self.set.parseAndPut(alloc, value),
|
||||
.table => |table_name| {
|
||||
const table = self.tables.getPtr(table_name) orelse {
|
||||
self.chain_target = .root;
|
||||
return error.InvalidFormat;
|
||||
};
|
||||
try table.parseAndPut(alloc, value);
|
||||
},
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse into default set
|
||||
try self.set.parseAndPut(alloc, value);
|
||||
self.chain_target = .root;
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
@@ -7490,6 +7530,63 @@ pub const Keybinds = struct {
|
||||
try testing.expect(keybinds.tables.contains("mytable"));
|
||||
}
|
||||
|
||||
test "parseCLI chain without prior parsed binding is invalid" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var keybinds: Keybinds = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidFormat,
|
||||
keybinds.parseCLI(alloc, "chain=new_tab"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseCLI table chain syntax is invalid" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var keybinds: Keybinds = .{};
|
||||
|
||||
try keybinds.parseCLI(alloc, "foo/a=text:hello");
|
||||
try testing.expectError(
|
||||
error.InvalidFormat,
|
||||
keybinds.parseCLI(alloc, "foo/chain=deactivate_key_table"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseCLI chain applies to most recent table binding" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var keybinds: Keybinds = .{};
|
||||
|
||||
try keybinds.parseCLI(alloc, "ctrl+n=activate_key_table:foo");
|
||||
try keybinds.parseCLI(alloc, "foo/a=text:hello");
|
||||
try keybinds.parseCLI(alloc, "chain=deactivate_key_table");
|
||||
|
||||
const root_entry = keybinds.set.get(.{
|
||||
.mods = .{ .ctrl = true },
|
||||
.key = .{ .unicode = 'n' },
|
||||
}).?.value_ptr.*;
|
||||
try testing.expect(root_entry == .leaf);
|
||||
try testing.expect(root_entry.leaf.action == .activate_key_table);
|
||||
|
||||
const foo_entry = keybinds.tables.get("foo").?.get(.{
|
||||
.key = .{ .unicode = 'a' },
|
||||
}).?.value_ptr.*;
|
||||
try testing.expect(foo_entry == .leaf_chained);
|
||||
try testing.expectEqual(@as(usize, 2), foo_entry.leaf_chained.actions.items.len);
|
||||
try testing.expect(foo_entry.leaf_chained.actions.items[0] == .text);
|
||||
try testing.expect(foo_entry.leaf_chained.actions.items[1] == .deactivate_key_table);
|
||||
}
|
||||
|
||||
test "clone with tables" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user