139 lines
5.1 KiB
Zig
139 lines
5.1 KiB
Zig
//! 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);
|
|
}
|