diff --git a/src/config/Config.zig b/src/config/Config.zig index 8f1cece45..52141293a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1676,8 +1676,10 @@ class: ?[:0]const u8 = null, /// Key tables are defined using the syntax `/`. The /// `` value is everything documented above for keybinds. The /// `
` value is the name of the key table. Table names can contain -/// anything except `/` and `=`. For example `foo/ctrl+a=new_window` -/// defines a binding within a table named `foo`. +/// anything except `/`, `=`, `+`, and `>`. The characters `+` and `>` are +/// reserved for keybind syntax (modifier combinations and key sequences). +/// For example `foo/ctrl+a=new_window` defines a binding within a table +/// named `foo`. /// /// Tables are activated and deactivated using the binding actions /// `activate_key_table:` and `deactivate_key_table`. Other table @@ -6644,12 +6646,21 @@ pub const Keybinds = struct { // We look for '/' only before the first '=' to avoid matching // action arguments like "foo=text:/hello". const eq_idx = std.mem.indexOfScalar(u8, value, '=') orelse value.len; - if (std.mem.indexOfScalar(u8, value[0..eq_idx], '/')) |slash_idx| { + if (std.mem.indexOfScalar(u8, value[0..eq_idx], '/')) |slash_idx| table: { const table_name = value[0..slash_idx]; - const binding = value[slash_idx + 1 ..]; - // Table name cannot be empty - if (table_name.len == 0) return error.InvalidFormat; + // Length zero is valid, so you can set `/=action` for the slash key + if (table_name.len == 0) break :table; + + // Ignore '+', '>' because they can be part of sequences and + // triggers. This lets things like `ctrl+/=action` work. + if (std.mem.indexOfAny( + u8, + table_name, + "+>", + ) != null) break :table; + + const binding = value[slash_idx + 1 ..]; // Get or create the table const gop = try self.tables.getOrPut(alloc, table_name); @@ -7002,6 +7013,105 @@ pub const Keybinds = struct { try testing.expectEqual(0, keybinds.tables.count()); } + test "parseCLI slash as key with modifier is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // ctrl+/ should be parsed as a keybind with '/' as the key, not a table + try keybinds.parseCLI(alloc, "ctrl+/=text:foo"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI shift+slash as key is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // shift+/ should be parsed as a keybind, not a table + try keybinds.parseCLI(alloc, "shift+/=ignore"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI bare slash as key is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Bare / as a key should work (empty table name is rejected) + try keybinds.parseCLI(alloc, "/=text:foo"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI slash in key sequence is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Key sequence ending with / should work + try keybinds.parseCLI(alloc, "ctrl+a>ctrl+/=new_window"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI table with slash in binding" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Table with a binding that uses / as the key + try keybinds.parseCLI(alloc, "mytable//=text:foo"); + + // Should be in the table + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("mytable")); + try testing.expectEqual(1, keybinds.tables.get("mytable").?.bindings.count()); + } + + test "parseCLI table with sequence containing slash" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Table with a key sequence that ends with / + try keybinds.parseCLI(alloc, "mytable/a>/=new_window"); + + // Should be in the table + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("mytable")); + } + test "clone with tables" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator);