diff --git a/src/Window.zig b/src/Window.zig index 71983b158..c317d4d35 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -486,6 +486,7 @@ fn keyCallback( const tracy = trace(@src()); defer tracy.end(); + //log.info("KEY {} {} {} {}", .{ key, scancode, mods, action }); _ = scancode; if (action == .press and mods.super) { @@ -542,7 +543,6 @@ fn keyCallback( } } - //log.info("KEY {} {} {} {}", .{ key, scancode, mods, action }); if (action == .press or action == .repeat) { const c: u8 = switch (key) { // Lots more of these: diff --git a/src/key.zig b/src/key.zig new file mode 100644 index 000000000..8c5bb7a59 --- /dev/null +++ b/src/key.zig @@ -0,0 +1,205 @@ +const std = @import("std"); + +/// A single binding. +pub const Binding = struct { + /// The key that has to be pressed for this binding to take action. + key: Key = .invalid, + + /// The key modifiers that must be active for this to match. + mods: Mods = .{}, + + /// The action to take if this binding matches + action: Action, + + pub const Error = error{ + InvalidFormat, + }; + + /// Parse the format "ctrl+a=csi:A" into a binding. The format is + /// specifically "trigger=action". Trigger is a "+"-delimited series of + /// modifiers and keys. Action is the action name and optionally a + /// parameter after a colon, i.e. "csi:A" or "ignore". + pub fn parse(input: []const u8) !Binding { + // Find the first = which splits are mapping into the trigger + // and action, respectively. + const eqlIdx = std.mem.indexOf(u8, input, "=") orelse return Error.InvalidFormat; + + // Accumulator for our result + var result: Binding = .{ .action = undefined }; + + // Determine our trigger conditions by parsing the part before + // the "=", i.e. "ctrl+shift+a" or "a" + var iter = std.mem.tokenize(u8, input[0..eqlIdx], "+"); + trigger: while (iter.next()) |part| { + // All parts must be non-empty + if (part.len == 0) return Error.InvalidFormat; + + // Check if its a modifier + const modsInfo = @typeInfo(Mods).Struct; + inline for (modsInfo.fields) |field| { + if (field.field_type == bool) { + if (std.mem.eql(u8, part, field.name)) { + // Repeat not allowed + if (@field(result.mods, field.name)) return Error.InvalidFormat; + + @field(result.mods, field.name) = true; + continue :trigger; + } + } + } + + // Check if its a key + const keysInfo = @typeInfo(Key).Enum; + inline for (keysInfo.fields) |field| { + if (!std.mem.eql(u8, field.name, "invalid")) { + if (std.mem.eql(u8, part, field.name)) { + // Repeat not allowed + if (result.key != .invalid) return Error.InvalidFormat; + + result.key = @field(Key, field.name); + continue :trigger; + } + } + } + + // We didn't recognize this value + return Error.InvalidFormat; + } + + // Split our action by colon. A colon may not exist for some + // actions so it is optional. The part preceding the colon is the + // action name. + const actionRaw = input[eqlIdx + 1 ..]; + const colonIdx = std.mem.indexOf(u8, actionRaw, ":"); + const action = actionRaw[0..(colonIdx orelse actionRaw.len)]; + + // An action name is always required + if (action.len == 0) return Error.InvalidFormat; + + // Find a matching action + const actionInfo = @typeInfo(Action).Union; + inline for (actionInfo.fields) |field| { + if (std.mem.eql(u8, action, field.name)) { + // If the field type is void we expect no value + if (field.field_type == void) { + if (colonIdx != null) return Error.InvalidFormat; + result.action = @unionInit(Action, field.name, {}); + } + } + } + + return result; + } + + test "parse: triggers" { + const testing = std.testing; + + // single character + try testing.expectEqual( + Binding{ .key = .a, .action = .{ .ignore = {} } }, + try parse("a=ignore"), + ); + + // single modifier + try testing.expectEqual(Binding{ + .mods = .{ .shift = true }, + .key = .a, + .action = .{ .ignore = {} }, + }, try parse("shift+a=ignore")); + try testing.expectEqual(Binding{ + .mods = .{ .ctrl = true }, + .key = .a, + .action = .{ .ignore = {} }, + }, try parse("ctrl+a=ignore")); + + // multiple modifier + try testing.expectEqual(Binding{ + .mods = .{ .shift = true, .ctrl = true }, + .key = .a, + .action = .{ .ignore = {} }, + }, try parse("shift+ctrl+a=ignore")); + + // key can come before modifier + try testing.expectEqual(Binding{ + .mods = .{ .shift = true }, + .key = .a, + .action = .{ .ignore = {} }, + }, try parse("a+shift=ignore")); + + // invalid key + try testing.expectError(Error.InvalidFormat, parse("foo=ignore")); + + // repeated control + try testing.expectError(Error.InvalidFormat, parse("shift+shift+a=ignore")); + + // multiple character + try testing.expectError(Error.InvalidFormat, parse("a+b=ignore")); + } +}; + +/// The set of actions that a keybinding can take. +pub const Action = union(enum) { + /// Ignore this key combination, don't send it to the child process, + /// just black hole it. + ignore: void, + + /// Send a CSI sequence. The value should be the CSI sequence + /// without the CSI header ("ESC ]" or "\x1b]"). + csi: []const u8, +}; + +/// A bitmask for all key modifiers. This is taken directly from the +/// GLFW representation, but we use this generically. +pub const Mods = packed struct { + shift: bool = false, + ctrl: bool = false, + alt: bool = false, + super: bool = false, + caps_lock: bool = false, + num_lock: bool = false, + _padding: u2 = 0, +}; + +/// The set of keys that can map to keybindings. These have no fixed enum +/// values because we map platform-specific keys to this set. Note that +/// this only needs to accomodate what maps to a key. If a key is not bound +/// to anything and the key can be mapped to a printable character, then that +/// unicode character is sent directly to the pty. +pub const Key = enum { + invalid, + + // a-z + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + u, + v, + w, + x, + y, + z, + + // To support more keys (there are obviously more!) add them here + // and ensure the mapping is up to date in the Window key handler. +}; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/main.zig b/src/main.zig index 748d4f85d..fcd839cc1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -110,4 +110,5 @@ test { // TODO _ = @import("config.zig"); _ = @import("cli_args.zig"); + _ = @import("key.zig"); }