//! Key event primitives. //! //! Port of ultraviolet's `key.go`. Key codes are modeled as a Zig enum with //! the exact integer values Go assigns (based on `KeyExtended = MaxRune + 1`), //! while printable characters are carried as raw runes (`u21`) on the `Key` //! struct. `displayName`/`codeForName` replace Go's `keyTypeString` and //! `stringKeyType` maps; they are spelled as match functions (no string map). const std = @import("std"); const t = std.testing; /// The actual characters received (printable keys only). Borrowed slice. const Key = @This(); text: []const u8 = "", /// Modifier keys. mod: Mod = .{}, /// The key pressed. A special key code or a printable rune. code: u21 = 0, /// Shifted code (Kitty / Windows only). shifted_code: u21 = 0, /// Base PC-101 code (Kitty / Windows only). base_code: u21 = 0, /// Whether the key is repeating (Kitty / Windows only). is_repeat: bool = false, /// Whether the code is one of the named special keys. pub fn isSpecial(self: Key) bool { return keyCodeFromRune(self.code) != null; } /// The `Keystroke()` string representation (modifiers + key name). pub fn keystroke(self: Key, out: []u8) []const u8 { const mods = self.mod; const c = self.code; var n: usize = 0; const mod_prefixes = [_]struct { on: bool, s: []const u8 }{ .{ .on = mods.ctrl and c != Code.KeyLeftCtrl.rune() and c != Code.KeyRightCtrl.rune(), .s = "ctrl+" }, .{ .on = mods.alt and c != Code.KeyLeftAlt.rune() and c != Code.KeyRightAlt.rune(), .s = "alt+" }, .{ .on = mods.shift and c != Code.KeyLeftShift.rune() and c != Code.KeyRightShift.rune(), .s = "shift+" }, .{ .on = mods.meta and c != Code.KeyLeftMeta.rune() and c != Code.KeyRightMeta.rune(), .s = "meta+" }, .{ .on = mods.hyper and c != Code.KeyLeftHyper.rune() and c != Code.KeyRightHyper.rune(), .s = "hyper+" }, .{ .on = mods.super and c != Code.KeyLeftSuper.rune() and c != Code.KeyRightSuper.rune(), .s = "super+" }, }; outer: for (mod_prefixes) |p| { if (p.on) { if (n + p.s.len > out.len) break :outer; @memcpy(out[n .. n + p.s.len], p.s); n += p.s.len; } } var code = c; if (self.base_code != 0) code = self.base_code; if (keyCodeFromRune(code)) |kc| { const dn = displayName(kc); if (n + dn.len <= out.len) { @memcpy(out[n .. n + dn.len], dn); n += dn.len; } } else { switch (code) { Code.KeySpace.rune() => { @memcpy(out[n .. n + 5], "space"); n += 5; }, key_extended => { if (n + self.text.len <= out.len) { @memcpy(out[n .. n + self.text.len], self.text); n += self.text.len; } }, else => { var buf: [4]u8 = undefined; // `code` can be any parsed key code (e.g. kitty `base_code`); // emit U+FFFD for runes utf8 cannot encode, like Go's // `utf8.EncodeRune`. const rune: u21 = if (std.unicode.utf8ValidCodepoint(code)) code else std.unicode.replacement_character; const len = std.unicode.utf8Encode(rune, &buf) catch unreachable; if (n + len <= out.len) { @memcpy(out[n .. n + len], buf[0..len]); n += len; } }, } } return out[0..n]; } /// `String()`: text representation if `Text` is a non-space printable, /// otherwise the keystroke. pub fn string(self: Key, out: []u8) []const u8 { if (self.text.len > 0 and !std.mem.eql(u8, self.text, " ")) return self.text; return self.keystroke(out); } /// `KeyExtended` in Go — `unicode.MaxRune + 1`. A sentinel used to signify a /// key event that carries multiple runes in `Text`. pub const key_extended: u21 = 0x10FFFF + 1; /// Named key codes. Values mirror ultraviolet `key.go` exactly. pub const Code = enum(u21) { KeyUp = 1114113, KeyDown = 1114114, KeyRight = 1114115, KeyLeft = 1114116, KeyBegin = 1114117, KeyFind = 1114118, KeyInsert = 1114119, KeyDelete = 1114120, KeySelect = 1114121, KeyPgUp = 1114122, KeyPgDown = 1114123, KeyHome = 1114124, KeyEnd = 1114125, KeyKpEnter = 1114126, KeyKpEqual = 1114127, KeyKpMultiply = 1114128, KeyKpPlus = 1114129, KeyKpComma = 1114130, KeyKpMinus = 1114131, KeyKpDecimal = 1114132, KeyKpDivide = 1114133, KeyKp0 = 1114134, KeyKp1 = 1114135, KeyKp2 = 1114136, KeyKp3 = 1114137, KeyKp4 = 1114138, KeyKp5 = 1114139, KeyKp6 = 1114140, KeyKp7 = 1114141, KeyKp8 = 1114142, KeyKp9 = 1114143, KeyKpSep = 1114144, KeyKpUp = 1114145, KeyKpDown = 1114146, KeyKpLeft = 1114147, KeyKpRight = 1114148, KeyKpPgUp = 1114149, KeyKpPgDown = 1114150, KeyKpHome = 1114151, KeyKpEnd = 1114152, KeyKpInsert = 1114153, KeyKpDelete = 1114154, KeyKpBegin = 1114155, KeyF1 = 1114156, KeyF2 = 1114157, KeyF3 = 1114158, KeyF4 = 1114159, KeyF5 = 1114160, KeyF6 = 1114161, KeyF7 = 1114162, KeyF8 = 1114163, KeyF9 = 1114164, KeyF10 = 1114165, KeyF11 = 1114166, KeyF12 = 1114167, KeyF13 = 1114168, KeyF14 = 1114169, KeyF15 = 1114170, KeyF16 = 1114171, KeyF17 = 1114172, KeyF18 = 1114173, KeyF19 = 1114174, KeyF20 = 1114175, KeyF21 = 1114176, KeyF22 = 1114177, KeyF23 = 1114178, KeyF24 = 1114179, KeyF25 = 1114180, KeyF26 = 1114181, KeyF27 = 1114182, KeyF28 = 1114183, KeyF29 = 1114184, KeyF30 = 1114185, KeyF31 = 1114186, KeyF32 = 1114187, KeyF33 = 1114188, KeyF34 = 1114189, KeyF35 = 1114190, KeyF36 = 1114191, KeyF37 = 1114192, KeyF38 = 1114193, KeyF39 = 1114194, KeyF40 = 1114195, KeyF41 = 1114196, KeyF42 = 1114197, KeyF43 = 1114198, KeyF44 = 1114199, KeyF45 = 1114200, KeyF46 = 1114201, KeyF47 = 1114202, KeyF48 = 1114203, KeyF49 = 1114204, KeyF50 = 1114205, KeyF51 = 1114206, KeyF52 = 1114207, KeyF53 = 1114208, KeyF54 = 1114209, KeyF55 = 1114210, KeyF56 = 1114211, KeyF57 = 1114212, KeyF58 = 1114213, KeyF59 = 1114214, KeyF60 = 1114215, KeyF61 = 1114216, KeyF62 = 1114217, KeyF63 = 1114218, KeyCapsLock = 1114219, KeyScrollLock = 1114220, KeyNumLock = 1114221, KeyPrintScreen = 1114222, KeyPause = 1114223, KeyMenu = 1114224, KeyMediaPlay = 1114225, KeyMediaPause = 1114226, KeyMediaPlayPause = 1114227, KeyMediaReverse = 1114228, KeyMediaStop = 1114229, KeyMediaFastForward = 1114230, KeyMediaRewind = 1114231, KeyMediaNext = 1114232, KeyMediaPrev = 1114233, KeyMediaRecord = 1114234, KeyLowerVol = 1114235, KeyRaiseVol = 1114236, KeyMute = 1114237, KeyLeftShift = 1114238, KeyLeftAlt = 1114239, KeyLeftCtrl = 1114240, KeyLeftSuper = 1114241, KeyLeftHyper = 1114242, KeyLeftMeta = 1114243, KeyRightShift = 1114244, KeyRightAlt = 1114245, KeyRightCtrl = 1114246, KeyRightSuper = 1114247, KeyRightHyper = 1114248, KeyRightMeta = 1114249, KeyIsoLevel3Shift = 1114250, KeyIsoLevel5Shift = 1114251, // Special names in C0. KeyBackspace = 0x7F, KeyTab = 0x09, KeyEnter = 0x0D, KeyEscape = 0x1B, // Special name in G0. KeySpace = 0x20, /// The integer rune value of this key code. pub fn rune(self: Code) u21 { return @intFromEnum(self); } }; /// Looks up a named key code from a rune value; `null` for printable or /// unknown runes (mirrors not finding a match in Go's tables). pub fn keyCodeFromRune(rune: u21) ?Code { return std.enums.fromInt(Code, @as(u32, @intCast(rune))); } /// Modifier keys. Mirrors `uv.KeyMod` bit flags. pub const Mod = packed struct { shift: bool = false, alt: bool = false, ctrl: bool = false, meta: bool = false, hyper: bool = false, super: bool = false, caps_lock: bool = false, num_lock: bool = false, scroll_lock: bool = false, pub const none: Mod = .{}; /// Returns the numeric bitmask value (matches `int(KeyMod)` in Go). pub fn value(self: Mod) u16 { return @as(u16, @intCast(@as(u9, @bitCast(self)))); } /// Whether `self` contains all modifiers in `mods`. pub fn contains(self: Mod, mods: Mod) bool { return (self.value() & mods.value()) == mods.value(); } pub fn hasAny(self: Mod, mods: Mod) bool { return (self.value() & mods.value()) != 0; } pub fn eql(self: Mod, other: Mod) bool { return self.value() == other.value(); } pub fn addMods(self: Mod, mods: Mod) Mod { return fromMask(self.value() | mods.value()); } pub fn clearMods(self: Mod, mods: Mod) Mod { return fromMask(self.value() & ~mods.value()); } /// Rebuilds a `KeyMod` from a raw u16 bitmask (low 9 bits used). pub fn fromMask(mask: u16) Mod { return @bitCast(@as(u9, @truncate(mask))); } }; /// Named modifier masks (strings like "ctrl", "alt", … used by `MatchString`). const mod_mask_by_name = [_]struct { name: []const u8, mask: Mod }{ .{ .name = "ctrl", .mask = .{ .ctrl = true } }, .{ .name = "alt", .mask = .{ .alt = true } }, .{ .name = "shift", .mask = .{ .shift = true } }, .{ .name = "meta", .mask = .{ .meta = true } }, .{ .name = "hyper", .mask = .{ .hyper = true } }, .{ .name = "super", .mask = .{ .super = true } }, .{ .name = "capslock", .mask = .{ .caps_lock = true } }, .{ .name = "scrolllock", .mask = .{ .scroll_lock = true } }, .{ .name = "numlock", .mask = .{ .num_lock = true } }, }; /// The display name for a named key code (mirrors `keyTypeString`). `null` /// when the code has no dedicated name (printable / unknown). pub fn displayName(code: Code) []const u8 { return switch (code) { .KeyEnter, .KeyKpEnter => "enter", .KeyTab => "tab", .KeyBackspace => "backspace", .KeyEscape => "esc", .KeySpace => "space", .KeyUp => "up", .KeyDown => "down", .KeyLeft => "left", .KeyRight => "right", .KeyBegin => "begin", .KeyFind => "find", .KeyInsert => "insert", .KeyDelete => "delete", .KeySelect => "select", .KeyPgUp => "pgup", .KeyPgDown => "pgdown", .KeyHome => "home", .KeyEnd => "end", .KeyKpEqual => "equal", .KeyKpMultiply => "mul", .KeyKpPlus => "plus", .KeyKpComma => "comma", .KeyKpMinus => "minus", .KeyKpDecimal => "period", .KeyKpDivide => "div", .KeyKp0 => "0", .KeyKp1 => "1", .KeyKp2 => "2", .KeyKp3 => "3", .KeyKp4 => "4", .KeyKp5 => "5", .KeyKp6 => "6", .KeyKp7 => "7", .KeyKp8 => "8", .KeyKp9 => "9", .KeyKpSep => "sep", .KeyKpUp => "up", .KeyKpDown => "down", .KeyKpLeft => "left", .KeyKpRight => "right", .KeyKpPgUp => "pgup", .KeyKpPgDown => "pgdown", .KeyKpHome => "home", .KeyKpEnd => "end", .KeyKpInsert => "insert", .KeyKpDelete => "delete", .KeyKpBegin => "begin", .KeyF1 => "f1", .KeyF2 => "f2", .KeyF3 => "f3", .KeyF4 => "f4", .KeyF5 => "f5", .KeyF6 => "f6", .KeyF7 => "f7", .KeyF8 => "f8", .KeyF9 => "f9", .KeyF10 => "f10", .KeyF11 => "f11", .KeyF12 => "f12", .KeyF13 => "f13", .KeyF14 => "f14", .KeyF15 => "f15", .KeyF16 => "f16", .KeyF17 => "f17", .KeyF18 => "f18", .KeyF19 => "f19", .KeyF20 => "f20", .KeyF21 => "f21", .KeyF22 => "f22", .KeyF23 => "f23", .KeyF24 => "f24", .KeyF25 => "f25", .KeyF26 => "f26", .KeyF27 => "f27", .KeyF28 => "f28", .KeyF29 => "f29", .KeyF30 => "f30", .KeyF31 => "f31", .KeyF32 => "f32", .KeyF33 => "f33", .KeyF34 => "f34", .KeyF35 => "f35", .KeyF36 => "f36", .KeyF37 => "f37", .KeyF38 => "f38", .KeyF39 => "f39", .KeyF40 => "f40", .KeyF41 => "f41", .KeyF42 => "f42", .KeyF43 => "f43", .KeyF44 => "f44", .KeyF45 => "f45", .KeyF46 => "f46", .KeyF47 => "f47", .KeyF48 => "f48", .KeyF49 => "f49", .KeyF50 => "f50", .KeyF51 => "f51", .KeyF52 => "f52", .KeyF53 => "f53", .KeyF54 => "f54", .KeyF55 => "f55", .KeyF56 => "f56", .KeyF57 => "f57", .KeyF58 => "f58", .KeyF59 => "f59", .KeyF60 => "f60", .KeyF61 => "f61", .KeyF62 => "f62", .KeyF63 => "f63", .KeyCapsLock => "capslock", .KeyScrollLock => "scrolllock", .KeyNumLock => "numlock", .KeyPrintScreen => "printscreen", .KeyPause => "pause", .KeyMenu => "menu", .KeyMediaPlay => "mediaplay", .KeyMediaPause => "mediapause", .KeyMediaPlayPause => "mediaplaypause", .KeyMediaReverse => "mediareverse", .KeyMediaStop => "mediastop", .KeyMediaFastForward => "mediafastforward", .KeyMediaRewind => "mediarewind", .KeyMediaNext => "medianext", .KeyMediaPrev => "mediaprev", .KeyMediaRecord => "mediarecord", .KeyLowerVol => "lowervol", .KeyRaiseVol => "raisevol", .KeyMute => "mute", .KeyLeftShift => "leftshift", .KeyLeftAlt => "leftalt", .KeyLeftCtrl => "leftctrl", .KeyLeftSuper => "leftsuper", .KeyLeftHyper => "lefthyper", .KeyLeftMeta => "leftmeta", .KeyRightShift => "rightshift", .KeyRightAlt => "rightalt", .KeyRightCtrl => "rightctrl", .KeyRightSuper => "rightsuper", .KeyRightHyper => "righthyper", .KeyRightMeta => "rightmeta", .KeyIsoLevel3Shift => "isolevel3shift", .KeyIsoLevel5Shift => "isolevel5shift", }; } /// Resolves a key-name string to a `Code` (mirrors `stringKeyType`). /// `null` when the name is unknown. pub fn codeForName(name: []const u8) ?Code { if (std.mem.eql(u8, name, "escape")) return .KeyEscape; if (std.mem.eql(u8, name, "esc")) return .KeyEscape; if (std.mem.eql(u8, name, "enter")) return .KeyEnter; const entries = std.meta.tags(Code); for (entries) |tag| { if (std.mem.eql(u8, displayName(tag), name)) return tag; } return null; } /// Whether `k` (a key event's data) matches one of the given match strings. /// Strings look like "enter", "a", "ctrl+a", "shift+enter", etc. pub fn keyMatch(k: Key, s: []const u8) bool { var mod: Mod = .{}; var code: u21 = 0; var text: []const u8 = ""; var scratch: [4]u8 = undefined; var it = std.mem.splitScalar(u8, s, '+'); while (it.next()) |part| { var found_mod = false; for (mod_mask_by_name) |m| { if (std.mem.eql(u8, part, m.name)) { mod = Mod.addMods(mod, m.mask); found_mod = true; break; } } if (found_mod) continue; if (codeForName(part)) |kc| { code = kc.rune(); } else if ((std.unicode.utf8CountCodepoints(part) catch 0) == 1) { code = std.unicode.utf8Decode(part) catch 0; } else { code = key_extended; text = part; } } // If we have a printable character, compute the display text. const smod = mod.clearMods(.addMods(.{ .shift = true }, .{ .caps_lock = true })); if (smod.value() == 0 and text.len == 0 and isPrint(code)) { const c = if (mod.shift or mod.caps_lock) unicodeToUpper(code) else code; const n = std.unicode.utf8Encode(@intCast(c), &scratch) catch unreachable; text = scratch[0..n]; } return (Mod.eql(k.mod, mod) and k.code == code) or (k.text.len != 0 and std.mem.eql(u8, k.text, text)); } /// Whether `k` matches any of the given match strings. pub fn keyMatchesAny(k: Key, ss: []const []const u8) bool { for (ss) |s| { if (keyMatch(k, s)) return true; } return false; } fn isPrint(c: u21) bool { return switch (c) { ' '...'~', 0xA0...0xD7FF, 0xE000...0xFFFD, 0x10000...0x10FFFF => true, else => false, }; } fn unicodeToUpper(c: u21) u21 { if (c <= 0xFF) return std.ascii.toUpper(@intCast(c)); return c; } test "key code values" { try std.testing.expectEqual(@as(u21, 0x0D), Code.KeyEnter.rune()); try std.testing.expectEqual(@as(u21, 0x1B), Code.KeyEscape.rune()); try std.testing.expectEqual(@as(u21, 0x20), Code.KeySpace.rune()); try std.testing.expectEqual(@as(u21, 1114113), Code.KeyUp.rune()); try std.testing.expectEqual(@as(u21, 1114124), Code.KeyHome.rune()); try std.testing.expectEqual(@as(u21, 1114156), Code.KeyF1.rune()); try std.testing.expectEqual(@as(u21, 1114251), Code.KeyIsoLevel5Shift.rune()); } test "display name roundtrip" { try t.expectEqualStrings("enter", displayName(.KeyEnter)); try t.expectEqualStrings("pgup", displayName(.KeyPgUp)); try t.expectEqual(@as(?Code, .KeyHome), codeForName("home")); try t.expectEqual(@as(?Code, null), codeForName("nope")); } test "fuzz key" { try t.fuzz({}, fuzzKey, .{ .corpus = &.{ "", "a", "enter", "ctrl+a", "shift+enter", "mediafastforward", "ctrl+alt+space", "super+capslock+delete", }, }); } fn fuzzKey(_: void, smith: *t.Smith) anyerror!void { var txt: [16]u8 = undefined; const tlen = smith.slice(&txt); var k: Key = .{ .text = txt[0..tlen], .mod = .{ .shift = smith.value(bool), .alt = smith.value(bool), .ctrl = smith.value(bool), .meta = smith.value(bool), .hyper = smith.value(bool), .super = smith.value(bool), .caps_lock = smith.value(bool), .num_lock = smith.value(bool), .scroll_lock = smith.value(bool), }, .code = smith.value(u21), .shifted_code = smith.value(u21), .base_code = smith.value(u21), .is_repeat = smith.value(bool), }; var scratch: [16]u8 = undefined; _ = k.string(&scratch); var big: [64]u8 = undefined; _ = k.keystroke(&big); var mbuf: [64]u8 = undefined; const mlen = smith.slice(&mbuf); _ = k.keyMatch(mbuf[0..mlen]); _ = Key.codeForName(mbuf[0..mlen]); if (Key.keyCodeFromRune(k.code)) |c| { _ = Key.displayName(c); } // keyMatchesAny walks a list of match strings. var list: [4][]const u8 = undefined; const bn = smith.index(list.len + 1); for (list[0..bn]) |*s| { const rlen = smith.index(@min(mlen, 32) + 1); s.* = mbuf[0..rlen]; } _ = k.keyMatchesAny(list[0..bn]); }