diff --git a/src/config/Config.zig b/src/config/Config.zig index c1888dbe8..9b0e6cc0f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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);