//! Input value types for the TUI. //! //! This is the *rich* key model. The P1 decoder (`tui_input.zig`) only //! populates a subset of it (printable chars, enter, backspace, arrows, //! home/end, escape, Ctrl+C, Ctrl+D), but the model is intentionally complete //! so later phases (full Kitty disambiguation, key-release events, super/hyper //! modifiers) can populate the rest without changing the type. Components //! built on this model will keep compiling. const std = @import("std"); /// A logical key, independent of the raw bytes that produced it. /// /// `char` carries a Unicode codepoint (the printable case). All other /// variants are non-printable named keys. The decoder resolves the *display /// text* of a printable key separately into `Key.text` (so e.g. a pasted or /// composed grapheme can be carried verbatim), while `char` holds the single /// decoded codepoint. pub const KeyCode = union(enum) { /// A printable character, as a Unicode codepoint. char: u21, enter, escape, tab, backspace, delete, up, down, left, right, home, end, page_up, page_down, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, }; /// Modifier flags. Packed so it round-trips cheaply and compares by value. /// /// `super` is Cmd/Win, `hyper` is the (rare) Hyper modifier; both are only /// expressible under protocols like Kitty's, so the P1 decoder leaves them /// false. They exist so the model can represent them later. pub const Mods = packed struct { ctrl: bool = false, alt: bool = false, shift: bool = false, super: bool = false, hyper: bool = false, pub const none: Mods = .{}; pub fn eql(a: Mods, b: Mods) bool { return @as(u5, @bitCast(a)) == @as(u5, @bitCast(b)); } pub fn any(self: Mods) bool { return @as(u5, @bitCast(self)) != 0; } }; /// Press / repeat / release. Terminals that don't report key-release (most, /// without the Kitty protocol) only ever produce `.press`. A component opts /// into receiving `.release` via `Component.wantsKeyRelease`. pub const KeyEvent = enum { press, repeat, release, }; /// A fully decoded key. /// /// `text` is the resolved text to insert for a printable key (UTF-8 encoding /// of `code.char`, or pasted text surfaced as a literal run). It is null for /// non-printable keys. `text`, when non-null, is borrowed from the input /// buffer the decoder was handed; callers must copy it if they need it to /// outlive that buffer. pub const Key = struct { code: KeyCode, mods: Mods = .{}, event: KeyEvent = .press, text: ?[]const u8 = null, /// Convenience: is this a plain (unmodified) press of `code`? pub fn isPlain(self: Key, code: KeyCode) bool { return self.event == .press and !self.mods.any() and std.meta.eql(self.code, code); } /// Convenience: Ctrl+ press, e.g. `isCtrl('c')`. `letter` is /// matched case-insensitively against the printable codepoint. pub fn isCtrl(self: Key, letter: u8) bool { if (self.event == .release) return false; if (!self.mods.ctrl) return false; return switch (self.code) { .char => |cp| cp == std.ascii.toLower(@intCast(letter & 0x7f)) or cp == std.ascii.toUpper(@intCast(letter & 0x7f)), else => false, }; } }; test "Mods eql / any" { try std.testing.expect(Mods.none.eql(.{})); try std.testing.expect(!Mods.none.any()); const c: Mods = .{ .ctrl = true }; try std.testing.expect(c.any()); try std.testing.expect(!c.eql(.{ .shift = true })); } test "Key.isPlain / isCtrl" { const a: Key = .{ .code = .{ .char = 'a' } }; try std.testing.expect(a.isPlain(.{ .char = 'a' })); try std.testing.expect(!a.isPlain(.enter)); const ctrl_c: Key = .{ .code = .{ .char = 'c' }, .mods = .{ .ctrl = true } }; try std.testing.expect(ctrl_c.isCtrl('c')); try std.testing.expect(ctrl_c.isCtrl('C')); try std.testing.expect(!ctrl_c.isCtrl('d')); }