//! Shared input-buffer state machine for the TUI's modal text //! prompts (symbol input, projections' as-of date input, etc). //! //! Pure free function over `(buf, len_ptr, key)` — no App or //! tab-state coupling. Callers own: //! //! - The byte buffer (typically a fixed-size `[16]u8`). //! - The `len: *usize` cursor into it. //! - Mode/modal cleanup on `cancelled` and `committed` results. //! - Side effects (status messages, downstream dispatch, etc). //! //! The state machine handles only: //! - Esc → reset `len` to 0, return `.cancelled`. //! - Enter → return `.committed` (caller reads `buf[0..len]`). //! - Backspace → decrement `len`. //! - Ctrl+U → reset `len` to 0 (readline-style clear). //! - Printable ASCII → append byte, increment `len` (capped at //! buffer length). const std = @import("std"); const vaxis = @import("vaxis"); /// Outcome of one `handleKey` call. Caller dispatches on the /// variant. pub const Result = enum { /// Esc pressed. `len.*` has been reset to 0. Caller should /// clear its modal flag and any related UI state. cancelled, /// Enter pressed. Caller reads `buf[0..len.*]` to commit, /// then resets `len.*` and clears the modal flag itself. committed, /// Character appended / removed / cleared. Caller should /// just redraw; no further action. edited, /// Key didn't match any input-buffer semantic (e.g. a /// function key). Caller may layer on its own handling. ignored, }; /// Apply a key event to the shared input buffer state machine. /// See module-level docs for the contract. pub fn handleKey(buf: []u8, len: *usize, key: vaxis.Key) Result { if (key.codepoint == vaxis.Key.escape) { len.* = 0; return .cancelled; } if (key.codepoint == vaxis.Key.enter) { return .committed; } if (key.codepoint == vaxis.Key.backspace) { if (len.* > 0) len.* -= 1; return .edited; } // Ctrl+U: clear entire input (readline convention) if (key.matches('u', .{ .ctrl = true })) { len.* = 0; return .edited; } // Accept printable ASCII (letters, digits, common punctuation). if (key.codepoint < std.math.maxInt(u7) and std.ascii.isPrint(@intCast(key.codepoint)) and len.* < buf.len) { buf[len.*] = @intCast(key.codepoint); len.* += 1; return .edited; } return .ignored; } // ── Tests ───────────────────────────────────────────────────── const testing = std.testing; test "handleKey: escape resets len and returns cancelled" { var buf: [16]u8 = undefined; var len: usize = 5; const result = handleKey(&buf, &len, .{ .codepoint = vaxis.Key.escape }); try testing.expectEqual(Result.cancelled, result); try testing.expectEqual(@as(usize, 0), len); } test "handleKey: enter returns committed without touching len" { var buf: [16]u8 = undefined; @memcpy(buf[0..3], "abc"); var len: usize = 3; const result = handleKey(&buf, &len, .{ .codepoint = vaxis.Key.enter }); try testing.expectEqual(Result.committed, result); try testing.expectEqual(@as(usize, 3), len); try testing.expectEqualStrings("abc", buf[0..len]); } test "handleKey: printable ASCII appends and increments len" { var buf: [16]u8 = undefined; var len: usize = 0; const result = handleKey(&buf, &len, .{ .codepoint = 'x' }); try testing.expectEqual(Result.edited, result); try testing.expectEqual(@as(usize, 1), len); try testing.expectEqual(@as(u8, 'x'), buf[0]); } test "handleKey: backspace decrements len" { var buf: [16]u8 = undefined; var len: usize = 3; const result = handleKey(&buf, &len, .{ .codepoint = vaxis.Key.backspace }); try testing.expectEqual(Result.edited, result); try testing.expectEqual(@as(usize, 2), len); } test "handleKey: backspace at len=0 stays at 0" { var buf: [16]u8 = undefined; var len: usize = 0; const result = handleKey(&buf, &len, .{ .codepoint = vaxis.Key.backspace }); try testing.expectEqual(Result.edited, result); try testing.expectEqual(@as(usize, 0), len); } test "handleKey: ctrl+U clears buffer" { var buf: [16]u8 = undefined; var len: usize = 5; const result = handleKey(&buf, &len, .{ .codepoint = 'u', .mods = .{ .ctrl = true } }); try testing.expectEqual(Result.edited, result); try testing.expectEqual(@as(usize, 0), len); } test "handleKey: append capped at buffer length" { var buf: [3]u8 = undefined; var len: usize = 3; const result = handleKey(&buf, &len, .{ .codepoint = 'x' }); // Returns .ignored (or whatever the cap path produces) — len should not advance past buf.len try testing.expectEqual(Result.ignored, result); try testing.expectEqual(@as(usize, 3), len); } test "handleKey: unrecognized key returns ignored" { var buf: [16]u8 = undefined; var len: usize = 0; // F1 is a non-printable, non-special-handled key const result = handleKey(&buf, &len, .{ .codepoint = vaxis.Key.f1 }); try testing.expectEqual(Result.ignored, result); try testing.expectEqual(@as(usize, 0), len); }