434 lines
14 KiB
Zig
434 lines
14 KiB
Zig
const std = @import("std");
|
|
const testing = std.testing;
|
|
|
|
const Key = @This();
|
|
|
|
/// Modifier Keys for a Key Match Event.
|
|
pub const Modifiers = 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,
|
|
};
|
|
|
|
/// Flags for the Kitty Protocol.
|
|
pub const KittyFlags = packed struct(u5) {
|
|
disambiguate: bool = true,
|
|
report_events: bool = false,
|
|
report_alternate_keys: bool = true,
|
|
report_all_as_ctl_seqs: bool = true,
|
|
report_text: bool = true,
|
|
};
|
|
|
|
/// the unicode codepoint of the key event.
|
|
codepoint: u21,
|
|
|
|
/// the text generated from the key event. The underlying slice has a limited
|
|
/// lifetime. Vaxis maintains an internal ring buffer to temporarily store text.
|
|
/// If the application needs these values longer than the lifetime of the event
|
|
/// it must copy the data.
|
|
text: ?[]const u8 = null,
|
|
|
|
/// the shifted codepoint of this key event. This will only be present if the
|
|
/// Shift modifier was used to generate the event
|
|
shifted_codepoint: ?u21 = null,
|
|
|
|
/// the key that would have been pressed on a standard keyboard layout. This is
|
|
/// useful for shortcut matching
|
|
base_layout_codepoint: ?u21 = null,
|
|
|
|
mods: Modifiers = .{},
|
|
|
|
// matches follows a loose matching algorithm for key matches.
|
|
// 1. If the codepoint and modifiers are exact matches, after removing caps_lock
|
|
// and num_lock
|
|
// 2. If the utf8 encoding of the codepoint matches the text, after removing
|
|
// num_lock
|
|
// 3. If there is a shifted codepoint and it matches after removing the shift
|
|
// modifier from self, after removing caps_lock and num_lock
|
|
pub fn matches(self: Key, cp: u21, mods: Modifiers) bool {
|
|
// rule 1
|
|
if (self.matchExact(cp, mods)) return true;
|
|
|
|
// rule 2
|
|
if (self.matchText(cp, mods)) return true;
|
|
|
|
// rule 3
|
|
if (self.matchShiftedCodepoint(cp, mods)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/// matches against any of the provided codepoints.
|
|
pub fn matchesAny(self: Key, cps: []const u21, mods: Modifiers) bool {
|
|
for (cps) |cp| {
|
|
if (self.matches(cp, mods)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// matches base layout codes, useful for shortcut matching when an alternate key
|
|
/// layout is used
|
|
pub fn matchShortcut(self: Key, cp: u21, mods: Modifiers) bool {
|
|
if (self.base_layout_codepoint == null) return false;
|
|
return cp == self.base_layout_codepoint.? and std.meta.eql(self.mods, mods);
|
|
}
|
|
|
|
/// matches keys that aren't upper case versions when shifted. For example, shift
|
|
/// + semicolon produces a colon. The key can be matched against shift +
|
|
/// semicolon or just colon...or shift + ctrl + ; or just ctrl + :
|
|
pub fn matchShiftedCodepoint(self: Key, cp: u21, mods: Modifiers) bool {
|
|
if (self.shifted_codepoint == null) return false;
|
|
if (!self.mods.shift) return false;
|
|
var self_mods = self.mods;
|
|
self_mods.shift = false;
|
|
self_mods.caps_lock = false;
|
|
self_mods.num_lock = false;
|
|
var tgt_mods = mods;
|
|
tgt_mods.caps_lock = false;
|
|
tgt_mods.num_lock = false;
|
|
return cp == self.shifted_codepoint.? and std.meta.eql(self_mods, mods);
|
|
}
|
|
|
|
/// matches when the utf8 encoding of the codepoint and relevant mods matches the
|
|
/// text of the key. This function will consume Shift and Caps Lock when matching
|
|
pub fn matchText(self: Key, cp: u21, mods: Modifiers) bool {
|
|
// return early if we have no text
|
|
if (self.text == null) return false;
|
|
|
|
var self_mods = self.mods;
|
|
self_mods.num_lock = false;
|
|
self_mods.shift = false;
|
|
self_mods.caps_lock = false;
|
|
var arg_mods = mods;
|
|
arg_mods.num_lock = false;
|
|
arg_mods.shift = false;
|
|
arg_mods.caps_lock = false;
|
|
|
|
var buf: [4]u8 = undefined;
|
|
const n = std.unicode.utf8Encode(cp, buf[0..]) catch return false;
|
|
return std.mem.eql(u8, self.text.?, buf[0..n]) and std.meta.eql(self_mods, arg_mods);
|
|
}
|
|
|
|
// The key must exactly match the codepoint and modifiers. caps_lock and
|
|
// num_lock are removed before matching
|
|
pub fn matchExact(self: Key, cp: u21, mods: Modifiers) bool {
|
|
var self_mods = self.mods;
|
|
self_mods.caps_lock = false;
|
|
self_mods.num_lock = false;
|
|
var tgt_mods = mods;
|
|
tgt_mods.caps_lock = false;
|
|
tgt_mods.num_lock = false;
|
|
return self.codepoint == cp and std.meta.eql(self_mods, tgt_mods);
|
|
}
|
|
|
|
/// True if the key is a single modifier (ie: left_shift)
|
|
pub fn isModifier(self: Key) bool {
|
|
return self.codepoint == left_shift or
|
|
self.codepoint == left_alt or
|
|
self.codepoint == left_super or
|
|
self.codepoint == left_hyper or
|
|
self.codepoint == left_control or
|
|
self.codepoint == left_meta or
|
|
self.codepoint == right_shift or
|
|
self.codepoint == right_alt or
|
|
self.codepoint == right_super or
|
|
self.codepoint == right_hyper or
|
|
self.codepoint == right_control or
|
|
self.codepoint == right_meta;
|
|
}
|
|
|
|
// a few special keys that we encode as their actual ascii value
|
|
pub const tab: u21 = 0x09;
|
|
pub const enter: u21 = 0x0D;
|
|
pub const escape: u21 = 0x1B;
|
|
pub const space: u21 = 0x20;
|
|
pub const backspace: u21 = 0x7F;
|
|
|
|
/// multicodepoint is a key which generated text but cannot be expressed as a
|
|
/// single codepoint. The value is the maximum unicode codepoint + 1
|
|
pub const multicodepoint: u21 = 1_114_112 + 1;
|
|
|
|
// kitty encodes these keys directly in the private use area. We reuse those
|
|
// mappings
|
|
pub const insert: u21 = 57348;
|
|
pub const delete: u21 = 57349;
|
|
pub const left: u21 = 57350;
|
|
pub const right: u21 = 57351;
|
|
pub const up: u21 = 57352;
|
|
pub const down: u21 = 57353;
|
|
pub const page_up: u21 = 57354;
|
|
pub const page_down: u21 = 57355;
|
|
pub const home: u21 = 57356;
|
|
pub const end: u21 = 57357;
|
|
pub const caps_lock: u21 = 57358;
|
|
pub const scroll_lock: u21 = 57359;
|
|
pub const num_lock: u21 = 57360;
|
|
pub const print_screen: u21 = 57361;
|
|
pub const pause: u21 = 57362;
|
|
pub const menu: u21 = 57363;
|
|
pub const f1: u21 = 57364;
|
|
pub const f2: u21 = 57365;
|
|
pub const f3: u21 = 57366;
|
|
pub const f4: u21 = 57367;
|
|
pub const f5: u21 = 57368;
|
|
pub const f6: u21 = 57369;
|
|
pub const f7: u21 = 57370;
|
|
pub const f8: u21 = 57371;
|
|
pub const f9: u21 = 57372;
|
|
pub const f10: u21 = 57373;
|
|
pub const f11: u21 = 57374;
|
|
pub const f12: u21 = 57375;
|
|
pub const f13: u21 = 57376;
|
|
pub const f14: u21 = 57377;
|
|
pub const f15: u21 = 57378;
|
|
pub const @"f16": u21 = 57379;
|
|
pub const f17: u21 = 57380;
|
|
pub const f18: u21 = 57381;
|
|
pub const f19: u21 = 57382;
|
|
pub const f20: u21 = 57383;
|
|
pub const f21: u21 = 57384;
|
|
pub const f22: u21 = 57385;
|
|
pub const f23: u21 = 57386;
|
|
pub const f24: u21 = 57387;
|
|
pub const f25: u21 = 57388;
|
|
pub const f26: u21 = 57389;
|
|
pub const f27: u21 = 57390;
|
|
pub const f28: u21 = 57391;
|
|
pub const f29: u21 = 57392;
|
|
pub const f30: u21 = 57393;
|
|
pub const f31: u21 = 57394;
|
|
pub const @"f32": u21 = 57395;
|
|
pub const f33: u21 = 57396;
|
|
pub const f34: u21 = 57397;
|
|
pub const f35: u21 = 57398;
|
|
pub const kp_0: u21 = 57399;
|
|
pub const kp_1: u21 = 57400;
|
|
pub const kp_2: u21 = 57401;
|
|
pub const kp_3: u21 = 57402;
|
|
pub const kp_4: u21 = 57403;
|
|
pub const kp_5: u21 = 57404;
|
|
pub const kp_6: u21 = 57405;
|
|
pub const kp_7: u21 = 57406;
|
|
pub const kp_8: u21 = 57407;
|
|
pub const kp_9: u21 = 57408;
|
|
pub const kp_decimal: u21 = 57409;
|
|
pub const kp_divide: u21 = 57410;
|
|
pub const kp_multiply: u21 = 57411;
|
|
pub const kp_subtract: u21 = 57412;
|
|
pub const kp_add: u21 = 57413;
|
|
pub const kp_enter: u21 = 57414;
|
|
pub const kp_equal: u21 = 57415;
|
|
pub const kp_separator: u21 = 57416;
|
|
pub const kp_left: u21 = 57417;
|
|
pub const kp_right: u21 = 57418;
|
|
pub const kp_up: u21 = 57419;
|
|
pub const kp_down: u21 = 57420;
|
|
pub const kp_page_up: u21 = 57421;
|
|
pub const kp_page_down: u21 = 57422;
|
|
pub const kp_home: u21 = 57423;
|
|
pub const kp_end: u21 = 57424;
|
|
pub const kp_insert: u21 = 57425;
|
|
pub const kp_delete: u21 = 57426;
|
|
pub const kp_begin: u21 = 57427;
|
|
pub const media_play: u21 = 57428;
|
|
pub const media_pause: u21 = 57429;
|
|
pub const media_play_pause: u21 = 57430;
|
|
pub const media_reverse: u21 = 57431;
|
|
pub const media_stop: u21 = 57432;
|
|
pub const media_fast_forward: u21 = 57433;
|
|
pub const media_rewind: u21 = 57434;
|
|
pub const media_track_next: u21 = 57435;
|
|
pub const media_track_previous: u21 = 57436;
|
|
pub const media_record: u21 = 57437;
|
|
pub const lower_volume: u21 = 57438;
|
|
pub const raise_volume: u21 = 57439;
|
|
pub const mute_volume: u21 = 57440;
|
|
pub const left_shift: u21 = 57441;
|
|
pub const left_control: u21 = 57442;
|
|
pub const left_alt: u21 = 57443;
|
|
pub const left_super: u21 = 57444;
|
|
pub const left_hyper: u21 = 57445;
|
|
pub const left_meta: u21 = 57446;
|
|
pub const right_shift: u21 = 57447;
|
|
pub const right_control: u21 = 57448;
|
|
pub const right_alt: u21 = 57449;
|
|
pub const right_super: u21 = 57450;
|
|
pub const right_hyper: u21 = 57451;
|
|
pub const right_meta: u21 = 57452;
|
|
pub const iso_level_3_shift: u21 = 57453;
|
|
pub const iso_level_5_shift: u21 = 57454;
|
|
|
|
pub const name_map = blk: {
|
|
@setEvalBranchQuota(2000);
|
|
break :blk std.ComptimeStringMap(u21, .{
|
|
// common names
|
|
.{ "plus", '+' },
|
|
.{ "minus", '-' },
|
|
.{ "colon", ':' },
|
|
.{ "semicolon", ';' },
|
|
.{ "comma", ',' },
|
|
|
|
// special keys
|
|
.{ "insert", insert },
|
|
.{ "delete", delete },
|
|
.{ "left", left },
|
|
.{ "right", right },
|
|
.{ "up", up },
|
|
.{ "down", down },
|
|
.{ "page_up", page_up },
|
|
.{ "page_down", page_down },
|
|
.{ "home", home },
|
|
.{ "end", end },
|
|
.{ "caps_lock", caps_lock },
|
|
.{ "scroll_lock", scroll_lock },
|
|
.{ "num_lock", num_lock },
|
|
.{ "print_screen", print_screen },
|
|
.{ "pause", pause },
|
|
.{ "menu", menu },
|
|
.{ "f1", f1 },
|
|
.{ "f2", f2 },
|
|
.{ "f3", f3 },
|
|
.{ "f4", f4 },
|
|
.{ "f5", f5 },
|
|
.{ "f6", f6 },
|
|
.{ "f7", f7 },
|
|
.{ "f8", f8 },
|
|
.{ "f9", f9 },
|
|
.{ "f10", f10 },
|
|
.{ "f11", f11 },
|
|
.{ "f12", f12 },
|
|
.{ "f13", f13 },
|
|
.{ "f14", f14 },
|
|
.{ "f15", f15 },
|
|
.{ "f16", @"f16" },
|
|
.{ "f17", f17 },
|
|
.{ "f18", f18 },
|
|
.{ "f19", f19 },
|
|
.{ "f20", f20 },
|
|
.{ "f21", f21 },
|
|
.{ "f22", f22 },
|
|
.{ "f23", f23 },
|
|
.{ "f24", f24 },
|
|
.{ "f25", f25 },
|
|
.{ "f26", f26 },
|
|
.{ "f27", f27 },
|
|
.{ "f28", f28 },
|
|
.{ "f29", f29 },
|
|
.{ "f30", f30 },
|
|
.{ "f31", f31 },
|
|
.{ "f32", @"f32" },
|
|
.{ "f33", f33 },
|
|
.{ "f34", f34 },
|
|
.{ "f35", f35 },
|
|
.{ "kp_0", kp_0 },
|
|
.{ "kp_1", kp_1 },
|
|
.{ "kp_2", kp_2 },
|
|
.{ "kp_3", kp_3 },
|
|
.{ "kp_4", kp_4 },
|
|
.{ "kp_5", kp_5 },
|
|
.{ "kp_6", kp_6 },
|
|
.{ "kp_7", kp_7 },
|
|
.{ "kp_8", kp_8 },
|
|
.{ "kp_9", kp_9 },
|
|
.{ "kp_decimal", kp_decimal },
|
|
.{ "kp_divide", kp_divide },
|
|
.{ "kp_multiply", kp_multiply },
|
|
.{ "kp_subtract", kp_subtract },
|
|
.{ "kp_add", kp_add },
|
|
.{ "kp_enter", kp_enter },
|
|
.{ "kp_equal", kp_equal },
|
|
.{ "kp_separator", kp_separator },
|
|
.{ "kp_left", kp_left },
|
|
.{ "kp_right", kp_right },
|
|
.{ "kp_up", kp_up },
|
|
.{ "kp_down", kp_down },
|
|
.{ "kp_page_up", kp_page_up },
|
|
.{ "kp_page_down", kp_page_down },
|
|
.{ "kp_home", kp_home },
|
|
.{ "kp_end", kp_end },
|
|
.{ "kp_insert", kp_insert },
|
|
.{ "kp_delete", kp_delete },
|
|
.{ "kp_begin", kp_begin },
|
|
.{ "media_play", media_play },
|
|
.{ "media_pause", media_pause },
|
|
.{ "media_play_pause", media_play_pause },
|
|
.{ "media_reverse", media_reverse },
|
|
.{ "media_stop", media_stop },
|
|
.{ "media_fast_forward", media_fast_forward },
|
|
.{ "media_rewind", media_rewind },
|
|
.{ "media_track_next", media_track_next },
|
|
.{ "media_track_previous", media_track_previous },
|
|
.{ "media_record", media_record },
|
|
.{ "lower_volume", lower_volume },
|
|
.{ "raise_volume", raise_volume },
|
|
.{ "mute_volume", mute_volume },
|
|
.{ "left_shift", left_shift },
|
|
.{ "left_control", left_control },
|
|
.{ "left_alt", left_alt },
|
|
.{ "left_super", left_super },
|
|
.{ "left_hyper", left_hyper },
|
|
.{ "left_meta", left_meta },
|
|
.{ "right_shift", right_shift },
|
|
.{ "right_control", right_control },
|
|
.{ "right_alt", right_alt },
|
|
.{ "right_super", right_super },
|
|
.{ "right_hyper", right_hyper },
|
|
.{ "right_meta", right_meta },
|
|
.{ "iso_level_3_shift", iso_level_3_shift },
|
|
.{ "iso_level_5_shift", iso_level_5_shift },
|
|
});
|
|
};
|
|
|
|
test "matches 'a'" {
|
|
const key: Key = .{
|
|
.codepoint = 'a',
|
|
.mods = .{ .num_lock = true },
|
|
};
|
|
try testing.expect(key.matches('a', .{}));
|
|
}
|
|
|
|
test "matches 'shift+a'" {
|
|
const key: Key = .{
|
|
.codepoint = 'a',
|
|
.mods = .{ .shift = true },
|
|
.text = "A",
|
|
};
|
|
try testing.expect(key.matches('a', .{ .shift = true }));
|
|
try testing.expect(key.matches('A', .{}));
|
|
try testing.expect(!key.matches('A', .{ .ctrl = true }));
|
|
}
|
|
|
|
test "matches 'shift+tab'" {
|
|
const key: Key = .{
|
|
.codepoint = Key.tab,
|
|
.mods = .{ .shift = true, .num_lock = true },
|
|
};
|
|
try testing.expect(key.matches(Key.tab, .{ .shift = true }));
|
|
try testing.expect(!key.matches(Key.tab, .{}));
|
|
}
|
|
|
|
test "matches 'shift+;'" {
|
|
const key: Key = .{
|
|
.codepoint = ';',
|
|
.shifted_codepoint = ':',
|
|
.mods = .{ .shift = true },
|
|
.text = ":",
|
|
};
|
|
try testing.expect(key.matches(';', .{ .shift = true }));
|
|
try testing.expect(key.matches(':', .{}));
|
|
|
|
const colon: Key = .{
|
|
.codepoint = ':',
|
|
.mods = .{},
|
|
};
|
|
try testing.expect(colon.matches(':', .{}));
|
|
}
|
|
|
|
test "name_map" {
|
|
try testing.expectEqual(insert, name_map.get("insert"));
|
|
}
|