input(kitty): fix reporting of alternate keys

Fix reporting of alternate keys when using the kitty protocol. Alternate
keyboard layouts were failing to report the "base layout" key. This
implementation now matches kitty's output 1:1, and has some added unit
tests for cyrillic characters.

This also fixes a bug where a caps_lock modified key would report the
shifted key as well. The protocol explicitly requires that shifted keys
are only reported if the shift modifier is true.
This commit is contained in:
Tim Culverhouse
2023-09-28 12:23:20 -05:00
parent 4f2d67d8f3
commit fb649e689d
2 changed files with 211 additions and 10 deletions

View File

@ -143,12 +143,25 @@ fn kitty(
} }
if (self.kitty_flags.report_alternates) alternates: { if (self.kitty_flags.report_alternates) alternates: {
const view = try std.unicode.Utf8View.init(self.event.utf8); // Break early if this is a control key
var it = view.iterator(); if (isControl(seq.key)) break :alternates;
const cp = it.nextCodepoint() orelse break :alternates;
if (it.nextCodepoint() != null) break :alternates; // Set the first alternate (shifted version)
if (cp != seq.key and !isControl(cp)) { {
seq.alternates = &.{cp}; 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;
}
} }
} }
@ -549,7 +562,7 @@ const KittySequence = struct {
final: u8, final: u8,
mods: KittyMods = .{}, mods: KittyMods = .{},
event: Event = .none, event: Event = .none,
alternates: []const u21 = &.{}, alternates: [2]?u21 = .{ null, null },
text: []const u8 = "", text: []const u8 = "",
/// Values for the event code (see "event-type" in above comment). /// Values for the event code (see "event-type" in above comment).
@ -577,7 +590,17 @@ const KittySequence = struct {
// Key section // Key section
try writer.print("\x1B[{d}", .{self.key}); try writer.print("\x1B[{d}", .{self.key});
for (self.alternates) |alt| try writer.print(":{d}", .{alt}); // 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 // Mods and events section
const mods = self.mods.seqInt(); const mods = self.mods.seqInt();
@ -897,9 +920,115 @@ test "kitty: matching unshifted codepoint" {
// WARNING: This is not a valid encoding. This is a hypothetical encoding // WARNING: This is not a valid encoding. This is a hypothetical encoding
// just to test that our logic is correct around matching unshifted // just to test that our logic is correct around matching unshifted
// codepoints. // codepoints. We get an alternate here because the unshifted_codepoint does
// not match the base key
const actual = try enc.kitty(&buf); const actual = try enc.kitty(&buf);
try testing.expectEqualStrings("\x1b[65;2u", actual); 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. // macOS generates utf8 text for arrow keys.

View File

@ -448,4 +448,76 @@ pub const Key = enum(c_int) {
else => false, else => false,
}; };
} }
// Returns the codepoint representing this key, or null if the key is not
// printable
pub fn codepoint(self: Key) ?u21 {
return switch (self) {
.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',
.zero => '0',
.one => '1',
.two => '2',
.three => '3',
.four => '4',
.five => '5',
.six => '6',
.seven => '7',
.eight => '8',
.nine => '9',
.semicolon => ';',
.space => ' ',
.apostrophe => '\'',
.comma => ',',
.grave_accent => '`',
.period => '.',
.slash => '/',
.minus => '-',
.equal => '=',
.left_bracket => '[',
.right_bracket => ']',
.backslash => '\\',
.kp_0 => '0',
.kp_1 => '1',
.kp_2 => '2',
.kp_3 => '3',
.kp_4 => '4',
.kp_5 => '5',
.kp_6 => '6',
.kp_7 => '7',
.kp_8 => '8',
.kp_9 => '9',
.kp_decimal => '.',
.kp_divide => '/',
.kp_multiply => '*',
.kp_subtract => '-',
.kp_add => '+',
.kp_equal => '=',
else => null,
};
}
}; };