summaryrefslogtreecommitdiff
path: root/src/tui_key.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui_key.zig')
-rw-r--r--src/tui_key.zig129
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'));
+}