From 2800a468546b1068da6f518dd93b011a4d2e6f60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 09:31:14 -0700 Subject: [PATCH] keybind parsing in CLI args --- src/config.zig | 74 +++++++++++++++++++++++++++++++++++++++++++ src/input/Binding.zig | 16 +++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/config.zig b/src/config.zig index 0b68542e4..8e24814f0 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; +const inputpkg = @import("input.zig"); /// Config is the main config struct. These fields map directly to the /// CLI flag names hence we use a lot of `@""` syntax to support hyphens. @@ -21,6 +22,37 @@ pub const Config = struct { /// it'll be looked up in the PATH. command: ?[]const u8 = null, + /// Key bindings. The format is "trigger=action". Duplicate triggers + /// will overwrite previously set values. + /// + /// Trigger: "+"-separated list of keys and modifiers. Example: + /// "ctrl+a", "ctrl+shift+b", "up". Some notes: + /// + /// - modifiers cannot repeat, "ctrl+ctrl+a" is invalid. + /// - modifers and key scan be in any order, "shift+a+ctrl" is weird, + /// but valid. + /// - only a single key input is allowed, "ctrl+a+b" is invalid. + /// + /// Action is the action to take when the trigger is satisfied. It takes + /// the format "action" or "action:param". The latter form is only valid + /// if the action requires a parameter. + /// + /// - "ignore" - Do nothing, ignore the key input. This can be used to + /// black hole certain inputs to have no effect. + /// - "unbind" - Remove the binding. This makes it so the previous action + /// is removed, and the key will be sent through to the child command + /// if it is printable. + /// - "csi:text" - Send a CSI sequence. i.e. "csi:A" sends "cursor up". + /// + /// Some notes for the action: + /// + /// - The parameter is taken as-is after the ":". Double quotes or + /// other mechanisms are included and NOT parsed. If you want to + /// send a string value that includes spaces, wrap the entire + /// trigger/action in double quotes. Example: --keybind="up=csi:A B" + /// + keybind: Keybinds = .{}, + /// Additional configuration files to read. @"config-file": RepeatableString = .{}, @@ -114,6 +146,48 @@ pub const RepeatableString = struct { } }; +/// Stores a set of keybinds. +pub const Keybinds = struct { + set: inputpkg.Binding.Set = .{}, + + pub fn parseCLI(self: *Keybinds, alloc: Allocator, input: ?[]const u8) !void { + var copy: ?[]u8 = null; + var value = value: { + const value = input orelse return error.ValueRequired; + + // If we don't have a colon, use the value as-is, no copy + if (std.mem.indexOf(u8, value, ":") == null) + break :value value; + + // If we have a colon, we copy the whole value for now. We could + // do this more efficiently later if we wanted to. + const buf = try alloc.alloc(u8, value.len); + copy = buf; + + std.mem.copy(u8, buf, value); + break :value buf; + }; + errdefer if (copy) |v| alloc.free(v); + + const binding = try inputpkg.Binding.parse(value); + switch (binding.action) { + .unbind => self.set.remove(binding.trigger), + else => try self.set.put(alloc, binding.trigger, binding.action), + } + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var set: Keybinds = .{}; + try set.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try set.parseCLI(alloc, "shift+a=csi:hello"); + } +}; + test { std.testing.refAllDecls(@This()); } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 4fdd0528e..be59c60f7 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -4,6 +4,7 @@ const Binding = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const assert = std.debug.assert; const key = @import("key.zig"); /// The trigger that needs to be performed to execute the action. @@ -127,6 +128,11 @@ pub const Action = union(enum) { /// just black hole it. ignore: void, + /// This action is used to flag that the binding should be removed + /// from the set. This should never exist in an active set and + /// `set.put` has an assertion to verify this. + unbind: Void, + /// Send a CSI sequence. The value should be the CSI sequence /// without the CSI header ("ESC ]" or "\x1b]"). csi: []const u8, @@ -164,7 +170,10 @@ pub const Set = struct { /// Add a binding to the set. If the binding already exists then /// this will overwrite it. - pub fn put(self: Set, alloc: Allocator, t: Trigger, action: Action) !void { + pub fn put(self: *Set, alloc: Allocator, t: Trigger, action: Action) !void { + // unbind should never go into the set, it should be handled prior + assert(action != .unbind); + try self.bindings.put(alloc, t, action); } @@ -172,6 +181,11 @@ pub const Set = struct { pub fn get(self: Set, t: Trigger) ?Action { return self.bindings.get(t); } + + /// Remove a binding for a given trigger. + pub fn remove(self: *Set, t: Trigger) void { + _ = self.bindings.remove(t); + } }; test "parse: triggers" {