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