From f57fd99d3e0d8aa650c5926a4ed27251b7d25c21 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 08:52:53 -0700 Subject: [PATCH] input: starting to work on KeyEncoder, got ctrl sequences --- src/input.zig | 1 + src/input/KeyEncoder.zig | 131 +++++++++++++++++++++++++++++++++++++++ src/input/key.zig | 56 ++++++++++++++++- 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/input/KeyEncoder.zig diff --git a/src/input.zig b/src/input.zig index e2deed0e2..f382386c6 100644 --- a/src/input.zig +++ b/src/input.zig @@ -6,6 +6,7 @@ pub usingnamespace @import("input/key.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); pub const Binding = @import("input/Binding.zig"); +pub const KeyEncoder = @import("input/KeyEncoder.zig"); pub const SplitDirection = Binding.Action.SplitDirection; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig new file mode 100644 index 000000000..65a80c1f7 --- /dev/null +++ b/src/input/KeyEncoder.zig @@ -0,0 +1,131 @@ +/// KeyEncoder is responsible for processing keyboard input and generating +/// the proper VT sequence for any events. +/// +/// A new KeyEncoder should be created for each individual key press. +/// These encoders are not meant to be reused. +const KeyEncoder = @This(); + +const std = @import("std"); +const testing = std.testing; + +const key = @import("key.zig"); + +key: key.Key, +binding_mods: key.Mods, + +/// Initialize +fn init(event: key.Event) KeyEncoder { + const effective_mods = event.effectiveMods(); + const binding_mods = effective_mods.binding(); + + return .{ + .key = event.key, + .binding_mods = binding_mods, + }; +} + +/// Returns the C0 byte for the key event if it should be used. +/// This converts a key event into the expected terminal behavior +/// such as Ctrl+C turning into 0x03, amongst many other translations. +/// +/// This will return null if the key event should not be converted +/// into a C0 byte. There are many cases for this and you should read +/// the source code to understand them. +fn ctrlSeq(self: *const KeyEncoder) ?u8 { + // Remove alt from our modifiers because it does not impact whether + // we are generating a ctrl sequence. + const unalt_mods = unalt_mods: { + var unalt_mods = self.binding_mods; + unalt_mods.alt = false; + break :unalt_mods unalt_mods; + }; + + // If we have any other modifier key set, then we do not generate + // a C0 sequence. + const ctrl_only = comptime (key.Mods{ .ctrl = true }).int(); + if (unalt_mods.int() != ctrl_only) return null; + + // The normal approach to get this value is to make the ascii byte + // with 0x1F. However, not all apprt key translation will properly + // generate the correct value so we just hardcode this based on + // logical key. + return switch (self.key) { + .space => 0, + .slash => 0x1F, + .zero => 0x30, + .one => 0x31, + .two => 0x00, + .three => 0x1B, + .four => 0x1C, + .five => 0x1D, + .six => 0x1E, + .seven => 0x1F, + .eight => 0x7F, + .nine => 0x39, + .backslash => 0x1C, + .left_bracket => 0x1B, + .right_bracket => 0x1D, + .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 => null, + }; +} + +test "ctrlseq: normal ctrl c" { + const enc = init(.{ .key = .c, .mods = .{ .ctrl = true } }); + const seq = enc.ctrlSeq(); + try testing.expectEqual(@as(u8, 0x03), seq.?); +} + +test "ctrlseq: alt should be allowed" { + const enc = init(.{ .key = .c, .mods = .{ .alt = true, .ctrl = true } }); + const seq = enc.ctrlSeq(); + try testing.expectEqual(@as(u8, 0x03), seq.?); +} + +test "ctrlseq: no ctrl does nothing" { + const enc = init(.{ .key = .c, .mods = .{} }); + try testing.expect(enc.ctrlSeq() == null); +} + +test "ctrlseq: shift does not generate ctrl seq" { + { + const enc = init(.{ + .key = .c, + .mods = .{ .shift = true }, + }); + try testing.expect(enc.ctrlSeq() == null); + } + + { + const enc = init(.{ + .key = .c, + .mods = .{ .shift = true, .ctrl = true }, + }); + try testing.expect(enc.ctrlSeq() == null); + } +} diff --git a/src/input/key.zig b/src/input/key.zig index 8ec485c9a..92db8009e 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -1,8 +1,55 @@ 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. +/// A generic key input event. This is the information that is necessary +/// regardless of apprt in order to generate the proper terminal +/// control sequences for a given key press. +/// +/// Some apprts may not be able to provide all of this information, such +/// as GLFW. In this case, the apprt should provide as much information +/// as it can and it should be expected that the terminal behavior +/// will not be totally correct. +pub const Event = struct { + /// The action: press, release, etc. + action: Action = .press, + + /// "key" is the logical key that was pressed. For example, if + /// a Dvorak keyboard layout is being used on a US keyboard, + /// the "i" physical key will be reported as "c". The physical + /// key is the key that was physically pressed on the keyboard. + key: Key, + physical_key: Key = .invalid, + + /// Mods are the modifiers that are pressed. + mods: Mods = .{}, + + /// The mods that were consumed in order to generate the text + /// in utf8. This has the mods set that were consumed, so to + /// get the set of mods that are effective you must negate + /// mods with this. + /// + /// This field is meaningless if utf8 is empty. + consumed_mods: Mods = .{}, + + /// Composing is true when this key event is part of a dead key + /// composition sequence and we're in the middle of it. + composing: bool = false, + + /// The utf8 sequence that was generated by this key event. + /// This will be an empty string if there is no text generated. + /// If composing is true and this is non-empty, this is preedit + /// text. + utf8: []const u8 = "", + + /// Returns the effective modifiers for this event. The effective + /// modifiers are the mods that should be considered for keybindings. + pub fn effectiveMods(self: Event) Mods { + if (self.utf8.len == 0) return self.mods; + return self.mods.unset(self.consumed_mods); + } +}; + +/// A bitmask for all key modifiers. /// /// IMPORTANT: Any changes here update include/ghostty.h pub const Mods = packed struct(Mods.Backing) { @@ -56,6 +103,11 @@ pub const Mods = packed struct(Mods.Backing) { }; } + /// Perform `self &~ other` to remove the other mods from self. + pub fn unset(self: Mods, other: Mods) Mods { + return @bitCast(self.int() & ~other.int()); + } + /// Returns the mods without locks set. pub fn withoutLocks(self: Mods) Mods { var copy = self;