a modern tui library written in zig
at v0.1.0 9.1 kB view raw
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}