1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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'));
}
|