/// 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"); const function_keys = @import("function_keys.zig"); event: key.Event, /// The state of various modes of a terminal that impact encoding. alt_esc_prefix: bool = false, 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(); // 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]; } // If we have no UTF8 text then at this point there is nothing to do. const utf8 = self.event.utf8; if (utf8.len == 0) return ""; // In modify other keys state 2, we send the CSI 27 sequence // for any char with a modifier. Ctrl sequences like Ctrl+a // are already handled above. if (self.modify_other_keys_state_2) modify_other: { const view = try std.unicode.Utf8View.init(utf8); var it = view.iterator(); const codepoint = it.nextCodepoint() orelse break :modify_other; // We only do this if we have a single codepoint. There shouldn't // ever be a multi-codepoint sequence that triggers this. if (it.nextCodepoint() != null) break :modify_other; // This copies xterm's `ModifyOtherKeys` function that returns // whether modify other keys should be encoded for the given // input. const should_modify = should_modify: { // xterm IsControlInput if (codepoint >= 0x40 and codepoint <= 0x7F) break :should_modify true; // If we have anything other than shift pressed, encode. var mods_no_shift = binding_mods; mods_no_shift.shift = false; if (!mods_no_shift.empty()) break :should_modify true; // We only have shift pressed. We only allow space. if (codepoint == ' ') break :should_modify true; // This logic isn't complete but I don't fully understand // the rest so I'm going to wait until we can have a // reasonable test scenario. break :should_modify false; }; if (should_modify) { for (function_keys.modifiers, 2..) |modset, code| { if (!binding_mods.equal(modset)) continue; return try std.fmt.bufPrint( buf, "\x1B[27;{};{}~", .{ code, codepoint }, ); } } } // // Let's see if we should apply fixterms to this codepoint. // // At this stage of key processing, we only need to apply fixterms // // to unicode codepoints (the point of charCallback) if we have // // ctrl set. // if (mods.ctrl) { // const csi_u_mods = terminal.csi_u.Mods.fromInput(mods); // const resp = try std.fmt.bufPrint( // &data, // "\x1B[{};{}u", // .{ codepoint, csi_u_mods.seqInt() }, // ); // _ = self.io_thread.mailbox.push(.{ // .write_small = .{ // .data = data, // .len = @intCast(resp.len), // }, // }, .{ .forever = {} }); // try self.io_thread.wakeup.notify(); // return; // } // If we have alt-pressed and alt-esc-prefix is enabled, then // we need to prefix the utf8 sequence with an esc. if (binding_mods.alt and self.alt_esc_prefix) { // TODO: port this, I think we can just use effective mods // without any OS special case // // On macOS, we have to opt-in to using alt because option // by default is a unicode character sequence. // if (comptime builtin.target.isDarwin()) { // switch (self.config.macos_option_as_alt) { // .false => break :alt, // .true => {}, // .left => if (mods.sides.alt != .left) break :alt, // .right => if (mods.sides.alt != .right) break :alt, // } // } return try std.fmt.bufPrint(buf, "\x1B{s}", .{utf8}); } return utf8; } /// 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. /// 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(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 = 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 (keyval) { .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 "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 "legacy: ctrl+shift+char with modify other state 2" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ .key = .h, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "H", }, .modify_other_keys_state_2 = true, }; const actual = try enc.legacy(&buf); try testing.expectEqualStrings("\x1b[27;6;72~", actual); } test "ctrlseq: normal ctrl c" { const seq = ctrlSeq(.c, .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: alt should be allowed" { const seq = ctrlSeq(.c, .{ .alt = true, .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: no ctrl does nothing" { try testing.expect(ctrlSeq(.c, .{}) == null); } test "ctrlseq: shift does not generate ctrl seq" { try testing.expect(ctrlSeq(.c, .{ .shift = true }) == null); try testing.expect(ctrlSeq(.c, .{ .shift = true, .ctrl = true }) == null); }