Merge pull request #295 from mitchellh/kitty-keys

Kitty Keyboard Protocol
This commit is contained in:
Mitchell Hashimoto
2023-08-17 10:12:57 -07:00
committed by GitHub
13 changed files with 957 additions and 35 deletions

View File

@ -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(.{

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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" {

View File

@ -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
View 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;
}

View File

@ -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
View 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(),
);
}

View File

@ -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");

View File

@ -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) {

View File

@ -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 } },

View File

@ -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