mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00

Associated text should only be sent to the terminal when printable text is generated from the keypress. Prevent sending associated text when any modifier is pressed, except for Shift, NumLock, and Capslock This brings Ghostty inline with the output of Kitty.
1595 lines
46 KiB
Zig
1595 lines
46 KiB
Zig
/// 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 builtin = @import("builtin");
|
|
const testing = std.testing;
|
|
|
|
const key = @import("key.zig");
|
|
const config = @import("../config.zig");
|
|
const function_keys = @import("function_keys.zig");
|
|
const terminal = @import("../terminal/main.zig");
|
|
const KittyEntry = @import("kitty.zig").Entry;
|
|
const kitty_entries = @import("kitty.zig").entries;
|
|
const KittyFlags = terminal.kitty.KeyFlags;
|
|
|
|
const log = std.log.scoped(.key_encoder);
|
|
|
|
event: key.KeyEvent,
|
|
|
|
/// The state of various modes of a terminal that impact encoding.
|
|
macos_option_as_alt: config.OptionAsAlt = .false,
|
|
alt_esc_prefix: bool = false,
|
|
cursor_key_application: bool = false,
|
|
keypad_key_application: bool = false,
|
|
modify_other_keys_state_2: bool = false,
|
|
kitty_flags: KittyFlags = .{},
|
|
|
|
/// Perform the proper encoding depending on the terminal state.
|
|
pub fn encode(
|
|
self: *const KeyEncoder,
|
|
buf: []u8,
|
|
) ![]const u8 {
|
|
// log.debug("encode {}", .{self.*});
|
|
|
|
if (self.kitty_flags.int() != 0) return try self.kitty(buf);
|
|
return try self.legacy(buf);
|
|
}
|
|
|
|
/// Perform Kitty keyboard protocol encoding of the key event.
|
|
fn kitty(
|
|
self: *const KeyEncoder,
|
|
buf: []u8,
|
|
) ![]const u8 {
|
|
// This should never happen but we'll check anyway.
|
|
if (self.kitty_flags.int() == 0) return try self.legacy(buf);
|
|
|
|
// We only processed "press" events unless report events is active
|
|
if (self.event.action == .release and !self.kitty_flags.report_events)
|
|
return "";
|
|
|
|
const all_mods = self.event.mods;
|
|
const effective_mods = self.event.effectiveMods();
|
|
const binding_mods = effective_mods.binding();
|
|
|
|
// Find the entry for this key in the kitty table.
|
|
const entry_: ?KittyEntry = entry: {
|
|
// Functional or predefined keys
|
|
for (kitty_entries) |entry| {
|
|
if (entry.key == self.event.key) break :entry entry;
|
|
}
|
|
|
|
// Otherwise, we use our unicode codepoint from UTF8. We
|
|
// always use the unshifted value.
|
|
if (self.event.unshifted_codepoint > 0) {
|
|
break :entry .{
|
|
.key = self.event.key,
|
|
.code = self.event.unshifted_codepoint,
|
|
.final = 'u',
|
|
.modifier = false,
|
|
};
|
|
}
|
|
|
|
break :entry null;
|
|
};
|
|
|
|
preprocessing: {
|
|
// When composing, the only keys sent are plain modifiers.
|
|
if (self.event.composing) {
|
|
if (entry_) |entry| {
|
|
if (entry.modifier) break :preprocessing;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
// IME confirmation still sends an enter key so if we have enter
|
|
// and UTF8 text we just send it directly since we assume that is
|
|
// whats happening.
|
|
if (self.event.key == .enter and
|
|
self.event.utf8.len > 0)
|
|
{
|
|
return try copyToBuf(buf, self.event.utf8);
|
|
}
|
|
|
|
// If we're reporting all then we always send CSI sequences.
|
|
if (!self.kitty_flags.report_all) {
|
|
// Quote:
|
|
// The only exceptions are the Enter, Tab and Backspace keys which
|
|
// still generate the same bytes as in legacy mode this is to allow the
|
|
// user to type and execute commands in the shell such as reset after a
|
|
// program that sets this mode crashes without clearing it.
|
|
//
|
|
// Quote ("report all" mode):
|
|
// Note that all keys are reported as escape codes, including Enter,
|
|
// Tab, Backspace etc.
|
|
if (effective_mods.empty()) {
|
|
switch (self.event.key) {
|
|
.enter => return try copyToBuf(buf, "\r"),
|
|
.tab => return try copyToBuf(buf, "\t"),
|
|
.backspace => return try copyToBuf(buf, "\x7F"),
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
// Send plain-text non-modified text directly to the terminal.
|
|
// We don't send release events because those are specially encoded.
|
|
if (self.event.utf8.len > 0 and
|
|
binding_mods.empty() and
|
|
self.event.action != .release)
|
|
plain_text: {
|
|
// We only do this for printable characters. We should
|
|
// inspect the real unicode codepoint properties here but
|
|
// the real world issue is usually control characters.
|
|
const view = try std.unicode.Utf8View.init(self.event.utf8);
|
|
var it = view.iterator();
|
|
while (it.nextCodepoint()) |cp| if (isControl(cp)) break :plain_text;
|
|
|
|
return try copyToBuf(buf, self.event.utf8);
|
|
}
|
|
}
|
|
}
|
|
|
|
const entry = entry_ orelse return "";
|
|
|
|
// If this is just a modifier we require "report all" to send the sequence.
|
|
if (entry.modifier and !self.kitty_flags.report_all) return "";
|
|
|
|
const seq: KittySequence = seq: {
|
|
var seq: KittySequence = .{
|
|
.key = entry.code,
|
|
.final = entry.final,
|
|
.mods = KittyMods.fromInput(all_mods),
|
|
};
|
|
|
|
if (self.kitty_flags.report_events) {
|
|
seq.event = switch (self.event.action) {
|
|
.press => .press,
|
|
.release => .release,
|
|
.repeat => .repeat,
|
|
};
|
|
}
|
|
|
|
if (self.kitty_flags.report_alternates) alternates: {
|
|
// Break early if this is a control key
|
|
if (isControl(seq.key)) break :alternates;
|
|
|
|
// Set the first alternate (shifted version)
|
|
{
|
|
const view = try std.unicode.Utf8View.init(self.event.utf8);
|
|
var it = view.iterator();
|
|
// We break early if there are codepoints...there are no
|
|
// alternate key(s) to report
|
|
const shifted = it.nextCodepoint() orelse break :alternates;
|
|
// Only report the shifted key if we have a shift modifier
|
|
if (shifted != seq.key and seq.mods.shift) seq.alternates[0] = shifted;
|
|
}
|
|
|
|
// Set the base layout key
|
|
if (self.event.key.codepoint()) |base| {
|
|
if (base != seq.key) seq.alternates[1] = base;
|
|
}
|
|
}
|
|
|
|
if (self.kitty_flags.report_associated and !seq.mods.preventsText()) {
|
|
seq.text = self.event.utf8;
|
|
}
|
|
|
|
break :seq seq;
|
|
};
|
|
|
|
return try seq.encode(buf);
|
|
}
|
|
|
|
/// 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 all_mods = self.event.mods;
|
|
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're in a dead key state then we never emit a sequence.
|
|
if (self.event.composing) return "";
|
|
|
|
// If we match a PC style function key then that is our result.
|
|
if (pcStyleFunctionKey(
|
|
self.event.key,
|
|
all_mods,
|
|
self.cursor_key_application,
|
|
self.keypad_key_application,
|
|
self.modify_other_keys_state_2,
|
|
)) |sequence| pc_style: {
|
|
// If we're pressing enter and have UTF-8 text, we probably are
|
|
// clearing a dead key state. This happens specifically on macOS.
|
|
// We have a unit test for this.
|
|
if (self.event.key == .enter and self.event.utf8.len > 0) {
|
|
break :pc_style;
|
|
}
|
|
|
|
return copyToBuf(buf, sequence);
|
|
}
|
|
|
|
// If we match a control sequence, we output that directly. For
|
|
// ctrlSeq we have to use all mods because we want it to only
|
|
// match ctrl+<char>.
|
|
if (ctrlSeq(self.event.key, all_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 the only possibility is the
|
|
// alt-prefix handling of unshifted codepoints... so we process that.
|
|
const utf8 = self.event.utf8;
|
|
if (utf8.len == 0) {
|
|
if (try self.legacyAltPrefix(binding_mods, all_mods)) |byte| {
|
|
return try std.fmt.bufPrint(buf, "\x1B{c}", .{byte});
|
|
}
|
|
|
|
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 if we have ctrl set.
|
|
if (self.event.mods.ctrl) {
|
|
// Important: we want to use the original mods here, not the
|
|
// effective mods. The fixterms spec states the shifted chars
|
|
// should be sent uppercase but Kitty changes that behavior
|
|
// so we'll send all the mods.
|
|
const csi_u_mods = CsiUMods.fromInput(self.event.mods);
|
|
const result = try std.fmt.bufPrint(
|
|
buf,
|
|
"\x1B[{};{}u",
|
|
.{ utf8[0], csi_u_mods.seqInt() },
|
|
);
|
|
// std.log.warn("CSI_U: {s}", .{result});
|
|
return result;
|
|
}
|
|
|
|
// If we have alt-pressed and alt-esc-prefix is enabled, then
|
|
// we need to prefix the utf8 sequence with an esc.
|
|
if (try self.legacyAltPrefix(binding_mods, all_mods)) |byte| {
|
|
return try std.fmt.bufPrint(buf, "\x1B{c}", .{byte});
|
|
}
|
|
|
|
return try copyToBuf(buf, utf8);
|
|
}
|
|
|
|
fn legacyAltPrefix(
|
|
self: *const KeyEncoder,
|
|
binding_mods: key.Mods,
|
|
mods: key.Mods,
|
|
) !?u8 {
|
|
// This only takes effect with alt pressed
|
|
if (!binding_mods.alt or !self.alt_esc_prefix) return null;
|
|
|
|
// On macOS, we only handle option like alt in certain
|
|
// circumstances. Otherwise, macOS does a unicode translation
|
|
// and we allow that to happen.
|
|
if (comptime builtin.target.isDarwin()) {
|
|
switch (self.macos_option_as_alt) {
|
|
.false => return null,
|
|
.left => if (mods.sides.alt == .right) return null,
|
|
.right => if (mods.sides.alt == .left) return null,
|
|
.true => {},
|
|
}
|
|
}
|
|
|
|
// Otherwise, we require utf8 to already have the byte represented.
|
|
const utf8 = self.event.utf8;
|
|
if (utf8.len == 1) {
|
|
if (std.math.cast(u8, utf8[0])) |byte| {
|
|
return byte;
|
|
}
|
|
}
|
|
|
|
// If UTF8 isn't set, we will allow unshifted codepoints through.
|
|
if (self.event.unshifted_codepoint > 0) {
|
|
if (std.math.cast(
|
|
u8,
|
|
self.event.unshifted_codepoint,
|
|
)) |byte| {
|
|
return byte;
|
|
}
|
|
}
|
|
|
|
// Else, we can't figure out the byte to alt-prefix so we
|
|
// exit this handling.
|
|
return null;
|
|
}
|
|
|
|
/// A helper to memcpy a src value to a buffer and return the result.
|
|
fn copyToBuf(buf: []u8, src: []const u8) ![]const u8 {
|
|
if (src.len > buf.len) return error.OutOfMemory;
|
|
const result = buf[0..src.len];
|
|
@memcpy(result, src);
|
|
return result;
|
|
}
|
|
|
|
/// 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 {
|
|
// We only want binding-sensitive mods because lock keys
|
|
// and directional modifiers (left/right) don't matter for
|
|
// pc-style function keys.
|
|
const mods_int = mods.binding().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.binding();
|
|
};
|
|
|
|
// 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,
|
|
.right_bracket => 0x1D,
|
|
.a => 0x01,
|
|
.b => 0x02,
|
|
.c => 0x03,
|
|
.d => 0x04,
|
|
.e => 0x05,
|
|
.f => 0x06,
|
|
.g => 0x07,
|
|
.h => 0x08,
|
|
.j => 0x0A,
|
|
.k => 0x0B,
|
|
.l => 0x0C,
|
|
.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,
|
|
|
|
// These are purposely NOT handled here because of the fixterms
|
|
// specification: https://www.leonerd.org.uk/hacks/fixterms/
|
|
// These are processed as CSI u.
|
|
// .i => 0x09,
|
|
// .m => 0x0D,
|
|
// .left_bracket => 0x1B,
|
|
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Returns true if this is an ASCII control character, matches libc implementation.
|
|
fn isControl(cp: u21) bool {
|
|
return cp < 0x20 or cp == 0x7F;
|
|
}
|
|
|
|
/// This is the bitmask for fixterm CSI u modifiers.
|
|
const CsiUMods = packed struct(u3) {
|
|
shift: bool = false,
|
|
alt: bool = false,
|
|
ctrl: bool = false,
|
|
|
|
/// Convert an input mods value into the CSI u mods value.
|
|
pub fn fromInput(mods: key.Mods) CsiUMods {
|
|
return .{
|
|
.shift = mods.shift,
|
|
.alt = mods.alt,
|
|
.ctrl = mods.ctrl,
|
|
};
|
|
}
|
|
|
|
/// Returns the raw int value of this packed struct.
|
|
pub fn int(self: CsiUMods) u3 {
|
|
return @bitCast(self);
|
|
}
|
|
|
|
/// Returns the integer value sent as part of the CSI u sequence.
|
|
/// This adds 1 to the bitmask value as described in the spec.
|
|
pub fn seqInt(self: CsiUMods) u4 {
|
|
const raw: u4 = @intCast(self.int());
|
|
return raw + 1;
|
|
}
|
|
|
|
test "modifer sequence values" {
|
|
// This is all sort of trivially seen by looking at the code but
|
|
// we want to make sure we never regress this.
|
|
var mods: CsiUMods = .{};
|
|
try testing.expectEqual(@as(u4, 1), mods.seqInt());
|
|
|
|
mods = .{ .shift = true };
|
|
try testing.expectEqual(@as(u4, 2), mods.seqInt());
|
|
|
|
mods = .{ .alt = true };
|
|
try testing.expectEqual(@as(u4, 3), mods.seqInt());
|
|
|
|
mods = .{ .ctrl = true };
|
|
try testing.expectEqual(@as(u4, 5), mods.seqInt());
|
|
|
|
mods = .{ .alt = true, .shift = true };
|
|
try testing.expectEqual(@as(u4, 4), mods.seqInt());
|
|
|
|
mods = .{ .ctrl = true, .shift = true };
|
|
try testing.expectEqual(@as(u4, 6), mods.seqInt());
|
|
|
|
mods = .{ .alt = true, .ctrl = true };
|
|
try testing.expectEqual(@as(u4, 7), mods.seqInt());
|
|
|
|
mods = .{ .alt = true, .ctrl = true, .shift = true };
|
|
try testing.expectEqual(@as(u4, 8), mods.seqInt());
|
|
}
|
|
};
|
|
|
|
/// This is the bitfields for Kitty modifiers.
|
|
const KittyMods = packed struct(u8) {
|
|
shift: bool = false,
|
|
alt: bool = false,
|
|
ctrl: bool = false,
|
|
super: bool = false,
|
|
hyper: bool = false,
|
|
meta: bool = false,
|
|
caps_lock: bool = false,
|
|
num_lock: bool = false,
|
|
|
|
/// Convert an input mods value into the CSI u mods value.
|
|
pub fn fromInput(mods: key.Mods) KittyMods {
|
|
return .{
|
|
.shift = mods.shift,
|
|
.alt = mods.alt,
|
|
.ctrl = mods.ctrl,
|
|
.super = mods.super,
|
|
.caps_lock = mods.caps_lock,
|
|
.num_lock = mods.num_lock,
|
|
};
|
|
}
|
|
|
|
/// Returns true if the modifiers prevent printable text
|
|
pub fn preventsText(self: KittyMods) bool {
|
|
if (self.alt or
|
|
self.ctrl or
|
|
self.super or
|
|
self.hyper or
|
|
self.meta)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Returns the raw int value of this packed struct.
|
|
pub fn int(self: KittyMods) u8 {
|
|
return @bitCast(self);
|
|
}
|
|
|
|
/// Returns the integer value sent as part of the Kitty sequence.
|
|
/// This adds 1 to the bitmask value as described in the spec.
|
|
pub fn seqInt(self: KittyMods) u9 {
|
|
const raw: u9 = @intCast(self.int());
|
|
return raw + 1;
|
|
}
|
|
|
|
test "modifer sequence values" {
|
|
// This is all sort of trivially seen by looking at the code but
|
|
// we want to make sure we never regress this.
|
|
var mods: KittyMods = .{};
|
|
try testing.expectEqual(@as(u9, 1), mods.seqInt());
|
|
|
|
mods = .{ .shift = true };
|
|
try testing.expectEqual(@as(u9, 2), mods.seqInt());
|
|
|
|
mods = .{ .alt = true };
|
|
try testing.expectEqual(@as(u9, 3), mods.seqInt());
|
|
|
|
mods = .{ .ctrl = true };
|
|
try testing.expectEqual(@as(u9, 5), mods.seqInt());
|
|
|
|
mods = .{ .alt = true, .shift = true };
|
|
try testing.expectEqual(@as(u9, 4), mods.seqInt());
|
|
|
|
mods = .{ .ctrl = true, .shift = true };
|
|
try testing.expectEqual(@as(u9, 6), mods.seqInt());
|
|
|
|
mods = .{ .alt = true, .ctrl = true };
|
|
try testing.expectEqual(@as(u9, 7), mods.seqInt());
|
|
|
|
mods = .{ .alt = true, .ctrl = true, .shift = true };
|
|
try testing.expectEqual(@as(u9, 8), mods.seqInt());
|
|
}
|
|
};
|
|
|
|
/// Represents a kitty key sequence and has helpers for encoding it.
|
|
/// The sequence from the Kitty specification:
|
|
///
|
|
/// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
|
|
const KittySequence = struct {
|
|
key: u21,
|
|
final: u8,
|
|
mods: KittyMods = .{},
|
|
event: Event = .none,
|
|
alternates: [2]?u21 = .{ null, null },
|
|
text: []const u8 = "",
|
|
|
|
/// Values for the event code (see "event-type" in above comment).
|
|
/// Note that Kitty omits the ":1" for the press event but other
|
|
/// terminals include it. We'll include it.
|
|
const Event = enum(u2) {
|
|
none = 0,
|
|
press = 1,
|
|
repeat = 2,
|
|
release = 3,
|
|
};
|
|
|
|
pub fn encode(self: KittySequence, buf: []u8) ![]const u8 {
|
|
if (self.final == 'u' or self.final == '~') return try self.encodeFull(buf);
|
|
return try self.encodeSpecial(buf);
|
|
}
|
|
|
|
fn encodeFull(self: KittySequence, buf: []u8) ![]const u8 {
|
|
// Boilerplate to basically create a string builder that writes
|
|
// over our buffer (but no more).
|
|
var fba = std.heap.FixedBufferAllocator.init(buf);
|
|
const alloc = fba.allocator();
|
|
var builder = try std.ArrayListUnmanaged(u8).initCapacity(alloc, buf.len);
|
|
const writer = builder.writer(alloc);
|
|
|
|
// Key section
|
|
try writer.print("\x1B[{d}", .{self.key});
|
|
// Write our alternates
|
|
if (self.alternates[0]) |shifted| try writer.print(":{d}", .{shifted});
|
|
if (self.alternates[1]) |base| {
|
|
if (self.alternates[0] == null) {
|
|
try writer.print("::{d}", .{base});
|
|
} else {
|
|
try writer.print(":{d}", .{base});
|
|
}
|
|
}
|
|
|
|
// Mods and events section
|
|
const mods = self.mods.seqInt();
|
|
var emit_prior = false;
|
|
if (self.event != .none) {
|
|
try writer.print(";{d}:{d}", .{ mods, @intFromEnum(self.event) });
|
|
emit_prior = true;
|
|
} else if (mods > 1) {
|
|
try writer.print(";{d}", .{mods});
|
|
emit_prior = true;
|
|
}
|
|
|
|
// Text section
|
|
if (self.text.len > 0) {
|
|
const view = try std.unicode.Utf8View.init(self.text);
|
|
var it = view.iterator();
|
|
var count: usize = 0;
|
|
while (it.nextCodepoint()) |cp| {
|
|
// If the codepoint is non-printable ASCII character, skip.
|
|
if (isControl(cp)) continue;
|
|
|
|
// We need to add our ";". We need to add two if we didn't emit
|
|
// the modifier section. We only do this initially.
|
|
if (count == 0) {
|
|
if (!emit_prior) try writer.writeByte(';');
|
|
try writer.writeByte(';');
|
|
} else {
|
|
try writer.writeByte(':');
|
|
}
|
|
|
|
try writer.print("{d}", .{cp});
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
try writer.print("{c}", .{self.final});
|
|
return builder.items;
|
|
}
|
|
|
|
fn encodeSpecial(self: KittySequence, buf: []u8) ![]const u8 {
|
|
const mods = self.mods.seqInt();
|
|
if (self.event != .none) {
|
|
return try std.fmt.bufPrint(buf, "\x1B[1;{d}:{d}{c}", .{
|
|
mods,
|
|
@intFromEnum(self.event),
|
|
self.final,
|
|
});
|
|
}
|
|
|
|
if (mods > 1) {
|
|
return try std.fmt.bufPrint(buf, "\x1B[1;{d}{c}", .{
|
|
mods,
|
|
self.final,
|
|
});
|
|
}
|
|
|
|
return try std.fmt.bufPrint(buf, "\x1B[{c}", .{self.final});
|
|
}
|
|
};
|
|
|
|
test "KittySequence: backspace" {
|
|
var buf: [128]u8 = undefined;
|
|
|
|
// Plain
|
|
{
|
|
var seq: KittySequence = .{ .key = 127, .final = 'u' };
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[127u", actual);
|
|
}
|
|
|
|
// Release event
|
|
{
|
|
var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release };
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[127;1:3u", actual);
|
|
}
|
|
|
|
// Shift
|
|
{
|
|
var seq: KittySequence = .{
|
|
.key = 127,
|
|
.final = 'u',
|
|
.mods = .{ .shift = true },
|
|
};
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[127;2u", actual);
|
|
}
|
|
}
|
|
|
|
test "KittySequence: text" {
|
|
var buf: [128]u8 = undefined;
|
|
|
|
// Plain
|
|
{
|
|
var seq: KittySequence = .{
|
|
.key = 127,
|
|
.final = 'u',
|
|
.text = "A",
|
|
};
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[127;;65u", actual);
|
|
}
|
|
|
|
// Release
|
|
{
|
|
var seq: KittySequence = .{
|
|
.key = 127,
|
|
.final = 'u',
|
|
.event = .release,
|
|
.text = "A",
|
|
};
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[127;1:3;65u", actual);
|
|
}
|
|
|
|
// Shift
|
|
{
|
|
var seq: KittySequence = .{
|
|
.key = 127,
|
|
.final = 'u',
|
|
.mods = .{ .shift = true },
|
|
.text = "A",
|
|
};
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[127;2;65u", actual);
|
|
}
|
|
}
|
|
|
|
test "KittySequence: text with control characters" {
|
|
var buf: [128]u8 = undefined;
|
|
|
|
// By itself
|
|
{
|
|
var seq: KittySequence = .{
|
|
.key = 127,
|
|
.final = 'u',
|
|
.text = "\n",
|
|
};
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1b[127u", actual);
|
|
}
|
|
|
|
// With other printables
|
|
{
|
|
var seq: KittySequence = .{
|
|
.key = 127,
|
|
.final = 'u',
|
|
.text = "A\n",
|
|
};
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1b[127;;65u", actual);
|
|
}
|
|
}
|
|
|
|
test "KittySequence: special no mods" {
|
|
var buf: [128]u8 = undefined;
|
|
var seq: KittySequence = .{ .key = 1, .final = 'A' };
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[A", actual);
|
|
}
|
|
|
|
test "KittySequence: special mods only" {
|
|
var buf: [128]u8 = undefined;
|
|
var seq: KittySequence = .{ .key = 1, .final = 'A', .mods = .{ .shift = true } };
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[1;2A", actual);
|
|
}
|
|
|
|
test "KittySequence: special mods and event" {
|
|
var buf: [128]u8 = undefined;
|
|
var seq: KittySequence = .{
|
|
.key = 1,
|
|
.final = 'A',
|
|
.event = .release,
|
|
.mods = .{ .shift = true },
|
|
};
|
|
const actual = try seq.encode(&buf);
|
|
try testing.expectEqualStrings("\x1B[1;2:3A", actual);
|
|
}
|
|
|
|
test "kitty: plain text" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .a,
|
|
.mods = .{},
|
|
.utf8 = "abcd",
|
|
},
|
|
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("abcd", actual);
|
|
}
|
|
|
|
test "kitty: repeat with just disambiguate" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .a,
|
|
.action = .repeat,
|
|
.mods = .{},
|
|
.utf8 = "a",
|
|
},
|
|
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("a", actual);
|
|
}
|
|
|
|
test "kitty: enter, backspace, tab" {
|
|
var buf: [128]u8 = undefined;
|
|
{
|
|
var enc: KeyEncoder = .{
|
|
.event = .{ .key = .enter, .mods = .{}, .utf8 = "" },
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\r", actual);
|
|
}
|
|
{
|
|
var enc: KeyEncoder = .{
|
|
.event = .{ .key = .backspace, .mods = .{}, .utf8 = "" },
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x7f", actual);
|
|
}
|
|
{
|
|
var enc: KeyEncoder = .{
|
|
.event = .{ .key = .tab, .mods = .{}, .utf8 = "" },
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\t", actual);
|
|
}
|
|
}
|
|
|
|
test "kitty: delete" {
|
|
var buf: [128]u8 = undefined;
|
|
{
|
|
var enc: KeyEncoder = .{
|
|
.event = .{ .key = .delete, .mods = .{}, .utf8 = "\x7F" },
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[3~", actual);
|
|
}
|
|
}
|
|
|
|
test "kitty: composing with no modifier" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .a,
|
|
.mods = .{ .shift = true },
|
|
.composing = true,
|
|
},
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("", actual);
|
|
}
|
|
|
|
test "kitty: composing with modifier" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .left_shift,
|
|
.mods = .{ .shift = true },
|
|
.composing = true,
|
|
},
|
|
.kitty_flags = .{ .disambiguate = true, .report_all = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[57441;2u", actual);
|
|
}
|
|
|
|
test "kitty: shift+a on US keyboard" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .a,
|
|
.mods = .{ .shift = true },
|
|
.utf8 = "A",
|
|
.unshifted_codepoint = 97, // lowercase A
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_alternates = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[97:65;2u", actual);
|
|
}
|
|
|
|
test "kitty: matching unshifted codepoint" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .a,
|
|
.mods = .{ .shift = true },
|
|
.utf8 = "A",
|
|
.unshifted_codepoint = 65,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_alternates = true,
|
|
},
|
|
};
|
|
|
|
// WARNING: This is not a valid encoding. This is a hypothetical encoding
|
|
// just to test that our logic is correct around matching unshifted
|
|
// codepoints. We get an alternate here because the unshifted_codepoint does
|
|
// not match the base key
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[65::97;2u", actual);
|
|
}
|
|
|
|
test "kitty: report alternates with caps" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .j,
|
|
.mods = .{ .caps_lock = true },
|
|
.utf8 = "J",
|
|
.unshifted_codepoint = 106,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_all = true,
|
|
.report_alternates = true,
|
|
.report_associated = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[106;65;74u", actual);
|
|
}
|
|
|
|
test "kitty: report alternates colon (shift+';')" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .semicolon,
|
|
.mods = .{ .shift = true },
|
|
.utf8 = ":",
|
|
.unshifted_codepoint = ';',
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_all = true,
|
|
.report_alternates = true,
|
|
.report_associated = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[59:58;2;58u", actual);
|
|
}
|
|
|
|
test "kitty: report alternates with ru layout" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .semicolon,
|
|
.mods = .{},
|
|
.utf8 = "ч",
|
|
.unshifted_codepoint = 1095,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_all = true,
|
|
.report_alternates = true,
|
|
.report_associated = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[1095::59;;1095u", actual);
|
|
}
|
|
|
|
test "kitty: report alternates with ru layout shifted" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .semicolon,
|
|
.mods = .{ .shift = true },
|
|
.utf8 = "Ч",
|
|
.unshifted_codepoint = 1095,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_all = true,
|
|
.report_alternates = true,
|
|
.report_associated = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[1095:1063:59;2;1063u", actual);
|
|
}
|
|
|
|
test "kitty: report alternates with ru layout caps lock" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .semicolon,
|
|
.mods = .{ .caps_lock = true },
|
|
.utf8 = "Ч",
|
|
.unshifted_codepoint = 1095,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_all = true,
|
|
.report_alternates = true,
|
|
.report_associated = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[1095::59;65;1063u", actual);
|
|
}
|
|
|
|
// macOS generates utf8 text for arrow keys.
|
|
test "kitty: up arrow with utf8" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .up,
|
|
.mods = .{},
|
|
.utf8 = &.{30},
|
|
},
|
|
|
|
.kitty_flags = .{ .disambiguate = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[A", actual);
|
|
}
|
|
|
|
test "kitty: shift+tab" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .tab,
|
|
.mods = .{ .shift = true },
|
|
.utf8 = "", // tab
|
|
},
|
|
|
|
.kitty_flags = .{ .disambiguate = true, .report_alternates = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[9;2u", actual);
|
|
}
|
|
|
|
test "kitty: left shift" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .left_shift,
|
|
.mods = .{},
|
|
.utf8 = "",
|
|
},
|
|
|
|
.kitty_flags = .{ .disambiguate = true, .report_alternates = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("", actual);
|
|
}
|
|
|
|
test "kitty: left shift with report all" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .left_shift,
|
|
.mods = .{},
|
|
.utf8 = "",
|
|
},
|
|
|
|
.kitty_flags = .{ .disambiguate = true, .report_all = true },
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[57441u", actual);
|
|
}
|
|
|
|
test "kitty: report associated with modifiers" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .j,
|
|
.mods = .{ .ctrl = true },
|
|
.utf8 = "j",
|
|
.unshifted_codepoint = 106,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_all = true,
|
|
.report_alternates = true,
|
|
.report_associated = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[106;5u", actual);
|
|
}
|
|
|
|
test "kitty: report associated" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .j,
|
|
.mods = .{ .shift = true },
|
|
.utf8 = "J",
|
|
.unshifted_codepoint = 106,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_all = true,
|
|
.report_alternates = true,
|
|
.report_associated = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[106:74;2;74u", actual);
|
|
}
|
|
|
|
test "kitty: alternates omit control characters" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .delete,
|
|
.mods = .{},
|
|
.utf8 = &.{0x7F},
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_alternates = true,
|
|
.report_all = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("\x1b[3~", actual);
|
|
}
|
|
|
|
test "kitty: enter with utf8 (dead key state)" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .enter,
|
|
.utf8 = "A",
|
|
.unshifted_codepoint = 0x0D,
|
|
},
|
|
.kitty_flags = .{
|
|
.disambiguate = true,
|
|
.report_alternates = true,
|
|
.report_all = true,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.kitty(&buf);
|
|
try testing.expectEqualStrings("A", actual);
|
|
}
|
|
|
|
test "legacy: enter with utf8 (dead key state)" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .enter,
|
|
.utf8 = "A",
|
|
.unshifted_codepoint = 0x0D,
|
|
},
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("A", actual);
|
|
}
|
|
|
|
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: alt+c" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .c,
|
|
.utf8 = "c",
|
|
.mods = .{ .alt = true },
|
|
},
|
|
.alt_esc_prefix = true,
|
|
.macos_option_as_alt = .true,
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1Bc", actual);
|
|
}
|
|
|
|
test "legacy: alt+e only unshifted" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .e,
|
|
.unshifted_codepoint = 'e',
|
|
.mods = .{ .alt = true },
|
|
},
|
|
.alt_esc_prefix = true,
|
|
.macos_option_as_alt = .true,
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1Be", actual);
|
|
}
|
|
|
|
test "legacy: alt+x macos" {
|
|
if (comptime !builtin.target.isDarwin()) return error.SkipZigTest;
|
|
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .c,
|
|
.utf8 = "≈",
|
|
.unshifted_codepoint = 'c',
|
|
.mods = .{ .alt = true },
|
|
},
|
|
.alt_esc_prefix = true,
|
|
.macos_option_as_alt = .true,
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1Bc", actual);
|
|
}
|
|
|
|
test "legacy: shift+alt+. macos" {
|
|
if (comptime !builtin.target.isDarwin()) return error.SkipZigTest;
|
|
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .period,
|
|
.utf8 = ">",
|
|
.unshifted_codepoint = '.',
|
|
.mods = .{ .alt = true, .shift = true },
|
|
},
|
|
.alt_esc_prefix = true,
|
|
.macos_option_as_alt = .true,
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1B>", actual);
|
|
}
|
|
|
|
test "legacy: alt+ф" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .f,
|
|
.utf8 = "ф",
|
|
.mods = .{ .alt = true },
|
|
},
|
|
.alt_esc_prefix = true,
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("ф", 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 "legacy: fixterm awkward letters" {
|
|
var buf: [128]u8 = undefined;
|
|
{
|
|
var enc: KeyEncoder = .{ .event = .{
|
|
.key = .i,
|
|
.mods = .{ .ctrl = true },
|
|
.utf8 = "i",
|
|
} };
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[105;5u", actual);
|
|
}
|
|
{
|
|
var enc: KeyEncoder = .{ .event = .{
|
|
.key = .m,
|
|
.mods = .{ .ctrl = true },
|
|
.utf8 = "m",
|
|
} };
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[109;5u", actual);
|
|
}
|
|
{
|
|
var enc: KeyEncoder = .{ .event = .{
|
|
.key = .left_bracket,
|
|
.mods = .{ .ctrl = true },
|
|
.utf8 = "[",
|
|
} };
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[91;5u", actual);
|
|
}
|
|
{
|
|
// This doesn't exactly match the fixterm spec but matches the
|
|
// behavior of Kitty.
|
|
var enc: KeyEncoder = .{ .event = .{
|
|
.key = .two,
|
|
.mods = .{ .ctrl = true, .shift = true },
|
|
.utf8 = "@",
|
|
} };
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[64;6u", actual);
|
|
}
|
|
}
|
|
|
|
test "legacy: shift+function key should use all mods" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .up,
|
|
.mods = .{ .shift = true },
|
|
.consumed_mods = .{ .shift = true },
|
|
},
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[1;2A", actual);
|
|
}
|
|
|
|
test "legacy: keypad enter" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .kp_enter,
|
|
.mods = .{},
|
|
.consumed_mods = .{},
|
|
},
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\r", actual);
|
|
}
|
|
|
|
test "legacy: f1" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .f1,
|
|
.mods = .{ .ctrl = true },
|
|
.consumed_mods = .{},
|
|
},
|
|
};
|
|
|
|
// F1
|
|
{
|
|
enc.event.key = .f1;
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[1;5P", actual);
|
|
}
|
|
|
|
// F2
|
|
{
|
|
enc.event.key = .f2;
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[1;5Q", actual);
|
|
}
|
|
|
|
// F3
|
|
{
|
|
enc.event.key = .f3;
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[1;5R", actual);
|
|
}
|
|
|
|
// F4
|
|
{
|
|
enc.event.key = .f4;
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[1;5S", actual);
|
|
}
|
|
|
|
// F5 uses new encoding
|
|
{
|
|
enc.event.key = .f5;
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[15;5~", actual);
|
|
}
|
|
}
|
|
|
|
test "legacy: left_shift+tab" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .tab,
|
|
.mods = .{
|
|
.shift = true,
|
|
.sides = .{ .shift = .left },
|
|
},
|
|
},
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[Z", actual);
|
|
}
|
|
|
|
test "legacy: right_shift+tab" {
|
|
var buf: [128]u8 = undefined;
|
|
var enc: KeyEncoder = .{
|
|
.event = .{
|
|
.key = .tab,
|
|
.mods = .{
|
|
.shift = true,
|
|
.sides = .{ .shift = .right },
|
|
},
|
|
},
|
|
};
|
|
|
|
const actual = try enc.legacy(&buf);
|
|
try testing.expectEqualStrings("\x1b[Z", 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);
|
|
}
|