mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
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:
@ -143,12 +143,25 @@ fn kitty(
|
||||
}
|
||||
|
||||
if (self.kitty_flags.report_alternates) alternates: {
|
||||
const view = try std.unicode.Utf8View.init(self.event.utf8);
|
||||
var it = view.iterator();
|
||||
const cp = it.nextCodepoint() orelse break :alternates;
|
||||
if (it.nextCodepoint() != null) break :alternates;
|
||||
if (cp != seq.key and !isControl(cp)) {
|
||||
seq.alternates = &.{cp};
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -549,7 +562,7 @@ const KittySequence = struct {
|
||||
final: u8,
|
||||
mods: KittyMods = .{},
|
||||
event: Event = .none,
|
||||
alternates: []const u21 = &.{},
|
||||
alternates: [2]?u21 = .{ null, null },
|
||||
text: []const u8 = "",
|
||||
|
||||
/// Values for the event code (see "event-type" in above comment).
|
||||
@ -577,7 +590,17 @@ const KittySequence = struct {
|
||||
|
||||
// Key section
|
||||
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
|
||||
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
|
||||
// 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);
|
||||
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.
|
||||
|
@ -448,4 +448,76 @@ pub const Key = enum(c_int) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user