mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #295 from mitchellh/kitty-keys
Kitty Keyboard Protocol
This commit is contained in:
@ -1037,11 +1037,12 @@ pub fn keyCallback(
|
||||
.cursor_key_application = t.modes.get(.cursor_keys),
|
||||
.keypad_key_application = t.modes.get(.keypad_keys),
|
||||
.modify_other_keys_state_2 = t.flags.modify_other_keys_2,
|
||||
.kitty_flags = t.screen.kitty_keyboard.current(),
|
||||
};
|
||||
};
|
||||
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
const seq = try enc.legacy(&data);
|
||||
const seq = try enc.encode(&data);
|
||||
if (seq.len == 0) return false;
|
||||
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
|
@ -458,6 +458,26 @@ pub const Surface = struct {
|
||||
// then we've consumed our translate mods.
|
||||
const consumed_mods: input.Mods = if (result.text.len > 0) translate_mods else .{};
|
||||
|
||||
// We need to always do a translation with no modifiers at all in
|
||||
// order to get the "unshifted_codepoint" for the key event.
|
||||
const unshifted_codepoint: u21 = unshifted: {
|
||||
var nomod_buf: [128]u8 = undefined;
|
||||
var nomod_state: input.Keymap.State = undefined;
|
||||
const nomod = try self.app.keymap.translate(
|
||||
&nomod_buf,
|
||||
&nomod_state,
|
||||
@intCast(keycode),
|
||||
.{},
|
||||
);
|
||||
|
||||
const view = std.unicode.Utf8View.init(nomod.text) catch |err| {
|
||||
log.warn("cannot build utf8 view over text: {}", .{err});
|
||||
break :unshifted 0;
|
||||
};
|
||||
var it = view.iterator();
|
||||
break :unshifted it.nextCodepoint() orelse 0;
|
||||
};
|
||||
|
||||
// log.warn("TRANSLATE: action={} keycode={x} dead={} key_len={} key={any} key_str={s} mods={}", .{
|
||||
// action,
|
||||
// keycode,
|
||||
@ -489,18 +509,9 @@ pub const Surface = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// If that doesn't work then we try to translate without
|
||||
// any modifiers and convert that.
|
||||
var nomod_buf: [128]u8 = undefined;
|
||||
var nomod_state: input.Keymap.State = undefined;
|
||||
const nomod = try self.app.keymap.translate(
|
||||
&nomod_buf,
|
||||
&nomod_state,
|
||||
@intCast(keycode),
|
||||
.{},
|
||||
);
|
||||
if (nomod.text.len == 1) {
|
||||
if (input.Key.fromASCII(nomod.text[0])) |key| {
|
||||
// If the above doesn't work, we use the unmodified value.
|
||||
if (std.math.cast(u8, unshifted_codepoint)) |ascii| {
|
||||
if (input.Key.fromASCII(ascii)) |key| {
|
||||
break :key key;
|
||||
}
|
||||
}
|
||||
@ -517,6 +528,7 @@ pub const Surface = struct {
|
||||
.consumed_mods = consumed_mods,
|
||||
.composing = result.composing,
|
||||
.utf8 = result.text,
|
||||
.unshifted_codepoint = unshifted_codepoint,
|
||||
}) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
return;
|
||||
@ -549,6 +561,7 @@ pub const Surface = struct {
|
||||
.consumed_mods = .{},
|
||||
.composing = false,
|
||||
.utf8 = buf[0..len],
|
||||
.unshifted_codepoint = 0,
|
||||
}) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
return;
|
||||
|
@ -1212,6 +1212,31 @@ pub const Surface = struct {
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
const keyval_unicode_unshifted: u21 = unshifted: {
|
||||
var n: c_int = undefined;
|
||||
var keys: [*c]c.GdkKeymapKey = undefined;
|
||||
var keyvals: [*c]c.guint = undefined;
|
||||
if (c.gdk_display_map_keycode(
|
||||
c.gdk_event_get_display(event),
|
||||
keycode,
|
||||
&keys,
|
||||
&keyvals,
|
||||
&n,
|
||||
) == 0) break :unshifted 0;
|
||||
|
||||
defer c.g_free(keys);
|
||||
defer c.g_free(keyvals);
|
||||
for (keys[0..@intCast(n)], 0..) |key, i| {
|
||||
if (key.group == 0 and key.level == 0) {
|
||||
break :unshifted @intCast(c.gdk_keyval_to_unicode(keyvals[i]));
|
||||
}
|
||||
}
|
||||
|
||||
break :unshifted 0;
|
||||
};
|
||||
|
||||
// We always reset our committed text when ending a keypress so that
|
||||
// future keypresses don't think we have a commit event.
|
||||
defer self.im_len = 0;
|
||||
@ -1317,6 +1342,7 @@ pub const Surface = struct {
|
||||
.consumed_mods = consumed_mods,
|
||||
.composing = self.im_composing,
|
||||
.utf8 = self.im_buf[0..self.im_len],
|
||||
.unshifted_codepoint = keyval_unicode_unshifted,
|
||||
}) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
return false;
|
||||
|
@ -5,6 +5,7 @@ pub usingnamespace @import("input/mouse.zig");
|
||||
pub usingnamespace @import("input/key.zig");
|
||||
pub const function_keys = @import("input/function_keys.zig");
|
||||
pub const keycodes = @import("input/keycodes.zig");
|
||||
pub const kitty = @import("input/kitty.zig");
|
||||
pub const Binding = @import("input/Binding.zig");
|
||||
pub const KeyEncoder = @import("input/KeyEncoder.zig");
|
||||
pub const SplitDirection = Binding.Action.SplitDirection;
|
||||
|
@ -10,6 +10,10 @@ const testing = std.testing;
|
||||
|
||||
const key = @import("key.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;
|
||||
|
||||
event: key.KeyEvent,
|
||||
|
||||
@ -18,6 +22,130 @@ 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 {
|
||||
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 != .press 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 "";
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
return try copyToBuf(buf, self.event.utf8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entry = entry_ orelse 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: {
|
||||
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) {
|
||||
seq.alternates = &.{cp};
|
||||
}
|
||||
}
|
||||
|
||||
if (self.kitty_flags.report_associated) {
|
||||
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
|
||||
@ -25,7 +153,7 @@ modify_other_keys_state_2: bool = false,
|
||||
/// 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.
|
||||
pub fn legacy(
|
||||
fn legacy(
|
||||
self: *const KeyEncoder,
|
||||
buf: []u8,
|
||||
) ![]const u8 {
|
||||
@ -312,34 +440,400 @@ const CsiUMods = packed struct(u3) {
|
||||
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());
|
||||
}
|
||||
};
|
||||
|
||||
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());
|
||||
/// 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,
|
||||
|
||||
mods = .{ .shift = true };
|
||||
try testing.expectEqual(@as(u4, 2), mods.seqInt());
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
|
||||
mods = .{ .alt = true };
|
||||
try testing.expectEqual(@as(u4, 3), mods.seqInt());
|
||||
/// Returns the raw int value of this packed struct.
|
||||
pub fn int(self: KittyMods) u8 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
mods = .{ .ctrl = true };
|
||||
try testing.expectEqual(@as(u4, 5), mods.seqInt());
|
||||
/// 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;
|
||||
}
|
||||
|
||||
mods = .{ .alt = true, .shift = true };
|
||||
try testing.expectEqual(@as(u4, 4), mods.seqInt());
|
||||
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 = .{ .ctrl = true, .shift = true };
|
||||
try testing.expectEqual(@as(u4, 6), mods.seqInt());
|
||||
mods = .{ .shift = true };
|
||||
try testing.expectEqual(@as(u9, 2), mods.seqInt());
|
||||
|
||||
mods = .{ .alt = true, .ctrl = true };
|
||||
try testing.expectEqual(@as(u4, 7), mods.seqInt());
|
||||
mods = .{ .alt = true };
|
||||
try testing.expectEqual(@as(u9, 3), mods.seqInt());
|
||||
|
||||
mods = .{ .alt = true, .ctrl = true, .shift = true };
|
||||
try testing.expectEqual(@as(u4, 8), 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: []const u21 = &.{},
|
||||
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});
|
||||
for (self.alternates) |alt| try writer.print(":{d}", .{alt});
|
||||
|
||||
// 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) {
|
||||
// We need to add our ";". We need to add two if we didn't emit
|
||||
// the modifier section.
|
||||
if (!emit_prior) try writer.writeByte(';');
|
||||
try writer.writeByte(';');
|
||||
|
||||
// First one has no prefix
|
||||
const view = try std.unicode.Utf8View.init(self.text);
|
||||
var it = view.iterator();
|
||||
if (it.nextCodepoint()) |cp| {
|
||||
try writer.print("{d}", .{cp});
|
||||
}
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
try writer.print(":{d}", .{cp});
|
||||
}
|
||||
}
|
||||
|
||||
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: 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: 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: 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 },
|
||||
};
|
||||
|
||||
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.
|
||||
const actual = try enc.kitty(&buf);
|
||||
try testing.expectEqualStrings("\x1b[65;2u", actual);
|
||||
}
|
||||
|
||||
test "legacy: ctrl+alt+c" {
|
||||
|
@ -41,6 +41,10 @@ pub const KeyEvent = struct {
|
||||
/// text.
|
||||
utf8: []const u8 = "",
|
||||
|
||||
/// The codepoint for this key when it is unshifted. For example,
|
||||
/// shift+a is "A" in UTF-8 but unshifted would provide 'a'.
|
||||
unshifted_codepoint: u21 = 0,
|
||||
|
||||
/// Returns the effective modifiers for this event. The effective
|
||||
/// modifiers are the mods that should be considered for keybindings.
|
||||
pub fn effectiveMods(self: KeyEvent) Mods {
|
||||
|
117
src/input/kitty.zig
Normal file
117
src/input/kitty.zig
Normal file
@ -0,0 +1,117 @@
|
||||
const std = @import("std");
|
||||
const key = @import("key.zig");
|
||||
|
||||
/// A single entry in the kitty keymap data. There are only ~100 entries
|
||||
/// so the recommendation is to just use a linear search to find the entry
|
||||
/// for a given key.
|
||||
pub const Entry = struct {
|
||||
key: key.Key,
|
||||
code: u21,
|
||||
final: u8,
|
||||
modifier: bool,
|
||||
};
|
||||
|
||||
/// The full list of entries for the current platform.
|
||||
pub const entries: []const Entry = entries: {
|
||||
var result: [raw_entries.len]Entry = undefined;
|
||||
for (raw_entries, 0..) |raw, i| {
|
||||
result[i] = .{
|
||||
.key = raw[0],
|
||||
.code = raw[1],
|
||||
.final = raw[2],
|
||||
.modifier = raw[3],
|
||||
};
|
||||
}
|
||||
break :entries &result;
|
||||
};
|
||||
|
||||
/// Raw entry is the tuple form of an entry for easy human management.
|
||||
/// This should never be used in a real program so it is not pub. For
|
||||
/// real programs, use `entries` which has properly typed, structured data.
|
||||
const RawEntry = struct { key.Key, u21, u8, bool };
|
||||
|
||||
/// The raw data for how to map keys to Kitty data. Based on the information:
|
||||
/// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
|
||||
/// And the exact table is ported from Foot:
|
||||
/// https://codeberg.org/dnkl/foot/src/branch/master/kitty-keymap.h
|
||||
///
|
||||
/// Note that we currently don't support all the same keysyms as Kitty,
|
||||
/// but we can add them as we add support.
|
||||
const raw_entries: []const RawEntry = &.{
|
||||
.{ .backspace, 127, 'u', false },
|
||||
.{ .tab, 9, 'u', false },
|
||||
.{ .enter, 13, 'u', false },
|
||||
.{ .pause, 57362, 'u', false },
|
||||
.{ .scroll_lock, 57359, 'u', false },
|
||||
.{ .escape, 27, 'u', false },
|
||||
.{ .home, 1, 'H', false },
|
||||
.{ .left, 1, 'D', false },
|
||||
.{ .up, 1, 'A', false },
|
||||
.{ .right, 1, 'C', false },
|
||||
.{ .down, 1, 'B', false },
|
||||
.{ .end, 1, 'F', false },
|
||||
.{ .print_screen, 57361, 'u', false },
|
||||
.{ .insert, 2, '~', false },
|
||||
.{ .num_lock, 57360, 'u', true },
|
||||
|
||||
.{ .kp_enter, 57414, 'u', false },
|
||||
.{ .kp_multiply, 57411, 'u', false },
|
||||
.{ .kp_add, 57413, 'u', false },
|
||||
.{ .kp_subtract, 57412, 'u', false },
|
||||
.{ .kp_decimal, 57409, 'u', false },
|
||||
.{ .kp_divide, 57410, 'u', false },
|
||||
.{ .kp_0, 57399, 'u', false },
|
||||
.{ .kp_1, 57400, 'u', false },
|
||||
.{ .kp_2, 57401, 'u', false },
|
||||
.{ .kp_3, 57402, 'u', false },
|
||||
.{ .kp_4, 57403, 'u', false },
|
||||
.{ .kp_5, 57404, 'u', false },
|
||||
.{ .kp_6, 57405, 'u', false },
|
||||
.{ .kp_7, 57406, 'u', false },
|
||||
.{ .kp_8, 57407, 'u', false },
|
||||
.{ .kp_9, 57408, 'u', false },
|
||||
.{ .kp_equal, 57415, 'u', false },
|
||||
|
||||
.{ .f1, 1, 'P', false },
|
||||
.{ .f2, 1, 'Q', false },
|
||||
.{ .f3, 13, '~', false },
|
||||
.{ .f4, 1, 'S', false },
|
||||
.{ .f5, 15, '~', false },
|
||||
.{ .f6, 17, '~', false },
|
||||
.{ .f7, 18, '~', false },
|
||||
.{ .f8, 19, '~', false },
|
||||
.{ .f9, 20, '~', false },
|
||||
.{ .f10, 21, '~', false },
|
||||
.{ .f11, 23, '~', false },
|
||||
.{ .f12, 24, '~', false },
|
||||
.{ .f13, 57376, 'u', false },
|
||||
.{ .f14, 57377, 'u', false },
|
||||
.{ .f15, 57378, 'u', false },
|
||||
.{ .f16, 57379, 'u', false },
|
||||
.{ .f17, 57380, 'u', false },
|
||||
.{ .f18, 57381, 'u', false },
|
||||
.{ .f19, 57382, 'u', false },
|
||||
.{ .f20, 57383, 'u', false },
|
||||
.{ .f21, 57384, 'u', false },
|
||||
.{ .f22, 57385, 'u', false },
|
||||
.{ .f23, 57386, 'u', false },
|
||||
.{ .f24, 57387, 'u', false },
|
||||
.{ .f25, 57388, 'u', false },
|
||||
|
||||
.{ .left_shift, 57441, 'u', true },
|
||||
.{ .right_shift, 57447, 'u', true },
|
||||
.{ .left_control, 57442, 'u', true },
|
||||
.{ .right_control, 57448, 'u', true },
|
||||
.{ .caps_lock, 57358, 'u', true },
|
||||
.{ .left_super, 57444, 'u', true },
|
||||
.{ .right_super, 57450, 'u', true },
|
||||
.{ .left_alt, 57443, 'u', true },
|
||||
.{ .right_alt, 57449, 'u', true },
|
||||
|
||||
.{ .delete, 3, '~', false },
|
||||
};
|
||||
|
||||
test {
|
||||
// To force comptime to test it
|
||||
_ = entries;
|
||||
}
|
@ -58,6 +58,7 @@ const utf8proc = @import("utf8proc");
|
||||
const trace = @import("tracy").trace;
|
||||
const sgr = @import("sgr.zig");
|
||||
const color = @import("color.zig");
|
||||
const kitty = @import("kitty.zig");
|
||||
const point = @import("point.zig");
|
||||
const CircBuf = @import("circ_buf.zig").CircBuf;
|
||||
const Selection = @import("Selection.zig");
|
||||
@ -861,6 +862,9 @@ saved_cursor: Cursor = .{},
|
||||
/// The selection for this screen (if any).
|
||||
selection: ?Selection = null,
|
||||
|
||||
/// The kitty keyboard settings.
|
||||
kitty_keyboard: kitty.KeyFlagStack = .{},
|
||||
|
||||
/// Initialize a new screen.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
|
154
src/terminal/kitty.zig
Normal file
154
src/terminal/kitty.zig
Normal file
@ -0,0 +1,154 @@
|
||||
//! Types and functions related to Kitty protocols.
|
||||
//!
|
||||
//! Documentation for the Kitty keyboard protocol:
|
||||
//! https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Stack for the key flags. This implements the push/pop behavior
|
||||
/// of the CSI > u and CSI < u sequences. We implement the stack as
|
||||
/// fixed size to avoid heap allocation.
|
||||
pub const KeyFlagStack = struct {
|
||||
const len = 8;
|
||||
|
||||
flags: [len]KeyFlags = .{.{}} ** len,
|
||||
idx: u3 = 0,
|
||||
|
||||
/// Return the current stack value
|
||||
pub fn current(self: KeyFlagStack) KeyFlags {
|
||||
return self.flags[self.idx];
|
||||
}
|
||||
|
||||
/// Perform the "set" operation as described in the spec for
|
||||
/// the CSI = u sequence.
|
||||
pub fn set(
|
||||
self: *KeyFlagStack,
|
||||
mode: KeySetMode,
|
||||
v: KeyFlags,
|
||||
) void {
|
||||
switch (mode) {
|
||||
.set => self.flags[self.idx] = v,
|
||||
.@"or" => self.flags[self.idx] = @bitCast(
|
||||
self.flags[self.idx].int() | v.int(),
|
||||
),
|
||||
.not => self.flags[self.idx] = @bitCast(
|
||||
self.flags[self.idx].int() & ~v.int(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new set of flags onto the stack. If the stack is full
|
||||
/// then the oldest entry is evicted.
|
||||
pub fn push(self: *KeyFlagStack, flags: KeyFlags) void {
|
||||
// Overflow and wrap around if we're full, which evicts
|
||||
// the oldest entry.
|
||||
self.idx +%= 1;
|
||||
self.flags[self.idx] = flags;
|
||||
}
|
||||
|
||||
/// Pop `n` entries from the stack. This will just wrap around
|
||||
/// if `n` is greater than the amount in the stack.
|
||||
pub fn pop(self: *KeyFlagStack, n: usize) void {
|
||||
// If n is more than our length then we just reset the stack.
|
||||
// This also avoids a DoS vector where a malicious client
|
||||
// could send a huge number of pop commands to waste cpu.
|
||||
if (n >= self.flags.len) {
|
||||
self.idx = 0;
|
||||
self.flags = .{.{}} ** len;
|
||||
return;
|
||||
}
|
||||
|
||||
for (0..n) |_| {
|
||||
self.flags[self.idx] = .{};
|
||||
self.idx -%= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we the overflow works as expected
|
||||
test {
|
||||
const testing = std.testing;
|
||||
var stack: KeyFlagStack = .{};
|
||||
stack.idx = stack.flags.len - 1;
|
||||
stack.idx +%= 1;
|
||||
try testing.expect(stack.idx == 0);
|
||||
|
||||
stack.idx = 0;
|
||||
stack.idx -%= 1;
|
||||
try testing.expect(stack.idx == stack.flags.len - 1);
|
||||
}
|
||||
};
|
||||
|
||||
/// The possible flags for the Kitty keyboard protocol.
|
||||
pub const KeyFlags = packed struct(u5) {
|
||||
disambiguate: bool = false,
|
||||
report_events: bool = false,
|
||||
report_alternates: bool = false,
|
||||
report_all: bool = false,
|
||||
report_associated: bool = false,
|
||||
|
||||
pub fn int(self: KeyFlags) u5 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
// Its easy to get packed struct ordering wrong so this test checks.
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(
|
||||
@as(u5, 0b1),
|
||||
(KeyFlags{ .disambiguate = true }).int(),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@as(u5, 0b10),
|
||||
(KeyFlags{ .report_events = true }).int(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// The possible modes for setting the key flags.
|
||||
pub const KeySetMode = enum { set, @"or", not };
|
||||
|
||||
test "KeyFlagStack: push pop" {
|
||||
const testing = std.testing;
|
||||
var stack: KeyFlagStack = .{};
|
||||
stack.push(.{ .disambiguate = true });
|
||||
try testing.expectEqual(
|
||||
KeyFlags{ .disambiguate = true },
|
||||
stack.current(),
|
||||
);
|
||||
|
||||
stack.pop(1);
|
||||
try testing.expectEqual(KeyFlags{}, stack.current());
|
||||
}
|
||||
|
||||
test "KeyFlagStack: pop big number" {
|
||||
const testing = std.testing;
|
||||
var stack: KeyFlagStack = .{};
|
||||
stack.pop(100);
|
||||
try testing.expectEqual(KeyFlags{}, stack.current());
|
||||
}
|
||||
|
||||
test "KeyFlagStack: set" {
|
||||
const testing = std.testing;
|
||||
var stack: KeyFlagStack = .{};
|
||||
stack.set(.set, .{ .disambiguate = true });
|
||||
try testing.expectEqual(
|
||||
KeyFlags{ .disambiguate = true },
|
||||
stack.current(),
|
||||
);
|
||||
|
||||
stack.set(.@"or", .{ .report_events = true });
|
||||
try testing.expectEqual(
|
||||
KeyFlags{
|
||||
.disambiguate = true,
|
||||
.report_events = true,
|
||||
},
|
||||
stack.current(),
|
||||
);
|
||||
|
||||
stack.set(.not, .{ .report_events = true });
|
||||
try testing.expectEqual(
|
||||
KeyFlags{ .disambiguate = true },
|
||||
stack.current(),
|
||||
);
|
||||
}
|
@ -7,6 +7,7 @@ const csi = @import("csi.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
pub const point = @import("point.zig");
|
||||
pub const color = @import("color.zig");
|
||||
pub const kitty = @import("kitty.zig");
|
||||
pub const modes = @import("modes.zig");
|
||||
pub const parse_table = @import("parse_table.zig");
|
||||
|
||||
|
@ -4,6 +4,7 @@ const Parser = @import("Parser.zig");
|
||||
const ansi = @import("ansi.zig");
|
||||
const charsets = @import("charsets.zig");
|
||||
const csi = @import("csi.zig");
|
||||
const kitty = @import("kitty.zig");
|
||||
const modes = @import("modes.zig");
|
||||
const osc = @import("osc.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
@ -644,6 +645,76 @@ pub fn Stream(comptime Handler: type) type {
|
||||
),
|
||||
},
|
||||
|
||||
// Kitty keyboard protocol
|
||||
'u' => switch (action.intermediates.len) {
|
||||
1 => switch (action.intermediates[0]) {
|
||||
'?' => if (@hasDecl(T, "queryKittyKeyboard")) {
|
||||
try self.handler.queryKittyKeyboard();
|
||||
},
|
||||
|
||||
'>' => if (@hasDecl(T, "pushKittyKeyboard")) push: {
|
||||
const flags: u5 = if (action.params.len == 1)
|
||||
std.math.cast(u5, action.params[0]) orelse {
|
||||
log.warn("invalid pushKittyKeyboard command: {}", .{action});
|
||||
break :push;
|
||||
}
|
||||
else
|
||||
0;
|
||||
|
||||
try self.handler.pushKittyKeyboard(@bitCast(flags));
|
||||
},
|
||||
|
||||
'<' => if (@hasDecl(T, "popKittyKeyboard")) {
|
||||
const number: u16 = if (action.params.len == 1)
|
||||
action.params[0]
|
||||
else
|
||||
0;
|
||||
|
||||
try self.handler.popKittyKeyboard(number);
|
||||
},
|
||||
|
||||
'=' => if (@hasDecl(T, "setKittyKeyboard")) set: {
|
||||
const flags: u5 = if (action.params.len >= 1)
|
||||
std.math.cast(u5, action.params[0]) orelse {
|
||||
log.warn("invalid setKittyKeyboard command: {}", .{action});
|
||||
break :set;
|
||||
}
|
||||
else
|
||||
0;
|
||||
|
||||
const number: u16 = if (action.params.len >= 2)
|
||||
action.params[1]
|
||||
else
|
||||
1;
|
||||
|
||||
const mode: kitty.KeySetMode = switch (number) {
|
||||
0 => .set,
|
||||
1 => .@"or",
|
||||
2 => .not,
|
||||
else => {
|
||||
log.warn("invalid setKittyKeyboard command: {}", .{action});
|
||||
break :set;
|
||||
},
|
||||
};
|
||||
|
||||
try self.handler.setKittyKeyboard(
|
||||
mode,
|
||||
@bitCast(flags),
|
||||
);
|
||||
},
|
||||
|
||||
else => log.warn(
|
||||
"unknown CSI s with intermediate: {}",
|
||||
.{action},
|
||||
),
|
||||
},
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI u: {}",
|
||||
.{action},
|
||||
),
|
||||
},
|
||||
|
||||
// ICH - Insert Blanks
|
||||
// TODO: test
|
||||
'@' => if (@hasDecl(T, "insertBlanks")) switch (action.params.len) {
|
||||
|
@ -67,8 +67,7 @@ pub const ghostty: Source = .{
|
||||
|
||||
// Full keyboard support using Kitty's keyboard protocol:
|
||||
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||
// Commented out because we don't yet support this.
|
||||
// .{ .name = "fullkbd", .value = .{ .boolean = {} } },
|
||||
.{ .name = "fullkbd", .value = .{ .boolean = {} } },
|
||||
|
||||
// Number of colors in the color palette.
|
||||
.{ .name = "colors", .value = .{ .numeric = 256 } },
|
||||
|
@ -1407,6 +1407,43 @@ const StreamHandler = struct {
|
||||
self.terminal.fullReset();
|
||||
}
|
||||
|
||||
pub fn queryKittyKeyboard(self: *StreamHandler) !void {
|
||||
// log.debug("querying kitty keyboard mode", .{});
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
const resp = try std.fmt.bufPrint(&data, "\x1b[?{}u", .{
|
||||
self.terminal.screen.kitty_keyboard.current().int(),
|
||||
});
|
||||
|
||||
self.messageWriter(.{
|
||||
.write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pushKittyKeyboard(
|
||||
self: *StreamHandler,
|
||||
flags: terminal.kitty.KeyFlags,
|
||||
) !void {
|
||||
// log.debug("pushing kitty keyboard mode: {}", .{flags});
|
||||
self.terminal.screen.kitty_keyboard.push(flags);
|
||||
}
|
||||
|
||||
pub fn popKittyKeyboard(self: *StreamHandler, n: u16) !void {
|
||||
// log.debug("popping kitty keyboard mode", .{});
|
||||
self.terminal.screen.kitty_keyboard.pop(@intCast(n));
|
||||
}
|
||||
|
||||
pub fn setKittyKeyboard(
|
||||
self: *StreamHandler,
|
||||
mode: terminal.kitty.KeySetMode,
|
||||
flags: terminal.kitty.KeyFlags,
|
||||
) !void {
|
||||
// log.debug("setting kitty keyboard mode: {} {}", .{mode, flags});
|
||||
self.terminal.screen.kitty_keyboard.set(mode, flags);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// OSC
|
||||
|
||||
|
Reference in New Issue
Block a user