feat(example/input): readline shortcuts; better navigation;
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 58s

This will be the basis for a Textfield `Element` implementation to make
it easier to add user inputs into the container structures.
This commit is contained in:
2025-06-28 13:04:00 +02:00
parent 743cdca174
commit 92ae8c9681

View File

@@ -24,10 +24,18 @@ const QuitText = struct {
}
};
// TODO create an own `Element` implementation from this
const InputField = struct {
/// Offset from the end describing the current position of the cursor.
cursor_offset: usize = 0,
/// Array holding the value of the input.
input: std.ArrayList(u21),
/// Reference to the app's queue to issue the associated event to trigger when completing the input.
queue: *App.Queue,
// TODO make the event to trigger user defined (needs to be `comptime`)
// - can this even be agnostic to `u8` / `u21`?
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
return .{
.input = .init(allocator),
@@ -53,13 +61,68 @@ const InputField = struct {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
if (key.isAscii()) try this.input.append(key.cp);
assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len);
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
// see *form* implementation in `zk`, which might become part of the library
if (key.eql(.{ .cp = zterm.input.Left }) or key.eql(.{ .cp = zterm.input.KpLeft }) or key.eql(.{ .cp = 'b', .mod = .{ .ctrl = true } })) {
if (this.cursor_offset < this.input.items.len) this.cursor_offset += 1;
}
if (key.eql(.{ .cp = zterm.input.Right }) or key.eql(.{ .cp = zterm.input.KpRight }) or key.eql(.{ .cp = 'f', .mod = .{ .ctrl = true } }))
this.cursor_offset -|= 1;
if (key.eql(.{ .cp = 'e', .mod = .{ .ctrl = true } })) this.cursor_offset = 0;
if (key.eql(.{ .cp = 'a', .mod = .{ .ctrl = true } })) this.cursor_offset = this.input.items.len;
if (key.eql(.{ .cp = zterm.input.Backspace })) {
if (this.cursor_offset < this.input.items.len) {
_ = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1);
}
}
if (key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete })) {
if (this.cursor_offset > 0) {
_ = this.input.orderedRemove(this.input.items.len - this.cursor_offset);
this.cursor_offset -= 1;
}
}
// TODO support readline keybindings (i.e. alt bindings alt-b, alt-f
// TODO maybe even ctrl-left, ctrl-right?)
// readline commands
if (key.eql(.{ .cp = 'k', .mod = .{ .ctrl = true } })) {
while (this.cursor_offset > 0) {
_ = this.input.pop();
this.cursor_offset -= 1;
}
}
if (key.eql(.{ .cp = 'u', .mod = .{ .ctrl = true } })) {
const len = this.input.items.len - this.cursor_offset;
for (0..len) |_| _ = this.input.orderedRemove(0);
this.cursor_offset = this.input.items.len;
}
if (key.eql(.{ .cp = 'w', .mod = .{ .ctrl = true } })) {
var non_whitespace = false;
while (this.cursor_offset < this.input.items.len) {
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') break;
// see backspace
const removed = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1);
if (removed != ' ') non_whitespace = true;
}
}
// usual input keys
if (key.isAscii()) try this.input.insert(this.input.items.len - this.cursor_offset, key.cp);
// TODO enter to accept?
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) {
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
_ = this.input.pop();
this.cursor_offset = 0;
}
},
else => {},
}
@@ -69,22 +132,17 @@ const InputField = struct {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.input.items.len == 0) return;
const row = 1;
const col = 1;
const anchor = (row * size.x) + col;
// TODO add configuration for coloring the text!
for (this.input.items, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .black;
cells[anchor + idx].style.cursor = false;
cells[anchor + idx].cp = cp;
cells[idx].style.fg = .black;
cells[idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true;
if (idx == cells.len - 1) break;
}
// TODO show ellipse `..` (maybe with configuration where - start, middle, end)
// show cursor after text (if there is still space available)
if (this.input.items.len < cells.len) cells[this.input.items.len - this.cursor_offset].style.cursor = true;
}
};