mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #578 from rockorager/dev
Kitty keyboard improvements
This commit is contained in:
@ -757,8 +757,8 @@ fn keyEvent(
|
||||
// Norwegian, and French layouts and thats what we have real users for
|
||||
// right now.
|
||||
const lower = c.gdk_keyval_to_lower(keyval);
|
||||
if (std.math.cast(u21, lower)) |val| break :unshifted val;
|
||||
break :unshifted 0;
|
||||
const lower_unicode = c.gdk_keyval_to_unicode(lower);
|
||||
break :unshifted std.math.cast(u21, lower_unicode) orelse 0;
|
||||
};
|
||||
|
||||
// We always reset our committed text when ending a keypress so that
|
||||
|
@ -143,12 +143,23 @@ fn kitty(
|
||||
}
|
||||
|
||||
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();
|
||||
const cp = it.nextCodepoint() orelse break :alternates;
|
||||
if (it.nextCodepoint() != null) break :alternates;
|
||||
if (cp != seq.key and !isControl(cp)) {
|
||||
seq.alternates = &.{cp};
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -553,7 +564,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).
|
||||
@ -581,7 +592,15 @@ 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();
|
||||
@ -901,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.
|
||||
|
@ -298,154 +298,138 @@ pub const Key = enum(c_int) {
|
||||
/// are independent of the physical key.
|
||||
pub fn fromASCII(ch: u8) ?Key {
|
||||
return switch (ch) {
|
||||
'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,
|
||||
'0' => .zero,
|
||||
'1' => .one,
|
||||
'2' => .two,
|
||||
'3' => .three,
|
||||
'4' => .four,
|
||||
'5' => .five,
|
||||
'6' => .six,
|
||||
'7' => .seven,
|
||||
'8' => .eight,
|
||||
'9' => .nine,
|
||||
';' => .semicolon,
|
||||
' ' => .space,
|
||||
'\'' => .apostrophe,
|
||||
',' => .comma,
|
||||
'`' => .grave_accent,
|
||||
'.' => .period,
|
||||
'/' => .slash,
|
||||
'-' => .minus,
|
||||
'=' => .equal,
|
||||
'[' => .left_bracket,
|
||||
']' => .right_bracket,
|
||||
'\\' => .backslash,
|
||||
else => null,
|
||||
inline else => |comptime_ch| {
|
||||
return comptime result: {
|
||||
@setEvalBranchQuota(100_000);
|
||||
for (codepoint_map) |entry| {
|
||||
if (entry[0] == @as(u21, @intCast(comptime_ch))) {
|
||||
break :result entry[1];
|
||||
}
|
||||
}
|
||||
|
||||
break :result null;
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// True if this key represents a printable character.
|
||||
pub fn printable(self: Key) bool {
|
||||
return switch (self) {
|
||||
.a,
|
||||
.b,
|
||||
.c,
|
||||
.d,
|
||||
.e,
|
||||
.f,
|
||||
.g,
|
||||
.h,
|
||||
.i,
|
||||
.j,
|
||||
.k,
|
||||
.l,
|
||||
.m,
|
||||
.n,
|
||||
.o,
|
||||
.p,
|
||||
.q,
|
||||
.r,
|
||||
.s,
|
||||
.t,
|
||||
.u,
|
||||
.v,
|
||||
.w,
|
||||
.x,
|
||||
.y,
|
||||
.z,
|
||||
.zero,
|
||||
.one,
|
||||
.two,
|
||||
.three,
|
||||
.four,
|
||||
.five,
|
||||
.six,
|
||||
.seven,
|
||||
.eight,
|
||||
.nine,
|
||||
.semicolon,
|
||||
.space,
|
||||
.apostrophe,
|
||||
.comma,
|
||||
.grave_accent,
|
||||
.period,
|
||||
.slash,
|
||||
.minus,
|
||||
.equal,
|
||||
.left_bracket,
|
||||
.right_bracket,
|
||||
.backslash,
|
||||
.kp_0,
|
||||
.kp_1,
|
||||
.kp_2,
|
||||
.kp_3,
|
||||
.kp_4,
|
||||
.kp_5,
|
||||
.kp_6,
|
||||
.kp_7,
|
||||
.kp_8,
|
||||
.kp_9,
|
||||
.kp_decimal,
|
||||
.kp_divide,
|
||||
.kp_multiply,
|
||||
.kp_subtract,
|
||||
.kp_add,
|
||||
.kp_equal,
|
||||
=> true,
|
||||
inline else => |tag| {
|
||||
return comptime result: {
|
||||
@setEvalBranchQuota(10_000);
|
||||
for (codepoint_map) |entry| {
|
||||
if (entry[1] == tag) break :result true;
|
||||
}
|
||||
|
||||
else => false,
|
||||
break :result false;
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if this is a keypad key.
|
||||
pub fn keypad(self: Key) bool {
|
||||
return switch (self) {
|
||||
.kp_0,
|
||||
.kp_1,
|
||||
.kp_2,
|
||||
.kp_3,
|
||||
.kp_4,
|
||||
.kp_5,
|
||||
.kp_6,
|
||||
.kp_7,
|
||||
.kp_8,
|
||||
.kp_9,
|
||||
.kp_decimal,
|
||||
.kp_divide,
|
||||
.kp_multiply,
|
||||
.kp_subtract,
|
||||
.kp_add,
|
||||
.kp_enter,
|
||||
.kp_equal,
|
||||
=> true,
|
||||
|
||||
else => false,
|
||||
inline else => |tag| {
|
||||
const name = @tagName(tag);
|
||||
const result = comptime std.mem.startsWith(u8, name, "kp_");
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Returns the codepoint representing this key, or null if the key is not
|
||||
// printable
|
||||
pub fn codepoint(self: Key) ?u21 {
|
||||
return switch (self) {
|
||||
inline else => |tag| {
|
||||
return comptime result: {
|
||||
@setEvalBranchQuota(10_000);
|
||||
for (codepoint_map) |entry| {
|
||||
if (entry[1] == tag) break :result entry[0];
|
||||
}
|
||||
|
||||
break :result null;
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test "keypad keys" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(Key.kp_0.keypad());
|
||||
try testing.expect(!Key.one.keypad());
|
||||
}
|
||||
|
||||
const codepoint_map: []const struct { u21, Key } = &.{
|
||||
.{ '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 },
|
||||
.{ '0', .zero },
|
||||
.{ '1', .one },
|
||||
.{ '2', .two },
|
||||
.{ '3', .three },
|
||||
.{ '4', .four },
|
||||
.{ '5', .five },
|
||||
.{ '6', .six },
|
||||
.{ '7', .seven },
|
||||
.{ '8', .eight },
|
||||
.{ '9', .nine },
|
||||
.{ ';', .semicolon },
|
||||
.{ ' ', .space },
|
||||
.{ '\'', .apostrophe },
|
||||
.{ ',', .comma },
|
||||
.{ '`', .grave_accent },
|
||||
.{ '.', .period },
|
||||
.{ '/', .slash },
|
||||
.{ '-', .minus },
|
||||
.{ '=', .equal },
|
||||
.{ '[', .left_bracket },
|
||||
.{ ']', .right_bracket },
|
||||
.{ '\\', .backslash },
|
||||
|
||||
// Keypad entries. We just assume keypad with the kp_ prefix
|
||||
// so that has some special meaning. These must also always be last.
|
||||
.{ '0', .kp_0 },
|
||||
.{ '1', .kp_1 },
|
||||
.{ '2', .kp_2 },
|
||||
.{ '3', .kp_3 },
|
||||
.{ '4', .kp_4 },
|
||||
.{ '5', .kp_5 },
|
||||
.{ '6', .kp_6 },
|
||||
.{ '7', .kp_7 },
|
||||
.{ '8', .kp_8 },
|
||||
.{ '9', .kp_9 },
|
||||
.{ '.', .kp_decimal },
|
||||
.{ '/', .kp_divide },
|
||||
.{ '*', .kp_multiply },
|
||||
.{ '-', .kp_subtract },
|
||||
.{ '+', .kp_add },
|
||||
.{ '=', .kp_equal },
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user