summaryrefslogtreecommitdiff
path: root/src/tui_key.zig
blob: e5056cdb5554b6909ad27c37102e2d9ae45d0cbf (plain)
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'));
}