config: keybind table parsing

This commit is contained in:
Mitchell Hashimoto
2025-12-20 13:26:20 -08:00
parent aac5d65ded
commit c53b3fffd5

View File

@@ -5812,6 +5812,10 @@ pub const RepeatableFontVariation = struct {
pub const Keybinds = struct {
set: inputpkg.Binding.Set = .{},
/// Defined key tables. The default key table is always the root "set",
/// which allows all table names to be available without reservation.
tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty,
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
@@ -6590,18 +6594,69 @@ pub const Keybinds = struct {
return;
}
// Let our much better tested binding package handle parsing and storage.
// Check for table syntax: "name/" or "name/binding"
// 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| {
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;
// Get or create the table
const gop = try self.tables.getOrPut(alloc, table_name);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
// If there's no binding after the slash, this is a table
// definition/clear command
if (binding.len == 0) {
log.debug("config has 'keybind = {s}/', table cleared", .{table_name});
gop.value_ptr.* = .{};
return;
}
// Parse and add the binding to the table
try gop.value_ptr.parseAndPut(alloc, binding);
return;
}
// Parse into default set
try self.set.parseAndPut(alloc, value);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Keybinds, alloc: Allocator) Allocator.Error!Keybinds {
return .{ .set = try self.set.clone(alloc) };
var tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty;
try tables.ensureTotalCapacity(alloc, @intCast(self.tables.count()));
var it = self.tables.iterator();
while (it.next()) |entry| {
tables.putAssumeCapacity(entry.key_ptr.*, try entry.value_ptr.clone(alloc));
}
return .{
.set = try self.set.clone(alloc),
.tables = tables,
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Keybinds, other: Keybinds) bool {
return equalSet(&self.set, &other.set);
if (!equalSet(&self.set, &other.set)) return false;
// Compare tables
if (self.tables.count() != other.tables.count()) return false;
var it = self.tables.iterator();
while (it.next()) |entry| {
const other_set = other.tables.get(entry.key_ptr.*) orelse return false;
if (!equalSet(entry.value_ptr, &other_set)) return false;
}
return true;
}
fn equalSet(
@@ -6652,12 +6707,14 @@ pub const Keybinds = struct {
/// Like formatEntry but has an option to include docs.
pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void {
if (self.set.bindings.size == 0) {
if (self.set.bindings.size == 0 and self.tables.count() == 0) {
try formatter.formatEntry(void, {});
return;
}
var buf: [1024]u8 = undefined;
// Format root set bindings
var iter = self.set.bindings.iterator();
while (iter.next()) |next| {
const k = next.key_ptr.*;
@@ -6684,6 +6741,23 @@ pub const Keybinds = struct {
writer.print("{f}", .{k}) catch return error.OutOfMemory;
try v.formatEntries(&writer, formatter);
}
// Format table bindings
var table_iter = self.tables.iterator();
while (table_iter.next()) |table_entry| {
const table_name = table_entry.key_ptr.*;
const table_set = table_entry.value_ptr.*;
var binding_iter = table_set.bindings.iterator();
while (binding_iter.next()) |next| {
const k = next.key_ptr.*;
const v = next.value_ptr.*;
var writer: std.Io.Writer = .fixed(&buf);
writer.print("{s}/{f}", .{ table_name, k }) catch return error.OutOfMemory;
try v.formatEntries(&writer, formatter);
}
}
}
/// Used by Formatter
@@ -6768,6 +6842,237 @@ pub const Keybinds = struct {
;
try std.testing.expectEqualStrings(want, buf.written());
}
test "parseCLI table definition" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Define a table by adding a binding to it
try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
try testing.expectEqual(1, keybinds.tables.count());
try testing.expect(keybinds.tables.contains("foo"));
const table = keybinds.tables.get("foo").?;
try testing.expectEqual(1, table.bindings.count());
}
test "parseCLI table clear" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Add a binding to a table
try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count());
// Clear the table with "foo/"
try keybinds.parseCLI(alloc, "foo/");
try testing.expectEqual(0, keybinds.tables.get("foo").?.bindings.count());
}
test "parseCLI table multiple bindings" {
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/shift+a=copy_to_clipboard");
try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard");
try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window");
try testing.expectEqual(2, keybinds.tables.count());
try testing.expectEqual(2, keybinds.tables.get("foo").?.bindings.count());
try testing.expectEqual(1, keybinds.tables.get("bar").?.bindings.count());
}
test "parseCLI table does not affect root set" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard");
try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard");
// Root set should have the first binding
try testing.expectEqual(1, keybinds.set.bindings.count());
// Table should have the second binding
try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count());
}
test "parseCLI table empty name 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, "/shift+a=copy_to_clipboard"));
}
test "parseCLI table with key sequence" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Key sequences should work within tables
try keybinds.parseCLI(alloc, "foo/ctrl+a>ctrl+b=new_window");
const table = keybinds.tables.get("foo").?;
try testing.expectEqual(1, table.bindings.count());
}
test "parseCLI slash in action argument is not a table" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// A slash after the = should not be interpreted as a table delimiter
try keybinds.parseCLI(alloc, "ctrl+a=text:/hello");
// Should be in root set, not a table
try testing.expectEqual(1, keybinds.set.bindings.count());
try testing.expectEqual(0, keybinds.tables.count());
}
test "clone with tables" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard");
try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard");
try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window");
const cloned = try keybinds.clone(alloc);
// Verify the clone has the same structure
try testing.expectEqual(keybinds.set.bindings.count(), cloned.set.bindings.count());
try testing.expectEqual(keybinds.tables.count(), cloned.tables.count());
try testing.expectEqual(
keybinds.tables.get("foo").?.bindings.count(),
cloned.tables.get("foo").?.bindings.count(),
);
try testing.expectEqual(
keybinds.tables.get("bar").?.bindings.count(),
cloned.tables.get("bar").?.bindings.count(),
);
}
test "equal with tables" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds1: Keybinds = .{};
try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{};
try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
try testing.expect(keybinds1.equal(keybinds2));
}
test "equal with tables different table count" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds1: Keybinds = .{};
try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{};
try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
try keybinds2.parseCLI(alloc, "bar/shift+b=paste_from_clipboard");
try testing.expect(!keybinds1.equal(keybinds2));
}
test "equal with tables different table names" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds1: Keybinds = .{};
try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{};
try keybinds2.parseCLI(alloc, "bar/shift+a=copy_to_clipboard");
try testing.expect(!keybinds1.equal(keybinds2));
}
test "equal with tables different bindings" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds1: Keybinds = .{};
try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{};
try keybinds2.parseCLI(alloc, "foo/shift+b=paste_from_clipboard");
try testing.expect(!keybinds1.equal(keybinds2));
}
test "formatEntry with tables" {
const testing = std.testing;
var buf: std.Io.Writer.Allocating = .init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello");
try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer));
try testing.expectEqualStrings("keybind = foo/shift+a=csi:hello\n", buf.written());
}
test "formatEntry with tables and root set" {
const testing = std.testing;
var buf: std.Io.Writer.Allocating = .init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "shift+b=csi:world");
try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello");
try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer));
const output = buf.written();
try testing.expect(std.mem.indexOf(u8, output, "keybind = shift+b=csi:world\n") != null);
try testing.expect(std.mem.indexOf(u8, output, "keybind = foo/shift+a=csi:hello\n") != null);
}
};
/// See "font-codepoint-map" for documentation.