a modern tui library written in zig
at main 15 kB view raw
1const std = @import("std"); 2const testing = std.testing; 3 4const Key = @This(); 5 6/// Modifier Keys for a Key Match Event. 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 pub fn eql(self: Modifiers, other: Modifiers) bool { 18 const a: u8 = @bitCast(self); 19 const b: u8 = @bitCast(other); 20 return a == b; 21 } 22}; 23 24/// Flags for the Kitty Protocol. 25pub const KittyFlags = packed struct(u5) { 26 disambiguate: bool = true, 27 report_events: bool = false, 28 report_alternate_keys: bool = true, 29 report_all_as_ctl_seqs: bool = true, 30 report_text: bool = true, 31}; 32 33/// the unicode codepoint of the key event. 34codepoint: u21, 35 36/// the text generated from the key event. The underlying slice has a limited 37/// lifetime. Vaxis maintains an internal ring buffer to temporarily store text. 38/// If the application needs these values longer than the lifetime of the event 39/// it must copy the data. 40text: ?[]const u8 = null, 41 42/// the shifted codepoint of this key event. This will only be present if the 43/// Shift modifier was used to generate the event 44shifted_codepoint: ?u21 = null, 45 46/// the key that would have been pressed on a standard keyboard layout. This is 47/// useful for shortcut matching 48base_layout_codepoint: ?u21 = null, 49 50mods: Modifiers = .{}, 51 52// matches follows a loose matching algorithm for key matches. 53// 1. If the codepoint and modifiers are exact matches, after removing caps_lock 54// and num_lock 55// 2. If the utf8 encoding of the codepoint matches the text, after removing 56// num_lock 57// 3. If there is a shifted codepoint and it matches after removing the shift 58// modifier from self, after removing caps_lock and num_lock 59pub fn matches(self: Key, cp: u21, mods: Modifiers) bool { 60 // rule 1 61 if (self.matchExact(cp, mods)) return true; 62 63 // rule 2 64 if (self.matchText(cp, mods)) return true; 65 66 // rule 3 67 if (self.matchShiftedCodepoint(cp, mods)) return true; 68 69 return false; 70} 71 72/// matches against any of the provided codepoints. 73pub fn matchesAny(self: Key, cps: []const u21, mods: Modifiers) bool { 74 for (cps) |cp| { 75 if (self.matches(cp, mods)) return true; 76 } 77 return false; 78} 79 80/// matches base layout codes, useful for shortcut matching when an alternate key 81/// layout is used 82pub fn matchShortcut(self: Key, cp: u21, mods: Modifiers) bool { 83 if (self.base_layout_codepoint == null) return false; 84 return cp == self.base_layout_codepoint.? and std.meta.eql(self.mods, mods); 85} 86 87/// matches keys that aren't upper case versions when shifted. For example, shift 88/// + semicolon produces a colon. The key can be matched against shift + 89/// semicolon or just colon...or shift + ctrl + ; or just ctrl + : 90pub fn matchShiftedCodepoint(self: Key, cp: u21, mods: Modifiers) bool { 91 if (self.shifted_codepoint == null) return false; 92 if (!self.mods.shift) return false; 93 var self_mods = self.mods; 94 self_mods.shift = false; 95 self_mods.caps_lock = false; 96 self_mods.num_lock = false; 97 var tgt_mods = mods; 98 tgt_mods.caps_lock = false; 99 tgt_mods.num_lock = false; 100 return cp == self.shifted_codepoint.? and std.meta.eql(self_mods, mods); 101} 102 103/// matches when the utf8 encoding of the codepoint and relevant mods matches the 104/// text of the key. This function will consume Shift and Caps Lock when matching 105pub fn matchText(self: Key, cp: u21, mods: Modifiers) bool { 106 // return early if we have no text 107 if (self.text == null) return false; 108 109 var self_mods = self.mods; 110 self_mods.num_lock = false; 111 self_mods.shift = false; 112 self_mods.caps_lock = false; 113 var arg_mods = mods; 114 115 // TODO: Use zg case_data for full unicode support. We'll need to allocate the case data 116 // somewhere 117 const _cp: u21 = if (cp < 128 and (mods.shift or mods.caps_lock)) 118 // Uppercase our codepoint 119 std.ascii.toUpper(@intCast(cp)) 120 else 121 cp; 122 123 arg_mods.num_lock = false; 124 arg_mods.shift = false; 125 arg_mods.caps_lock = false; 126 127 var buf: [4]u8 = undefined; 128 const n = std.unicode.utf8Encode(_cp, &buf) catch return false; 129 return std.mem.eql(u8, self.text.?, buf[0..n]) and std.meta.eql(self_mods, arg_mods); 130} 131 132// The key must exactly match the codepoint and modifiers. caps_lock and 133// num_lock are removed before matching 134pub fn matchExact(self: Key, cp: u21, mods: Modifiers) bool { 135 var self_mods = self.mods; 136 self_mods.caps_lock = false; 137 self_mods.num_lock = false; 138 var tgt_mods = mods; 139 tgt_mods.caps_lock = false; 140 tgt_mods.num_lock = false; 141 return self.codepoint == cp and std.meta.eql(self_mods, tgt_mods); 142} 143 144/// True if the key is a single modifier (ie: left_shift) 145pub fn isModifier(self: Key) bool { 146 return self.codepoint == left_shift or 147 self.codepoint == left_alt or 148 self.codepoint == left_super or 149 self.codepoint == left_hyper or 150 self.codepoint == left_control or 151 self.codepoint == left_meta or 152 self.codepoint == right_shift or 153 self.codepoint == right_alt or 154 self.codepoint == right_super or 155 self.codepoint == right_hyper or 156 self.codepoint == right_control or 157 self.codepoint == right_meta; 158} 159 160// a few special keys that we encode as their actual ascii value 161pub const tab: u21 = 0x09; 162pub const enter: u21 = 0x0D; 163pub const escape: u21 = 0x1B; 164pub const space: u21 = 0x20; 165pub const backspace: u21 = 0x7F; 166 167/// multicodepoint is a key which generated text but cannot be expressed as a 168/// single codepoint. The value is the maximum unicode codepoint + 1 169pub const multicodepoint: u21 = 1_114_112 + 1; 170 171// kitty encodes these keys directly in the private use area. We reuse those 172// mappings 173pub const insert: u21 = 57348; 174pub const delete: u21 = 57349; 175pub const left: u21 = 57350; 176pub const right: u21 = 57351; 177pub const up: u21 = 57352; 178pub const down: u21 = 57353; 179pub const page_up: u21 = 57354; 180pub const page_down: u21 = 57355; 181pub const home: u21 = 57356; 182pub const end: u21 = 57357; 183pub const caps_lock: u21 = 57358; 184pub const scroll_lock: u21 = 57359; 185pub const num_lock: u21 = 57360; 186pub const print_screen: u21 = 57361; 187pub const pause: u21 = 57362; 188pub const menu: u21 = 57363; 189pub const f1: u21 = 57364; 190pub const f2: u21 = 57365; 191pub const f3: u21 = 57366; 192pub const f4: u21 = 57367; 193pub const f5: u21 = 57368; 194pub const f6: u21 = 57369; 195pub const f7: u21 = 57370; 196pub const f8: u21 = 57371; 197pub const f9: u21 = 57372; 198pub const f10: u21 = 57373; 199pub const f11: u21 = 57374; 200pub const f12: u21 = 57375; 201pub const f13: u21 = 57376; 202pub const f14: u21 = 57377; 203pub const f15: u21 = 57378; 204pub const @"f16": u21 = 57379; 205pub const f17: u21 = 57380; 206pub const f18: u21 = 57381; 207pub const f19: u21 = 57382; 208pub const f20: u21 = 57383; 209pub const f21: u21 = 57384; 210pub const f22: u21 = 57385; 211pub const f23: u21 = 57386; 212pub const f24: u21 = 57387; 213pub const f25: u21 = 57388; 214pub const f26: u21 = 57389; 215pub const f27: u21 = 57390; 216pub const f28: u21 = 57391; 217pub const f29: u21 = 57392; 218pub const f30: u21 = 57393; 219pub const f31: u21 = 57394; 220pub const @"f32": u21 = 57395; 221pub const f33: u21 = 57396; 222pub const f34: u21 = 57397; 223pub const f35: u21 = 57398; 224pub const kp_0: u21 = 57399; 225pub const kp_1: u21 = 57400; 226pub const kp_2: u21 = 57401; 227pub const kp_3: u21 = 57402; 228pub const kp_4: u21 = 57403; 229pub const kp_5: u21 = 57404; 230pub const kp_6: u21 = 57405; 231pub const kp_7: u21 = 57406; 232pub const kp_8: u21 = 57407; 233pub const kp_9: u21 = 57408; 234pub const kp_decimal: u21 = 57409; 235pub const kp_divide: u21 = 57410; 236pub const kp_multiply: u21 = 57411; 237pub const kp_subtract: u21 = 57412; 238pub const kp_add: u21 = 57413; 239pub const kp_enter: u21 = 57414; 240pub const kp_equal: u21 = 57415; 241pub const kp_separator: u21 = 57416; 242pub const kp_left: u21 = 57417; 243pub const kp_right: u21 = 57418; 244pub const kp_up: u21 = 57419; 245pub const kp_down: u21 = 57420; 246pub const kp_page_up: u21 = 57421; 247pub const kp_page_down: u21 = 57422; 248pub const kp_home: u21 = 57423; 249pub const kp_end: u21 = 57424; 250pub const kp_insert: u21 = 57425; 251pub const kp_delete: u21 = 57426; 252pub const kp_begin: u21 = 57427; 253pub const media_play: u21 = 57428; 254pub const media_pause: u21 = 57429; 255pub const media_play_pause: u21 = 57430; 256pub const media_reverse: u21 = 57431; 257pub const media_stop: u21 = 57432; 258pub const media_fast_forward: u21 = 57433; 259pub const media_rewind: u21 = 57434; 260pub const media_track_next: u21 = 57435; 261pub const media_track_previous: u21 = 57436; 262pub const media_record: u21 = 57437; 263pub const lower_volume: u21 = 57438; 264pub const raise_volume: u21 = 57439; 265pub const mute_volume: u21 = 57440; 266pub const left_shift: u21 = 57441; 267pub const left_control: u21 = 57442; 268pub const left_alt: u21 = 57443; 269pub const left_super: u21 = 57444; 270pub const left_hyper: u21 = 57445; 271pub const left_meta: u21 = 57446; 272pub const right_shift: u21 = 57447; 273pub const right_control: u21 = 57448; 274pub const right_alt: u21 = 57449; 275pub const right_super: u21 = 57450; 276pub const right_hyper: u21 = 57451; 277pub const right_meta: u21 = 57452; 278pub const iso_level_3_shift: u21 = 57453; 279pub const iso_level_5_shift: u21 = 57454; 280 281pub const name_map = blk: { 282 @setEvalBranchQuota(2000); 283 break :blk std.StaticStringMap(u21).initComptime(.{ 284 // common names 285 .{ "plus", '+' }, 286 .{ "minus", '-' }, 287 .{ "colon", ':' }, 288 .{ "semicolon", ';' }, 289 .{ "comma", ',' }, 290 291 // special keys 292 .{ "tab", tab }, 293 .{ "enter", enter }, 294 .{ "escape", escape }, 295 .{ "space", space }, 296 .{ "backspace", backspace }, 297 .{ "insert", insert }, 298 .{ "delete", delete }, 299 .{ "left", left }, 300 .{ "right", right }, 301 .{ "up", up }, 302 .{ "down", down }, 303 .{ "page_up", page_up }, 304 .{ "page_down", page_down }, 305 .{ "home", home }, 306 .{ "end", end }, 307 .{ "caps_lock", caps_lock }, 308 .{ "scroll_lock", scroll_lock }, 309 .{ "num_lock", num_lock }, 310 .{ "print_screen", print_screen }, 311 .{ "pause", pause }, 312 .{ "menu", menu }, 313 .{ "f1", f1 }, 314 .{ "f2", f2 }, 315 .{ "f3", f3 }, 316 .{ "f4", f4 }, 317 .{ "f5", f5 }, 318 .{ "f6", f6 }, 319 .{ "f7", f7 }, 320 .{ "f8", f8 }, 321 .{ "f9", f9 }, 322 .{ "f10", f10 }, 323 .{ "f11", f11 }, 324 .{ "f12", f12 }, 325 .{ "f13", f13 }, 326 .{ "f14", f14 }, 327 .{ "f15", f15 }, 328 .{ "f16", @"f16" }, 329 .{ "f17", f17 }, 330 .{ "f18", f18 }, 331 .{ "f19", f19 }, 332 .{ "f20", f20 }, 333 .{ "f21", f21 }, 334 .{ "f22", f22 }, 335 .{ "f23", f23 }, 336 .{ "f24", f24 }, 337 .{ "f25", f25 }, 338 .{ "f26", f26 }, 339 .{ "f27", f27 }, 340 .{ "f28", f28 }, 341 .{ "f29", f29 }, 342 .{ "f30", f30 }, 343 .{ "f31", f31 }, 344 .{ "f32", @"f32" }, 345 .{ "f33", f33 }, 346 .{ "f34", f34 }, 347 .{ "f35", f35 }, 348 .{ "kp_0", kp_0 }, 349 .{ "kp_1", kp_1 }, 350 .{ "kp_2", kp_2 }, 351 .{ "kp_3", kp_3 }, 352 .{ "kp_4", kp_4 }, 353 .{ "kp_5", kp_5 }, 354 .{ "kp_6", kp_6 }, 355 .{ "kp_7", kp_7 }, 356 .{ "kp_8", kp_8 }, 357 .{ "kp_9", kp_9 }, 358 .{ "kp_decimal", kp_decimal }, 359 .{ "kp_divide", kp_divide }, 360 .{ "kp_multiply", kp_multiply }, 361 .{ "kp_subtract", kp_subtract }, 362 .{ "kp_add", kp_add }, 363 .{ "kp_enter", kp_enter }, 364 .{ "kp_equal", kp_equal }, 365 .{ "kp_separator", kp_separator }, 366 .{ "kp_left", kp_left }, 367 .{ "kp_right", kp_right }, 368 .{ "kp_up", kp_up }, 369 .{ "kp_down", kp_down }, 370 .{ "kp_page_up", kp_page_up }, 371 .{ "kp_page_down", kp_page_down }, 372 .{ "kp_home", kp_home }, 373 .{ "kp_end", kp_end }, 374 .{ "kp_insert", kp_insert }, 375 .{ "kp_delete", kp_delete }, 376 .{ "kp_begin", kp_begin }, 377 .{ "media_play", media_play }, 378 .{ "media_pause", media_pause }, 379 .{ "media_play_pause", media_play_pause }, 380 .{ "media_reverse", media_reverse }, 381 .{ "media_stop", media_stop }, 382 .{ "media_fast_forward", media_fast_forward }, 383 .{ "media_rewind", media_rewind }, 384 .{ "media_track_next", media_track_next }, 385 .{ "media_track_previous", media_track_previous }, 386 .{ "media_record", media_record }, 387 .{ "lower_volume", lower_volume }, 388 .{ "raise_volume", raise_volume }, 389 .{ "mute_volume", mute_volume }, 390 .{ "left_shift", left_shift }, 391 .{ "left_control", left_control }, 392 .{ "left_alt", left_alt }, 393 .{ "left_super", left_super }, 394 .{ "left_hyper", left_hyper }, 395 .{ "left_meta", left_meta }, 396 .{ "right_shift", right_shift }, 397 .{ "right_control", right_control }, 398 .{ "right_alt", right_alt }, 399 .{ "right_super", right_super }, 400 .{ "right_hyper", right_hyper }, 401 .{ "right_meta", right_meta }, 402 .{ "iso_level_3_shift", iso_level_3_shift }, 403 .{ "iso_level_5_shift", iso_level_5_shift }, 404 }); 405}; 406 407test "matches 'a'" { 408 const key: Key = .{ 409 .codepoint = 'a', 410 .mods = .{ .num_lock = true }, 411 .text = "a", 412 }; 413 try testing.expect(key.matches('a', .{})); 414 try testing.expect(!key.matches('a', .{ .shift = true })); 415} 416 417test "matches 'shift+a'" { 418 const key: Key = .{ 419 .codepoint = 'a', 420 .shifted_codepoint = 'A', 421 .mods = .{ .shift = true }, 422 .text = "A", 423 }; 424 try testing.expect(key.matches('a', .{ .shift = true })); 425 try testing.expect(!key.matches('a', .{})); 426 try testing.expect(key.matches('A', .{})); 427 try testing.expect(!key.matches('A', .{ .ctrl = true })); 428} 429 430test "matches 'shift+tab'" { 431 const key: Key = .{ 432 .codepoint = Key.tab, 433 .mods = .{ .shift = true, .num_lock = true }, 434 }; 435 try testing.expect(key.matches(Key.tab, .{ .shift = true })); 436 try testing.expect(!key.matches(Key.tab, .{})); 437} 438 439test "matches 'shift+;'" { 440 const key: Key = .{ 441 .codepoint = ';', 442 .shifted_codepoint = ':', 443 .mods = .{ .shift = true }, 444 .text = ":", 445 }; 446 try testing.expect(key.matches(';', .{ .shift = true })); 447 try testing.expect(key.matches(':', .{})); 448 449 const colon: Key = .{ 450 .codepoint = ':', 451 .mods = .{}, 452 }; 453 try testing.expect(colon.matches(':', .{})); 454} 455 456test "name_map" { 457 try testing.expectEqual(insert, name_map.get("insert")); 458}