Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630//! 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]);}