diff --git a/examples/continuous.zig b/examples/continuous.zig index bb91d0c..3113d80 100644 --- a/examples/continuous.zig +++ b/examples/continuous.zig @@ -133,7 +133,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/demo.zig b/examples/demo.zig index d15e82f..f904ac6 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/direct.zig b/examples/direct.zig index 6fd4e7c..a33485c 100644 --- a/examples/direct.zig +++ b/examples/direct.zig @@ -77,7 +77,7 @@ pub fn main() !void { const gpa = allocator.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(gpa, .{}, .{}); var renderer = zterm.Renderer.Direct.init(gpa); defer renderer.deinit(); @@ -147,9 +147,10 @@ pub fn main() !void { // pre event handling switch (event) { - // NOTE maybe I want to decouple the `key`s from the user input too? i.e. this only makes sense if the output is not echoed! - // otherwise just use gnu's `readline`? + // NOTE draw the character with the ctrl indication and a newline to make sure the rendering stays consistent + .cancel => try renderer.writeCtrlDWithNewline(), .line => |line| { + defer gpa.free(line); log.debug("{s}", .{line}); }, // NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback diff --git a/examples/elements/alignment.zig b/examples/elements/alignment.zig index a96ed90..2663b68 100644 --- a/examples/elements/alignment.zig +++ b/examples/elements/alignment.zig @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/button.zig b/examples/elements/button.zig index 4214177..975d5b1 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -82,7 +82,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/input.zig b/examples/elements/input.zig index cd2e782..524e5fa 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -65,7 +65,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/progress.zig b/examples/elements/progress.zig index 1023637..840fcb0 100644 --- a/examples/elements/progress.zig +++ b/examples/elements/progress.zig @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/radio-button.zig b/examples/elements/radio-button.zig index abcdf9d..b194cac 100644 --- a/examples/elements/radio-button.zig +++ b/examples/elements/radio-button.zig @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index a02a37b..ce7aa17 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -65,7 +65,7 @@ pub fn main() !void { } const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/selection.zig b/examples/elements/selection.zig index 4d5f611..924d89a 100644 --- a/examples/elements/selection.zig +++ b/examples/elements/selection.zig @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/errors.zig b/examples/errors.zig index 91f1bac..0f97b0e 100644 --- a/examples/errors.zig +++ b/examples/errors.zig @@ -97,7 +97,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/grid.zig b/examples/layouts/grid.zig index 5c73d25..90aced3 100644 --- a/examples/layouts/grid.zig +++ b/examples/layouts/grid.zig @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/horizontal.zig b/examples/layouts/horizontal.zig index 116571e..0ce72df 100644 --- a/examples/layouts/horizontal.zig +++ b/examples/layouts/horizontal.zig @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/mixed.zig b/examples/layouts/mixed.zig index 5ab93e3..1e4eb01 100644 --- a/examples/layouts/mixed.zig +++ b/examples/layouts/mixed.zig @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/vertical.zig b/examples/layouts/vertical.zig index a828256..78012b1 100644 --- a/examples/layouts/vertical.zig +++ b/examples/layouts/vertical.zig @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index d0e06cd..32b9a40 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/styles/text.zig b/examples/styles/text.zig index 58f94ac..d0c41f3 100644 --- a/examples/styles/text.zig +++ b/examples/styles/text.zig @@ -178,7 +178,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}, .{}); + var app: App = .init(allocator, .{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/src/app.zig b/src/app.zig index 4e9dc86..9460957 100644 --- a/src/app.zig +++ b/src/app.zig @@ -24,6 +24,7 @@ pub fn App(comptime M: type, comptime E: type) type { if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`"); if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`."); return struct { + gpa: Allocator, io: std.Io, model: Model, queue: Queue, @@ -71,8 +72,9 @@ pub fn App(comptime M: type, comptime E: type) type { this.postEvent(.resize); } - pub fn init(io: std.Io, model: Model) @This() { + pub fn init(gpa: Allocator, io: std.Io, model: Model) @This() { return .{ + .gpa = gpa, .io = io, .model = model, .queue = .{}, @@ -158,7 +160,7 @@ pub fn App(comptime M: type, comptime E: type) type { // thread to read user inputs var buf: [512]u8 = undefined; // NOTE set the `NONBLOCK` option for the stdin file, such that reading is not blocking! - { + if (this.config.rawMode) { // TODO is there a better way to do this through the `std.Io` interface? var fl_flags = posix.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0) catch |err| switch (err) { error.FileBusy => unreachable, @@ -178,7 +180,11 @@ pub fn App(comptime M: type, comptime E: type) type { else => |e| return e, }; } + var remaining_bytes: usize = 0; + var lines: std.ArrayList(u8) = .empty; + defer lines.deinit(this.gpa); + while (true) { this.quit_event.timedWait(20 * std.time.ns_per_ms) catch { // non-blocking read @@ -192,6 +198,12 @@ pub fn App(comptime M: type, comptime E: type) type { } + remaining_bytes; remaining_bytes = 0; + if (read_bytes == 0) { + // received + this.postEvent(.cancel); + continue; + } + // escape key presses if (buf[0] == 0x1b and read_bytes > 1) { switch (buf[1]) { @@ -448,14 +460,23 @@ pub fn App(comptime M: type, comptime E: type) type { var len = read_bytes; while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1; remaining_bytes = read_bytes - len; + if (this.config.rawMode) { var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 }; while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } }); - continue; + } else try lines.appendSlice(this.gpa, buf[0..len]); + + if (remaining_bytes > 0) { + @memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]); } else { - this.postEvent(.{ .line = buf[0..len] }); - continue; + if (read_bytes != 512 and buf[len - 1] == '\n') this.postEvent(.{ .line = try lines.toOwnedSlice(this.gpa) }); + // NOTE line did not end with `\n` but with EOF, meaning the user canceled + if (read_bytes != 512 and buf[len - 1] != '\n') { + lines.clearRetainingCapacity(); + this.postEvent(.cancel); + } } + continue; // this switch block does not return a `Key` we continue with loop }, }; this.postEvent(.{ .key = key }); @@ -509,6 +530,7 @@ const mem = std.mem; const fmt = std.fmt; const posix = std.posix; const Thread = std.Thread; +const Allocator = mem.Allocator; const assert = std.debug.assert; const event = @import("event.zig"); const input = @import("input.zig"); diff --git a/src/event.zig b/src/event.zig index ed0d1f6..a59cf18 100644 --- a/src/event.zig +++ b/src/event.zig @@ -8,6 +8,11 @@ pub const SystemEvent = union(enum) { init, /// Quit event to signify the end of the event loop (rendering should stop afterwards) quit, + /// Cancel event to signify that the user provided an EOF + /// + /// Usually this event is only triggered by the system in *non raw mode* + /// renderings otherwise the corresponding `.key` event would be fired instead. + cancel, /// Resize event to signify that the application should re-draw to resize /// /// Usually no `Container` nor `Element` should act on that event, as it @@ -23,7 +28,10 @@ pub const SystemEvent = union(enum) { /// associated error message msg: []const u8, }, - /// Input line event received in non *raw mode* (instead of individual `key` events) + /// Input line event received in *non raw mode* (instead of individual `key` events) + /// + /// This event contains the entire line until the ending newline character + /// (which is included in the payload of this event). line: []const u8, /// Input key event received from the user key: Key, diff --git a/src/render.zig b/src/render.zig index 4c3e01e..ed3ad50 100644 --- a/src/render.zig +++ b/src/render.zig @@ -158,6 +158,11 @@ pub const Direct = struct { try terminal.clearScreen(); } + pub fn writeCtrlDWithNewline(this: *@This()) !void { + _ = this; + _ = try terminal.write("^D\n"); + } + /// Render provided cells at size (anchor and dimension) into the *screen*. pub fn render(this: *@This(), comptime Container: type, container: *Container, comptime Model: type, model: *const Model) !void { const size: Point = container.size;