diff options
| author | t <t@tjp.lol> | 2026-06-08 10:41:30 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-08 12:25:55 -0600 |
| commit | e5ed00c52bb10aec811734c5568c881c41e58474 (patch) | |
| tree | f51342f759efac79ec175dcc3ede7ebebc0bc470 /src/tui_key.zig | |
| parent | a5881243f9bb4642f90fa94179f7a5bff23e4657 (diff) | |
Replace print CLIRenderer with differential TUI engine
Implement TUI Phase 1: a raw-mode terminal, differential render engine,
pinned input box + footer, and component model wired into the libpanto
event stream. Adds foundation modules (theme, key, component, input,
terminal), the render engine, components, and the app loop; removes the
old print CLIRenderer and driveTurn REPL from main.zig.
Also removes scratch status reports (tui-p1/, progress.md) for the now
completed work.
Diffstat (limited to 'src/tui_key.zig')
| -rw-r--r-- | src/tui_key.zig | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/src/tui_key.zig b/src/tui_key.zig new file mode 100644 index 0000000..e5056cd --- /dev/null +++ b/src/tui_key.zig @@ -0,0 +1,129 @@ +//! 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+<letter> 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')); +} |
