From 7303909d0143f4f762eb4e07a89ab77ff0c607ce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Aug 2022 17:40:36 -0700 Subject: [PATCH 01/10] key.Binding and basic parsing --- src/Window.zig | 2 +- src/key.zig | 205 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 1 + 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/key.zig 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"); } From bc9f81e8d2d1302d0530d351025b305d295fb81c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Aug 2022 17:48:52 -0700 Subject: [PATCH 02/10] binding parse action with parameter --- src/key.zig | 74 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/src/key.zig b/src/key.zig index 8c5bb7a59..352f0c498 100644 --- a/src/key.zig +++ b/src/key.zig @@ -20,6 +20,9 @@ pub const Binding = struct { /// 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 { + // NOTE(mitchellh): This is not the most efficient way to do any + // of this, I welcome any improvements here! + // Find the first = which splits are mapping into the trigger // and action, respectively. const eqlIdx = std.mem.indexOf(u8, input, "=") orelse return Error.InvalidFormat; @@ -66,27 +69,41 @@ pub const Binding = struct { 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, {}); + result.action = action: { + // 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; + + 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 + switch (field.field_type) { + void => { + if (colonIdx != null) return Error.InvalidFormat; + break :action @unionInit(Action, field.name, {}); + }, + + []const u8 => { + const idx = colonIdx orelse return Error.InvalidFormat; + const param = actionRaw[idx + 1 ..]; + break :action @unionInit(Action, field.name, param); + }, + + else => unreachable, + } } } - } + + return Error.InvalidFormat; + }; return result; } @@ -135,6 +152,27 @@ pub const Binding = struct { // multiple character try testing.expectError(Error.InvalidFormat, parse("a+b=ignore")); } + + test "parse: action" { + const testing = std.testing; + + // invalid action + try testing.expectError(Error.InvalidFormat, parse("a=nopenopenope")); + + // no parameters + try testing.expectEqual( + Binding{ .key = .a, .action = .{ .ignore = {} } }, + try parse("a=ignore"), + ); + try testing.expectError(Error.InvalidFormat, parse("a=ignore:A")); + + // parameter + { + const binding = try parse("a=csi:A"); + try testing.expect(binding.action == .csi); + try testing.expectEqualStrings("A", binding.action.csi); + } + } }; /// The set of actions that a keybinding can take. From 222f70857a0b0670c2b84e8809e0592af4e1b51d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Aug 2022 19:52:14 -0700 Subject: [PATCH 03/10] move input stuff to src/input --- src/input.zig | 8 ++ src/input/Binding.zig | 252 ++++++++++++++++++++++++++++++++++++++++++ src/input/key.zig | 54 +++++++++ src/key.zig | 243 ---------------------------------------- src/main.zig | 2 +- 5 files changed, 315 insertions(+), 244 deletions(-) create mode 100644 src/input.zig create mode 100644 src/input/Binding.zig create mode 100644 src/input/key.zig delete mode 100644 src/key.zig diff --git a/src/input.zig b/src/input.zig new file mode 100644 index 000000000..14efd40cc --- /dev/null +++ b/src/input.zig @@ -0,0 +1,8 @@ +const std = @import("std"); + +pub usingnamespace @import("input/key.zig"); +pub const Binding = @import("input/Binding.zig"); + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/input/Binding.zig b/src/input/Binding.zig new file mode 100644 index 000000000..4fdd0528e --- /dev/null +++ b/src/input/Binding.zig @@ -0,0 +1,252 @@ +//! A binding maps some input trigger to an action. When the trigger +//! occurs, the action is performed. +const Binding = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const key = @import("key.zig"); + +/// The trigger that needs to be performed to execute the action. +trigger: Trigger, + +/// 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 { + // NOTE(mitchellh): This is not the most efficient way to do any + // of this, I welcome any improvements here! + + // Find the first = which splits are mapping into the trigger + // and action, respectively. + const eqlIdx = std.mem.indexOf(u8, input, "=") orelse return Error.InvalidFormat; + + // Determine our trigger conditions by parsing the part before + // the "=", i.e. "ctrl+shift+a" or "a" + const trigger = trigger: { + var result: Trigger = .{}; + var iter = std.mem.tokenize(u8, input[0..eqlIdx], "+"); + loop: 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(key.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 :loop; + } + } + } + + // Check if its a key + const keysInfo = @typeInfo(key.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.Key, field.name); + continue :loop; + } + } + } + + // We didn't recognize this value + return Error.InvalidFormat; + } + + break :trigger result; + }; + + // Find a matching action + const action: Action = action: { + // 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; + + 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 + switch (field.field_type) { + void => { + if (colonIdx != null) return Error.InvalidFormat; + break :action @unionInit(Action, field.name, {}); + }, + + // see note about what Void is + Action.Void => { + if (colonIdx != null) return Error.InvalidFormat; + break :action @unionInit(Action, field.name, 0); + }, + + []const u8 => { + const idx = colonIdx orelse return Error.InvalidFormat; + const param = actionRaw[idx + 1 ..]; + break :action @unionInit(Action, field.name, param); + }, + + else => unreachable, + } + } + } + + return Error.InvalidFormat; + }; + + return Binding{ .trigger = trigger, .action = action }; +} + +/// The set of actions that a keybinding can take. +pub const Action = union(enum) { + // stage1 compiler bug where if this is "void" then it crashes the + // compiler. TODO: check this out when we upgrade to stage2. + const Void = u0; + + /// 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, + + /// Copy and paste. + copy_to_clipboard: Void, + paste_from_clipboard: Void, +}; + +/// Trigger is the associated key state that can trigger an action. +pub const Trigger = struct { + /// The key that has to be pressed for a binding to take action. + key: key.Key = .invalid, + + /// The key modifiers that must be active for this to match. + mods: key.Mods = .{}, + + /// Returns a hash code that can be used to uniquely identify this trigger. + pub fn hash(self: Binding) u64 { + var hasher = std.hash.Wyhash.init(0); + std.hash.autoHash(&hasher, self.key); + std.hash.autoHash(&hasher, self.mods); + return hasher.final(); + } +}; + +/// A structure that contains a set of bindings and focuses on fast lookup. +/// The use case is that this will be called on EVERY key input to look +/// for an associated action so it must be fast. +pub const Set = struct { + const HashMap = std.AutoHashMapUnmanaged(Trigger, Action); + + /// The set of bindings. + bindings: HashMap = .{}, + + /// 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 { + try self.bindings.put(alloc, t, action); + } + + /// Get a binding for a given trigger. + pub fn get(self: Set, t: Trigger) ?Action { + return self.bindings.get(t); + } +}; + +test "parse: triggers" { + const testing = std.testing; + + // single character + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .a }, + .action = .{ .ignore = {} }, + }, + try parse("a=ignore"), + ); + + // single modifier + try testing.expectEqual(Binding{ + .trigger = .{ + .mods = .{ .shift = true }, + .key = .a, + }, + .action = .{ .ignore = {} }, + }, try parse("shift+a=ignore")); + try testing.expectEqual(Binding{ + .trigger = .{ + .mods = .{ .ctrl = true }, + .key = .a, + }, + .action = .{ .ignore = {} }, + }, try parse("ctrl+a=ignore")); + + // multiple modifier + try testing.expectEqual(Binding{ + .trigger = .{ + .mods = .{ .shift = true, .ctrl = true }, + .key = .a, + }, + .action = .{ .ignore = {} }, + }, try parse("shift+ctrl+a=ignore")); + + // key can come before modifier + try testing.expectEqual(Binding{ + .trigger = .{ + .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")); +} + +test "parse: action" { + const testing = std.testing; + + // invalid action + try testing.expectError(Error.InvalidFormat, parse("a=nopenopenope")); + + // no parameters + try testing.expectEqual( + Binding{ .trigger = .{ .key = .a }, .action = .{ .ignore = {} } }, + try parse("a=ignore"), + ); + try testing.expectError(Error.InvalidFormat, parse("a=ignore:A")); + + // parameter + { + const binding = try parse("a=csi:A"); + try testing.expect(binding.action == .csi); + try testing.expectEqualStrings("A", binding.action.csi); + } +} diff --git a/src/input/key.zig b/src/input/key.zig new file mode 100644 index 000000000..dc03b72e1 --- /dev/null +++ b/src/input/key.zig @@ -0,0 +1,54 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// 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. +}; diff --git a/src/key.zig b/src/key.zig deleted file mode 100644 index 352f0c498..000000000 --- a/src/key.zig +++ /dev/null @@ -1,243 +0,0 @@ -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 { - // NOTE(mitchellh): This is not the most efficient way to do any - // of this, I welcome any improvements here! - - // 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; - } - - // Find a matching action - result.action = action: { - // 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; - - 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 - switch (field.field_type) { - void => { - if (colonIdx != null) return Error.InvalidFormat; - break :action @unionInit(Action, field.name, {}); - }, - - []const u8 => { - const idx = colonIdx orelse return Error.InvalidFormat; - const param = actionRaw[idx + 1 ..]; - break :action @unionInit(Action, field.name, param); - }, - - else => unreachable, - } - } - } - - return Error.InvalidFormat; - }; - - 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")); - } - - test "parse: action" { - const testing = std.testing; - - // invalid action - try testing.expectError(Error.InvalidFormat, parse("a=nopenopenope")); - - // no parameters - try testing.expectEqual( - Binding{ .key = .a, .action = .{ .ignore = {} } }, - try parse("a=ignore"), - ); - try testing.expectError(Error.InvalidFormat, parse("a=ignore:A")); - - // parameter - { - const binding = try parse("a=csi:A"); - try testing.expect(binding.action == .csi); - try testing.expectEqualStrings("A", binding.action.csi); - } - } -}; - -/// 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 fcd839cc1..282375cda 100644 --- a/src/main.zig +++ b/src/main.zig @@ -110,5 +110,5 @@ test { // TODO _ = @import("config.zig"); _ = @import("cli_args.zig"); - _ = @import("key.zig"); + _ = @import("input.zig"); } From 2800a468546b1068da6f518dd93b011a4d2e6f60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 09:31:14 -0700 Subject: [PATCH 04/10] 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" { From 622537d66591deca973f9a5664739ebb3240be92 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 10:02:13 -0700 Subject: [PATCH 05/10] look up keybindings on keypress, clean up how non-printables are handled --- src/Window.zig | 136 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 38 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index c317d4d35..13cdfa079 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -20,6 +20,7 @@ const trace = @import("tracy").trace; const max_timer = @import("max_timer.zig"); const terminal = @import("terminal/main.zig"); const Config = @import("config.zig").Config; +const input = @import("input.zig"); const RenderTimer = max_timer.MaxTimer(renderTimerCallback); @@ -486,15 +487,69 @@ fn keyCallback( const tracy = trace(@src()); defer tracy.end(); + const win = window.getUserPointer(Window) orelse return; + + // Reset the ignore char setting. If we didn't handle the char + // by here, we aren't going to get it so we just reset this. + win.ignore_char = false; + //log.info("KEY {} {} {} {}", .{ key, scancode, mods, action }); _ = scancode; + if (action == .press or action == .repeat) { + // Convert our glfw input into a platform agnostic trigger. When we + // extract the platform out of this file, we'll pull a lot of this out + // into a function. For now, this is the only place we do it so we just + // put it right here. + const trigger: input.Binding.Trigger = .{ + .mods = @bitCast(input.Mods, mods), + .key = switch (key) { + .a => .a, + .b => .b, + .c => .c, + .d => .d, + .e => .e, + .f => .f, + .g => .g, + .h => .h, + .i => .i, + .j => .j, + .k => .k, + .l => .l, + .m => .m, + .n => .n, + .o => .o, + .p => .p, + .q => .q, + .r => .r, + .s => .s, + .t => .t, + .u => .u, + .v => .v, + .w => .w, + .x => .x, + .y => .y, + .z => .z, + else => .invalid, + }, + }; + + if (win.config.keybind.set.get(trigger)) |binding_action| { + log.warn("BINDING ACTION={}", .{binding_action}); + _ = binding_action; + + // Bindings always result in us ignoring the char if printable + win.ignore_char = true; + + // No matter what, if there is a binding then we are done. + return; + } + } + if (action == .press and mods.super) { switch (key) { // Copy .c => { - const win = window.getUserPointer(Window) orelse return; - // Ignore this character for writing win.ignore_char = true; @@ -517,7 +572,6 @@ fn keyCallback( // Paste .v => { - const win = window.getUserPointer(Window) orelse return; // Ignore this character for writing win.ignore_char = true; @@ -543,46 +597,52 @@ fn keyCallback( } } - if (action == .press or action == .repeat) { - const c: u8 = switch (key) { - // Lots more of these: - // https://www.physics.udel.edu/~watson/scen103/ascii.html - .a => if (mods.control and !mods.shift) 0x01 else return, - .b => if (mods.control and !mods.shift) 0x02 else return, - .c => if (mods.control and !mods.shift) 0x03 else return, - .d => if (mods.control and !mods.shift) 0x04 else return, - .e => if (mods.control and !mods.shift) 0x05 else return, - .f => if (mods.control and !mods.shift) 0x06 else return, - .g => if (mods.control and !mods.shift) 0x07 else return, - .h => if (mods.control and !mods.shift) 0x08 else return, - .i => if (mods.control and !mods.shift) 0x09 else return, - .j => if (mods.control and !mods.shift) 0x0A else return, - .k => if (mods.control and !mods.shift) 0x0B else return, - .l => if (mods.control and !mods.shift) 0x0C else return, - .m => if (mods.control and !mods.shift) 0x0D else return, - .n => if (mods.control and !mods.shift) 0x0E else return, - .o => if (mods.control and !mods.shift) 0x0F else return, - .p => if (mods.control and !mods.shift) 0x10 else return, - .q => if (mods.control and !mods.shift) 0x11 else return, - .r => if (mods.control and !mods.shift) 0x12 else return, - .s => if (mods.control and !mods.shift) 0x13 else return, - .t => if (mods.control and !mods.shift) 0x14 else return, - .u => if (mods.control and !mods.shift) 0x15 else return, - .v => if (mods.control and !mods.shift) 0x16 else return, - .w => if (mods.control and !mods.shift) 0x17 else return, - .x => if (mods.control and !mods.shift) 0x18 else return, - .y => if (mods.control and !mods.shift) 0x19 else return, - .z => if (mods.control and !mods.shift) 0x1A else return, - + // Handle non-printable characters + const char: u8 = switch (@bitCast(u8, mods)) { + // No modifiers pressed at all + 0 => @as(u8, switch (key) { .backspace => 0x08, .enter => '\r', .tab => '\t', .escape => 0x1B, - else => return, - }; + else => 0, + }), - const win = window.getUserPointer(Window) orelse return; - win.queueWrite(&[1]u8{c}) catch |err| + // Control only + @bitCast(u8, glfw.Mods{ .control = true }) => @as(u8, switch (key) { + .a => 0x01, + .b => 0x02, + .c => 0x03, + .d => 0x04, + .e => 0x05, + .f => 0x06, + .g => 0x07, + .h => 0x08, + .i => 0x09, + .j => 0x0A, + .k => 0x0B, + .l => 0x0C, + .m => 0x0D, + .n => 0x0E, + .o => 0x0F, + .p => 0x10, + .q => 0x11, + .r => 0x12, + .s => 0x13, + .t => 0x14, + .u => 0x15, + .v => 0x16, + .w => 0x17, + .x => 0x18, + .y => 0x19, + .z => 0x1A, + else => 0, + }), + + else => 0, + }; + if (char > 0) { + win.queueWrite(&[1]u8{char}) catch |err| log.err("error queueing write in keyCallback err={}", .{err}); } } From b33268cee3eea4a5d1437c194e3a1cd79d945f0d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 10:18:05 -0700 Subject: [PATCH 06/10] only handle non-printables on press/release, and BS is 0x7F --- src/Window.zig | 90 ++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 13cdfa079..364a6c74c 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -598,52 +598,54 @@ fn keyCallback( } // Handle non-printable characters - const char: u8 = switch (@bitCast(u8, mods)) { - // No modifiers pressed at all - 0 => @as(u8, switch (key) { - .backspace => 0x08, - .enter => '\r', - .tab => '\t', - .escape => 0x1B, - else => 0, - }), + if (action == .press or action == .repeat) { + const char: u8 = switch (@bitCast(u8, mods)) { + // No modifiers pressed at all + 0 => @as(u8, switch (key) { + .backspace => 0x7F, + .enter => '\r', + .tab => '\t', + .escape => 0x1B, + else => 0, + }), - // Control only - @bitCast(u8, glfw.Mods{ .control = true }) => @as(u8, switch (key) { - .a => 0x01, - .b => 0x02, - .c => 0x03, - .d => 0x04, - .e => 0x05, - .f => 0x06, - .g => 0x07, - .h => 0x08, - .i => 0x09, - .j => 0x0A, - .k => 0x0B, - .l => 0x0C, - .m => 0x0D, - .n => 0x0E, - .o => 0x0F, - .p => 0x10, - .q => 0x11, - .r => 0x12, - .s => 0x13, - .t => 0x14, - .u => 0x15, - .v => 0x16, - .w => 0x17, - .x => 0x18, - .y => 0x19, - .z => 0x1A, - else => 0, - }), + // Control only + @bitCast(u8, glfw.Mods{ .control = true }) => @as(u8, switch (key) { + .a => 0x01, + .b => 0x02, + .c => 0x03, + .d => 0x04, + .e => 0x05, + .f => 0x06, + .g => 0x07, + .h => 0x08, + .i => 0x09, + .j => 0x0A, + .k => 0x0B, + .l => 0x0C, + .m => 0x0D, + .n => 0x0E, + .o => 0x0F, + .p => 0x10, + .q => 0x11, + .r => 0x12, + .s => 0x13, + .t => 0x14, + .u => 0x15, + .v => 0x16, + .w => 0x17, + .x => 0x18, + .y => 0x19, + .z => 0x1A, + else => 0, + }), - else => 0, - }; - if (char > 0) { - win.queueWrite(&[1]u8{char}) catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); + else => 0, + }; + if (char > 0) { + win.queueWrite(&[1]u8{char}) catch |err| + log.err("error queueing write in keyCallback err={}", .{err}); + } } } From e6f09093ddbbfd24b10de30c667c9b1611222a46 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 10:21:27 -0700 Subject: [PATCH 07/10] unify non-printables up by bindings --- src/Window.zig | 100 ++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 364a6c74c..6a91e9ccf 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -544,6 +544,55 @@ fn keyCallback( // No matter what, if there is a binding then we are done. return; } + + // Handle non-printables + const char: u8 = switch (@bitCast(u8, mods)) { + // No modifiers pressed at all + 0 => @as(u8, switch (key) { + .backspace => 0x7F, + .enter => '\r', + .tab => '\t', + .escape => 0x1B, + else => 0, + }), + + // Control only + @bitCast(u8, glfw.Mods{ .control = true }) => @as(u8, switch (key) { + .a => 0x01, + .b => 0x02, + .c => 0x03, + .d => 0x04, + .e => 0x05, + .f => 0x06, + .g => 0x07, + .h => 0x08, + .i => 0x09, + .j => 0x0A, + .k => 0x0B, + .l => 0x0C, + .m => 0x0D, + .n => 0x0E, + .o => 0x0F, + .p => 0x10, + .q => 0x11, + .r => 0x12, + .s => 0x13, + .t => 0x14, + .u => 0x15, + .v => 0x16, + .w => 0x17, + .x => 0x18, + .y => 0x19, + .z => 0x1A, + else => 0, + }), + + else => 0, + }; + if (char > 0) { + win.queueWrite(&[1]u8{char}) catch |err| + log.err("error queueing write in keyCallback err={}", .{err}); + } } if (action == .press and mods.super) { @@ -596,57 +645,6 @@ fn keyCallback( else => {}, } } - - // Handle non-printable characters - if (action == .press or action == .repeat) { - const char: u8 = switch (@bitCast(u8, mods)) { - // No modifiers pressed at all - 0 => @as(u8, switch (key) { - .backspace => 0x7F, - .enter => '\r', - .tab => '\t', - .escape => 0x1B, - else => 0, - }), - - // Control only - @bitCast(u8, glfw.Mods{ .control = true }) => @as(u8, switch (key) { - .a => 0x01, - .b => 0x02, - .c => 0x03, - .d => 0x04, - .e => 0x05, - .f => 0x06, - .g => 0x07, - .h => 0x08, - .i => 0x09, - .j => 0x0A, - .k => 0x0B, - .l => 0x0C, - .m => 0x0D, - .n => 0x0E, - .o => 0x0F, - .p => 0x10, - .q => 0x11, - .r => 0x12, - .s => 0x13, - .t => 0x14, - .u => 0x15, - .v => 0x16, - .w => 0x17, - .x => 0x18, - .y => 0x19, - .z => 0x1A, - else => 0, - }), - - else => 0, - }; - if (char > 0) { - win.queueWrite(&[1]u8{char}) catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); - } - } } fn focusCallback(window: glfw.Window, focused: bool) void { From 80376ce6da94e44d6d7e62ebc083ff32ae1b30ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 11:08:39 -0700 Subject: [PATCH 08/10] hook up keybindings for copy/paste and arrow keys --- src/Window.zig | 101 ++++++++++++++++++++++------------------------ src/config.zig | 34 +++++++++++++++- src/input/key.zig | 6 +++ src/main.zig | 3 +- 4 files changed, 89 insertions(+), 55 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 6a91e9ccf..b2c98a30a 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -530,13 +530,59 @@ fn keyCallback( .x => .x, .y => .y, .z => .z, + .up => .up, + .down => .down, + .right => .right, + .left => .left, else => .invalid, }, }; if (win.config.keybind.set.get(trigger)) |binding_action| { - log.warn("BINDING ACTION={}", .{binding_action}); - _ = binding_action; + //log.warn("BINDING ACTION={}", .{binding_action}); + + switch (binding_action) { + .unbind => unreachable, + .ignore => {}, + + .csi => |data| { + win.queueWrite("\x1B[") catch |err| + log.err("error queueing write in keyCallback err={}", .{err}); + win.queueWrite(data) catch |err| + log.warn("error pasting clipboard: {}", .{err}); + }, + + .copy_to_clipboard => { + if (win.terminal.selection) |sel| { + var buf = win.terminal.screen.selectionString(win.alloc, sel) catch |err| { + log.err("error reading selection string err={}", .{err}); + return; + }; + defer win.alloc.free(buf); + + glfw.setClipboardString(buf) catch |err| { + log.err("error setting clipboard string err={}", .{err}); + return; + }; + } + }, + + .paste_from_clipboard => { + const data = glfw.getClipboardString() catch |err| { + log.warn("error reading clipboard: {}", .{err}); + return; + }; + + if (data.len > 0) { + if (win.bracketed_paste) win.queueWrite("\x1B[200~") catch |err| + log.err("error queueing write in keyCallback err={}", .{err}); + win.queueWrite(data) catch |err| + log.warn("error pasting clipboard: {}", .{err}); + if (win.bracketed_paste) win.queueWrite("\x1B[201~") catch |err| + log.err("error queueing write in keyCallback err={}", .{err}); + } + }, + } // Bindings always result in us ignoring the char if printable win.ignore_char = true; @@ -594,57 +640,6 @@ fn keyCallback( log.err("error queueing write in keyCallback err={}", .{err}); } } - - if (action == .press and mods.super) { - switch (key) { - // Copy - .c => { - // Ignore this character for writing - win.ignore_char = true; - - // If we have a selection, copy it. - if (win.terminal.selection) |sel| { - var buf = win.terminal.screen.selectionString(win.alloc, sel) catch |err| { - log.err("error reading selection string err={}", .{err}); - return; - }; - defer win.alloc.free(buf); - - glfw.setClipboardString(buf) catch |err| { - log.err("error setting clipboard string err={}", .{err}); - return; - }; - } - - return; - }, - - // Paste - .v => { - - // Ignore this character for writing - win.ignore_char = true; - - const data = glfw.getClipboardString() catch |err| { - log.warn("error reading clipboard: {}", .{err}); - return; - }; - - if (data.len > 0) { - if (win.bracketed_paste) win.queueWrite("\x1B[200~") catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); - win.queueWrite(data) catch |err| - log.warn("error pasting clipboard: {}", .{err}); - if (win.bracketed_paste) win.queueWrite("\x1B[201~") catch |err| - log.err("error queueing write in keyCallback err={}", .{err}); - } - - return; - }, - - else => {}, - } - } } fn focusCallback(window: glfw.Window, focused: bool) void { diff --git a/src/config.zig b/src/config.zig index 8e24814f0..87f773d3f 100644 --- a/src/config.zig +++ b/src/config.zig @@ -20,7 +20,7 @@ pub const Config = struct { /// The command to run, usually a shell. If this is not an absolute path, /// it'll be looked up in the PATH. - command: ?[]const u8 = null, + command: ?[]const u8, /// Key bindings. The format is "trigger=action". Duplicate triggers /// will overwrite previously set values. @@ -63,6 +63,38 @@ pub const Config = struct { if (self._arena) |arena| arena.deinit(); self.* = undefined; } + + pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Build up our basic config + var result: Config = .{ + ._arena = arena, + .command = "sh", + }; + + // Add our default keybindings + try result.keybind.set.put( + alloc, + .{ .key = .c, .mods = .{ .super = true } }, + .{ .copy_to_clipboard = 0 }, + ); + + try result.keybind.set.put( + alloc, + .{ .key = .v, .mods = .{ .super = true } }, + .{ .paste_from_clipboard = 0 }, + ); + + try result.keybind.set.put(alloc, .{ .key = .up }, .{ .csi = "A" }); + try result.keybind.set.put(alloc, .{ .key = .down }, .{ .csi = "B" }); + try result.keybind.set.put(alloc, .{ .key = .right }, .{ .csi = "C" }); + try result.keybind.set.put(alloc, .{ .key = .left }, .{ .csi = "D" }); + + return result; + } }; /// Color represents a color using RGB. diff --git a/src/input/key.zig b/src/input/key.zig index dc03b72e1..940e7b570 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -49,6 +49,12 @@ pub const Key = enum { y, z, + // control + up, + down, + right, + left, + // To support more keys (there are obviously more!) add them here // and ensure the mapping is up to date in the Window key handler. }; diff --git a/src/main.zig b/src/main.zig index 282375cda..24fd0d8db 100644 --- a/src/main.zig +++ b/src/main.zig @@ -39,7 +39,8 @@ pub fn main() !void { // Parse the config from the CLI args var config = config: { - var result: Config = .{}; + var result = try Config.default(alloc); + errdefer result.deinit(); var iter = try std.process.argsWithAllocator(alloc); defer iter.deinit(); try cli_args.parse(Config, alloc, &result, &iter); From 3b5a9caff5fbe1dc0d48190fd66c5e7a872ed5c4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 11:16:36 -0700 Subject: [PATCH 09/10] hook up more control keys: home, end, page up, page down --- TODO.md | 2 -- src/Window.zig | 4 ++++ src/config.zig | 4 ++++ src/input/key.zig | 4 ++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 1b0d10762..87902f83d 100644 --- a/TODO.md +++ b/TODO.md @@ -27,8 +27,6 @@ Improvements: * double-click to select a word * triple-click to select a line * shift-click and drag to continue selection -* arrow keys do nothing, should send proper codes -* home/end should scroll to top/bottom of scrollback Major Features: diff --git a/src/Window.zig b/src/Window.zig index b2c98a30a..5eb806e20 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -534,6 +534,10 @@ fn keyCallback( .down => .down, .right => .right, .left => .left, + .home => .home, + .end => .end, + .page_up => .page_up, + .page_down => .page_down, else => .invalid, }, }; diff --git a/src/config.zig b/src/config.zig index 87f773d3f..c8a622ce7 100644 --- a/src/config.zig +++ b/src/config.zig @@ -92,6 +92,10 @@ pub const Config = struct { try result.keybind.set.put(alloc, .{ .key = .down }, .{ .csi = "B" }); try result.keybind.set.put(alloc, .{ .key = .right }, .{ .csi = "C" }); try result.keybind.set.put(alloc, .{ .key = .left }, .{ .csi = "D" }); + try result.keybind.set.put(alloc, .{ .key = .home }, .{ .csi = "H" }); + try result.keybind.set.put(alloc, .{ .key = .end }, .{ .csi = "F" }); + try result.keybind.set.put(alloc, .{ .key = .page_up }, .{ .csi = "5~" }); + try result.keybind.set.put(alloc, .{ .key = .page_down }, .{ .csi = "6~" }); return result; } diff --git a/src/input/key.zig b/src/input/key.zig index 940e7b570..2bb40b4c7 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -54,6 +54,10 @@ pub const Key = enum { down, right, left, + home, + end, + page_up, + page_down, // To support more keys (there are obviously more!) add them here // and ensure the mapping is up to date in the Window key handler. From 5713c2f468a1fd6cbf23345454438404efd78da1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Aug 2022 11:17:28 -0700 Subject: [PATCH 10/10] move input test to qualified --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 24fd0d8db..9585274f2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -102,6 +102,7 @@ test { _ = @import("TempDir.zig"); _ = @import("font/main.zig"); _ = @import("terminal/Terminal.zig"); + _ = @import("input.zig"); // Libraries _ = @import("libuv"); @@ -111,5 +112,4 @@ test { // TODO _ = @import("config.zig"); _ = @import("cli_args.zig"); - _ = @import("input.zig"); }