a modern tui library written in zig
1const std = @import("std");
2const testing = std.testing;
3const ziglyph = @import("ziglyph");
4
5const Key = @This();
6
7pub const Modifiers = packed struct(u8) {
8 shift: bool = false,
9 alt: bool = false,
10 ctrl: bool = false,
11 super: bool = false,
12 hyper: bool = false,
13 meta: bool = false,
14 caps_lock: bool = false,
15 num_lock: bool = false,
16};
17
18pub const KittyFlags = packed struct(u5) {
19 disambiguate: bool = true,
20 report_events: bool = false,
21 report_alternate_keys: bool = true,
22 report_all_as_ctl_seqs: bool = true,
23 report_text: bool = true,
24};
25
26/// the unicode codepoint of the key event.
27codepoint: u21,
28
29/// the text generated from the key event. The underlying slice has a limited
30/// lifetime. Vaxis maintains an internal ring buffer to temporarily store text.
31/// If the application needs these values longer than the lifetime of the event
32/// it must copy the data.
33text: ?[]const u8 = null,
34
35/// the shifted codepoint of this key event. This will only be present if the
36/// Shift modifier was used to generate the event
37shifted_codepoint: ?u21 = null,
38
39/// the key that would have been pressed on a standard keyboard layout. This is
40/// useful for shortcut matching
41base_layout_codepoint: ?u21 = null,
42
43mods: Modifiers = .{},
44
45// matches follows a loose matching algorithm for key matches.
46// 1. If the codepoint and modifiers are exact matches
47// 2. If the utf8 encoding of the codepoint matches the text
48// 3. If there is a shifted codepoint and it matches after removing the shift
49// modifier from self
50pub fn matches(self: Key, cp: u21, mods: Modifiers) bool {
51 // rule 1
52 if (self.matchExact(cp, mods)) return true;
53
54 // rule 2
55 if (self.matchText(cp, mods)) return true;
56
57 // rule 3
58 if (self.matchShiftedCodepoint(cp, mods)) return true;
59
60 // rule 4
61 if (self.matchShiftedCodepoint(cp, mods)) return true;
62
63 return false;
64}
65
66// matches base layout codes, useful for shortcut matching when an alternate key
67// layout is used
68pub fn matchShortcut(self: Key, cp: u21, mods: Modifiers) bool {
69 if (self.base_layout_codepoint == null) return false;
70 return cp == self.base_layout_codepoint.? and std.meta.eql(self.mods, mods);
71}
72
73// matches keys that aren't upper case versions when shifted. For example, shift
74// + semicolon produces a colon. The key can be matched against shift +
75// semicolon or just colon...or shift + ctrl + ; or just ctrl + :
76pub fn matchShiftedCodepoint(self: Key, cp: u21, mods: Modifiers) bool {
77 if (self.shifted_codepoint == null) return false;
78 if (!self.mods.shift) return false;
79 var self_mods = self.mods;
80 self_mods.shift = false;
81 return cp == self.shifted_codepoint.? and std.meta.eql(self_mods, mods);
82}
83
84// matches when the utf8 encoding of the codepoint and relevant mods matches the
85// text of the key. This function will consume Shift and Caps Lock when matching
86pub fn matchText(self: Key, cp: u21, mods: Modifiers) bool {
87 // return early if we have no text
88 if (self.text == null) return false;
89
90 var self_mods = self.mods;
91 var arg_mods = mods;
92 var code = cp;
93 // if the passed codepoint is upper, we consume all shift and caps mods for
94 // checking
95 if (ziglyph.isUpper(cp)) {
96 // consume mods
97 self_mods.shift = false;
98 self_mods.caps_lock = false;
99 arg_mods.shift = false;
100 arg_mods.caps_lock = false;
101 } else if (mods.shift or mods.caps_lock) {
102 // uppercase the cp and consume all mods
103 code = ziglyph.toUpper(cp);
104 self_mods.shift = false;
105 self_mods.caps_lock = false;
106 arg_mods.shift = false;
107 arg_mods.caps_lock = false;
108 }
109
110 var buf: [4]u8 = undefined;
111 const n = std.unicode.utf8Encode(cp, buf[0..]) catch return false;
112 return std.mem.eql(u8, self.text.?, buf[0..n]) and std.meta.eql(self_mods, arg_mods);
113}
114
115// The key must exactly match the codepoint and modifiers
116pub fn matchExact(self: Key, cp: u21, mods: Modifiers) bool {
117 return self.codepoint == cp and std.meta.eql(self.mods, mods);
118}
119
120// a few special keys that we encode as their actual ascii value
121pub const enter: u21 = 0x0D;
122pub const tab: u21 = 0x09;
123pub const escape: u21 = 0x1B;
124pub const space: u21 = 0x20;
125pub const backspace: u21 = 0x7F;
126
127// multicodepoint is a key which generated text but cannot be expressed as a
128// single codepoint. The value is the maximum unicode codepoint + 1
129pub const multicodepoint: u21 = 1_114_112 + 1;
130
131// kitty encodes these keys directly in the private use area. We reuse those
132// mappings
133pub const insert: u21 = 57348;
134pub const delete: u21 = 57349;
135pub const left: u21 = 57350;
136pub const right: u21 = 57351;
137pub const up: u21 = 57352;
138pub const down: u21 = 57353;
139pub const page_up: u21 = 57354;
140pub const page_down: u21 = 57355;
141pub const home: u21 = 57356;
142pub const end: u21 = 57357;
143pub const caps_lock: u21 = 57358;
144pub const scroll_lock: u21 = 57359;
145pub const num_lock: u21 = 57360;
146pub const print_screen: u21 = 57361;
147pub const pause: u21 = 57362;
148pub const menu: u21 = 57363;
149pub const f1: u21 = 57364;
150pub const f2: u21 = 57365;
151pub const f3: u21 = 57366;
152pub const f4: u21 = 57367;
153pub const f5: u21 = 57368;
154pub const f6: u21 = 57369;
155pub const f7: u21 = 57370;
156pub const f8: u21 = 57371;
157pub const f9: u21 = 57372;
158pub const f10: u21 = 57373;
159pub const f11: u21 = 57374;
160pub const f12: u21 = 57375;
161pub const f13: u21 = 57376;
162pub const f14: u21 = 57377;
163pub const f15: u21 = 57378;
164pub const @"f16": u21 = 57379;
165pub const f17: u21 = 57380;
166pub const f18: u21 = 57381;
167pub const f19: u21 = 57382;
168pub const f20: u21 = 57383;
169pub const f21: u21 = 57384;
170pub const f22: u21 = 57385;
171pub const f23: u21 = 57386;
172pub const f24: u21 = 57387;
173pub const f25: u21 = 57388;
174pub const f26: u21 = 57389;
175pub const f27: u21 = 57390;
176pub const f28: u21 = 57391;
177pub const f29: u21 = 57392;
178pub const f30: u21 = 57393;
179pub const f31: u21 = 57394;
180pub const @"f32": u21 = 57395;
181pub const f33: u21 = 57396;
182pub const f34: u21 = 57397;
183pub const f35: u21 = 57398;
184pub const kp_0: u21 = 57399;
185pub const kp_1: u21 = 57400;
186pub const kp_2: u21 = 57401;
187pub const kp_3: u21 = 57402;
188pub const kp_4: u21 = 57403;
189pub const kp_5: u21 = 57404;
190pub const kp_6: u21 = 57405;
191pub const kp_7: u21 = 57406;
192pub const kp_8: u21 = 57407;
193pub const kp_9: u21 = 57408;
194pub const kp_decimal: u21 = 57409;
195pub const kp_divide: u21 = 57410;
196pub const kp_multiply: u21 = 57411;
197pub const kp_subtract: u21 = 57412;
198pub const kp_add: u21 = 57413;
199pub const kp_enter: u21 = 57414;
200pub const kp_equal: u21 = 57415;
201pub const kp_separator: u21 = 57416;
202pub const kp_left: u21 = 57417;
203pub const kp_right: u21 = 57418;
204pub const kp_up: u21 = 57419;
205pub const kp_down: u21 = 57420;
206pub const kp_page_up: u21 = 57421;
207pub const kp_page_down: u21 = 57422;
208pub const kp_home: u21 = 57423;
209pub const kp_end: u21 = 57424;
210pub const kp_insert: u21 = 57425;
211pub const kp_delete: u21 = 57426;
212pub const kp_begin: u21 = 57427;
213pub const media_play: u21 = 57428;
214pub const media_pause: u21 = 57429;
215pub const media_play_pause: u21 = 57430;
216pub const media_reverse: u21 = 57431;
217pub const media_stop: u21 = 57432;
218pub const media_fast_forward: u21 = 57433;
219pub const media_rewind: u21 = 57434;
220pub const media_track_next: u21 = 57435;
221pub const media_track_previous: u21 = 57436;
222pub const media_record: u21 = 57437;
223pub const lower_volume: u21 = 57438;
224pub const raise_volume: u21 = 57439;
225pub const mute_volume: u21 = 57440;
226pub const left_shift: u21 = 57441;
227pub const left_control: u21 = 57442;
228pub const left_alt: u21 = 57443;
229pub const left_super: u21 = 57444;
230pub const left_hyper: u21 = 57445;
231pub const left_meta: u21 = 57446;
232pub const right_shift: u21 = 57447;
233pub const right_control: u21 = 57448;
234pub const right_alt: u21 = 57449;
235pub const right_super: u21 = 57450;
236pub const right_hyper: u21 = 57451;
237pub const right_meta: u21 = 57452;
238pub const iso_level_3_shift: u21 = 57453;
239pub const iso_level_5_shift: u21 = 57454;
240
241test "matches 'a'" {
242 const key: Key = .{
243 .codepoint = 'a',
244 };
245 try testing.expect(key.matches('a', .{}));
246}
247
248test "matches 'shift+a'" {
249 const key: Key = .{
250 .codepoint = 'a',
251 .mods = .{ .shift = true },
252 .text = "A",
253 };
254 try testing.expect(key.matches('a', .{ .shift = true }));
255 try testing.expect(key.matches('A', .{}));
256 try testing.expect(!key.matches('A', .{ .ctrl = true }));
257}
258
259test "matches 'shift+tab'" {
260 const key: Key = .{
261 .codepoint = Key.tab,
262 .mods = .{ .shift = true },
263 };
264 try testing.expect(key.matches(Key.tab, .{ .shift = true }));
265 try testing.expect(!key.matches(Key.tab, .{}));
266}
267
268test "matches 'shift+;'" {
269 const key: Key = .{
270 .codepoint = ';',
271 .shifted_codepoint = ':',
272 .mods = .{ .shift = true },
273 .text = ":",
274 };
275 try testing.expect(key.matches(';', .{ .shift = true }));
276 try testing.expect(key.matches(':', .{}));
277
278 const colon: Key = .{
279 .codepoint = ':',
280 .mods = .{},
281 };
282 try testing.expect(colon.matches(':', .{}));
283}