diff --git a/examples/continuous.zig b/examples/continuous.zig index 93ab2b7..965c194 100644 --- a/examples/continuous.zig +++ b/examples/continuous.zig @@ -87,10 +87,6 @@ const InputField = struct { .key => |key| { if (key.isAscii()) try this.input.append(key.cp); - // TODO support arrow keys for navigation? - // TODO support readline keybindings (i.e. ctrl-k, ctrl-u, ctrl-b, ctrl-f, etc. and the equivalent alt bindings) - // create an own `Element` implementation from this - if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) this.queue.push(.{ .accept = try this.input.toOwnedSlice() }); diff --git a/examples/elements/input.zig b/examples/elements/input.zig index 7239c5a..7a649a3 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -28,16 +28,24 @@ const QuitText = struct { const InputField = struct { /// Offset from the end describing the current position of the cursor. cursor_offset: usize = 0, + /// Configuration for the InputField. + configuration: Configuration, /// 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, + /// Configuration for InputField's. + pub const Configuration = struct { + color: Color = .default, + }; + // 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() { + pub fn init(allocator: std.mem.Allocator, queue: *App.Queue, configuration: Configuration) @This() { return .{ + .configuration = configuration, .input = .init(allocator), .queue = queue, }; @@ -63,7 +71,7 @@ const InputField = struct { .key => |key| { assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len); - // see *form* implementation in `zk`, which might become part of the library + // readline commands 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; } @@ -75,22 +83,6 @@ const InputField = struct { 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(); @@ -115,10 +107,52 @@ const InputField = struct { } } + if (key.eql(.{ .cp = 'b', .mod = .{ .alt = true } }) or key.eql(.{ .cp = zterm.input.Left, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = zterm.input.KpLeft, .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 + this.cursor_offset += 1; + if (this.cursor_offset == this.input.items.len) break; + const next = this.input.items[this.input.items.len - this.cursor_offset - 1]; + if (next != ' ') non_whitespace = true; + } + } + + if (key.eql(.{ .cp = 'f', .mod = .{ .alt = true } }) or key.eql(.{ .cp = zterm.input.Right, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = zterm.input.KpRight, .mod = .{ .ctrl = true } })) { + var non_whitespace = false; + while (this.cursor_offset > 0) { + if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') { + this.cursor_offset += 1; // correct cursor position back again to make sure the cursor is not on the whitespace, but at the end of the jumped word + break; + } + + // see backspace + this.cursor_offset -= 1; + const next = this.input.items[this.input.items.len - this.cursor_offset - 1]; + if (next != ' ') non_whitespace = true; + } + } + // usual input keys if (key.isAscii()) try this.input.insert(this.input.items.len - this.cursor_offset, key.cp); + 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 enter to accept? + // - shift+enter is not recognized by the input reader of `zterm`, so currently it is not possible to add newlines into the text box? if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) { this.queue.push(.{ .accept = try this.input.toOwnedSlice() }); this.cursor_offset = 0; @@ -191,7 +225,9 @@ pub fn main() !void { var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); - var input_field: InputField = .init(allocator, &app.queue); + var input_field: InputField = .init(allocator, &app.queue, .{ + .color = .black, + }); defer input_field.deinit(); var mouse_draw: MouseDraw = .{}; @@ -284,6 +320,8 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); +const Color = zterm.Color; + const App = zterm.App(union(enum) { accept: []u21, });