From 6555bb1410da29e5ce143ec554dd78b0bd2dbdc2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 09:30:36 -0700 Subject: [PATCH] input: KeyEncoder legacy encoding handles old keyCallback logic --- src/input/KeyEncoder.zig | 185 ++++++++++++++++++++++++++++++++------- 1 file changed, 152 insertions(+), 33 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 65a80c1f7..380f6b090 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -9,19 +9,102 @@ const std = @import("std"); const testing = std.testing; const key = @import("key.zig"); +const function_keys = @import("function_keys.zig"); -key: key.Key, -binding_mods: key.Mods, +event: key.Event, -/// Initialize -fn init(event: key.Event) KeyEncoder { - const effective_mods = event.effectiveMods(); +/// The state of various modes of a terminal that impact encoding. +cursor_key_application: bool = false, +keypad_key_application: bool = false, +modify_other_keys_state_2: bool = false, + +/// Perform legacy encoding of the key event. "Legacy" in this case +/// is referring to the behavior of traditional terminals, plus +/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec. +/// These together combine the legacy protocol because they're all +/// meant to be extensions that do not change any existing behavior +/// and therefore safe to combine. +fn legacy( + self: *const KeyEncoder, + buf: []u8, +) ![]const u8 { + const effective_mods = self.event.effectiveMods(); const binding_mods = effective_mods.binding(); - return .{ - .key = event.key, - .binding_mods = binding_mods, - }; + // Legacy encoding only does press/repeat + if (self.event.action != .press and + self.event.action != .repeat) return ""; + + // If we match a PC style function key then that is our result. + if (pcStyleFunctionKey( + self.event.key, + binding_mods, + self.cursor_key_application, + self.keypad_key_application, + self.modify_other_keys_state_2, + )) |sequence| return sequence; + + // If we match a control sequence, we output that directly. + if (ctrlSeq(self.event.key, binding_mods)) |char| { + // C0 sequences support alt-as-esc prefixing. + if (binding_mods.alt) { + if (buf.len < 2) return error.OutOfMemory; + buf[0] = 0x1B; + buf[1] = char; + return buf[0..2]; + } + + if (buf.len < 1) return error.OutOfMemory; + buf[0] = char; + return buf[0..1]; + } + + return ""; +} + +/// Determines whether the key should be encoded in the xterm +/// "PC-style Function Key" syntax (roughly). This is a hardcoded +/// table of keys and modifiers that result in a specific sequence. +fn pcStyleFunctionKey( + keyval: key.Key, + mods: key.Mods, + cursor_key_application: bool, + keypad_key_application: bool, + modify_other_keys: bool, // True if state 2 +) ?[]const u8 { + const mods_int = mods.int(); + for (function_keys.keys.get(keyval)) |entry| { + switch (entry.cursor) { + .any => {}, + .normal => if (cursor_key_application) continue, + .application => if (!cursor_key_application) continue, + } + + switch (entry.keypad) { + .any => {}, + .normal => if (keypad_key_application) continue, + .application => if (!keypad_key_application) continue, + } + + switch (entry.modify_other_keys) { + .any => {}, + .set => if (modify_other_keys) continue, + .set_other => if (!modify_other_keys) continue, + } + + const entry_mods_int = entry.mods.int(); + if (entry_mods_int == 0) { + if (mods_int != 0 and !entry.mods_empty_is_any) continue; + // mods are either empty, or empty means any so we allow it. + } else if (entry_mods_int != mods_int) { + // any set mods require an exact match + continue; + } + + return entry.sequence; + } + + return null; } /// Returns the C0 byte for the key event if it should be used. @@ -31,11 +114,11 @@ fn init(event: key.Event) KeyEncoder { /// 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 { +fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?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; + var unalt_mods = mods; unalt_mods.alt = false; break :unalt_mods unalt_mods; }; @@ -49,7 +132,7 @@ fn ctrlSeq(self: *const KeyEncoder) ?u8 { // 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) { + return switch (keyval) { .space => 0, .slash => 0x1F, .zero => 0x30, @@ -95,37 +178,73 @@ fn ctrlSeq(self: *const KeyEncoder) ?u8 { }; } +test "legacy: ctrl+alt+c" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .c, + .mods = .{ .ctrl = true, .alt = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x1b\x03", actual); +} + +test "legacy: ctrl+c" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .c, + .mods = .{ .ctrl = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x03", actual); +} + +test "legacy: ctrl+space" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .space, + .mods = .{ .ctrl = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x00", actual); +} + +test "legacy: ctrl+shift+backspace" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .backspace, + .mods = .{ .ctrl = true, .shift = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x08", actual); +} + test "ctrlseq: normal ctrl c" { - const enc = init(.{ .key = .c, .mods = .{ .ctrl = true } }); - const seq = enc.ctrlSeq(); + const seq = ctrlSeq(.c, .{ .ctrl = true }); 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(); + const seq = ctrlSeq(.c, .{ .alt = true, .ctrl = true }); 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); + try testing.expect(ctrlSeq(.c, .{}) == 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); - } + try testing.expect(ctrlSeq(.c, .{ .shift = true }) == null); + try testing.expect(ctrlSeq(.c, .{ .shift = true, .ctrl = true }) == null); }