diff --git a/build.zig b/build.zig index 4c84e38..c1573d7 100644 --- a/build.zig +++ b/build.zig @@ -24,6 +24,8 @@ pub fn build(b: *std.Build) void { palette, // error handling errors, + // non alternate screen applications + direct, }; const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all; @@ -73,6 +75,8 @@ pub fn build(b: *std.Build) void { .palette => "examples/styles/palette.zig", // error handling .errors => "examples/errors.zig", + // non-alternate screen + .direct => "examples/direct.zig", .all => unreachable, // should never happen }), .target = target, diff --git a/examples/continuous.zig b/examples/continuous.zig index 7f1177f..bb91d0c 100644 --- a/examples/continuous.zig +++ b/examples/continuous.zig @@ -165,7 +165,7 @@ pub fn main() !void { }, spinner.element()); try container.append(nested_container); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); var framerate: u64 = 60; diff --git a/examples/demo.zig b/examples/demo.zig index d8dab81..d15e82f 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -133,7 +133,7 @@ pub fn main() !void { }, .{})); defer container.deinit(); // also de-initializes the children - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop @@ -149,7 +149,7 @@ pub fn main() !void { if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) { try app.interrupt(); renderer.size = .{}; // reset size, such that next resize will cause a full re-draw! - defer app.start() catch @panic("could not start app event loop"); + defer app.start(.full) catch @panic("could not start app event loop"); var child = std.process.Child.init(&.{"vim"}, allocator); _ = child.spawnAndWait() catch |err| app.postEvent(.{ .err = .{ diff --git a/examples/direct.zig b/examples/direct.zig new file mode 100644 index 0000000..6fd4e7c --- /dev/null +++ b/examples/direct.zig @@ -0,0 +1,191 @@ +const QuitText = struct { + const text = "Press ctrl+c to quit."; + + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + .minSize = minSize, + .content = content, + }, + }; + } + + fn minSize(_: *anyopaque, _: *const App.Model, size: zterm.Point) zterm.Point { + return .{ .x = size.x, .y = 10 }; // this includes the border + } + + pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { + _ = ctx; + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + + const y = 0; + const x = 2; + const anchor = (y * size.x) + x; + + for (text, 0..) |cp, idx| { + cells[anchor + idx].style.fg = .white; + cells[anchor + idx].style.emphasis = &.{ .bold, .underline }; + cells[anchor + idx].cp = cp; + + // NOTE do not write over the contents of this `Container`'s `Size` + if (anchor + idx == cells.len - 1) break; + } + } +}; + +const Prompt = struct { + len: u16 = 3, + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + // .minSize = minSize, + .content = content, + }, + }; + } + + // NOTE size hint is not required as the `.size = .{ .dim = .{..} }` property is set accordingly which denotes the minimal size + // fn minSize(ctx: *anyopaque, _: *const App.Model, _: zterm.Point) zterm.Point { + // const this: *@This() = @ptrCast(@alignCast(ctx)); + // return .{ .x = this.len, .y = 1 }; + // } + + pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + assert(cells.len > 2); // expect at least two cells + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + + for (0..this.len) |idx| { + cells[idx].style.bg = .blue; + cells[idx].style.fg = .black; + cells[idx].style.emphasis = &.{.bold}; + } + cells[1].cp = '>'; + // leave one clear whitespace after the prompt + cells[this.len].style.bg = .default; + cells[this.len].style.cursor = true; // marks the actual end of the rendering! + } +}; + +pub fn main() !void { + errdefer |err| log.err("Application Error: {any}", .{err}); + + var allocator: std.heap.DebugAllocator(.{}) = .init; + defer if (allocator.deinit() == .leak) log.err("memory leak", .{}); + + const gpa = allocator.allocator(); + + var app: App = .init(.{}, .{}); + var renderer = zterm.Renderer.Direct.init(gpa); + defer renderer.deinit(); + + var container: App.Container = try .init(gpa, .{ + .layout = .{ + .direction = .vertical, + .gap = 1, // show empty line between elements to allow navigation through paragraph jumping + }, + .size = .{ + .grow = .horizontal_only, + }, + }, .{}); + defer container.deinit(); + + var quit_text: QuitText = .{}; + var intermediate: App.Container = try .init(gpa, .{ + .border = .{ + .sides = .{ .left = true }, + .color = .grey, + }, + .layout = .{ + .direction = .horizontal, + .padding = .{ .left = 1, .top = 1 }, + }, + }, quit_text.element()); + try intermediate.append(try .init(gpa, .{ + .rectangle = .{ .fill = .blue }, + }, .{})); + + try intermediate.append(try .init(gpa, .{ + .rectangle = .{ .fill = .green }, + }, .{})); + + var padding_container: App.Container = try .init(gpa, .{ + .layout = .{ + .padding = .horizontal(1), + }, + }, .{}); + try padding_container.append(intermediate); + try container.append(padding_container); + + var prompt: Prompt = .{}; + try container.append(try .init(gpa, .{ + .rectangle = .{ .fill = .grey }, + .size = .{ + .dim = .{ .y = 1 }, + }, + }, prompt.element())); + + try app.start(.direct); // needs to become configurable, as what should be enabled / disabled (i.e. show cursor, hide cursor, use alternate screen, etc.) + defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); + + // event loop + event: while (true) { + // batch events since last iteration + const len = blk: { + app.queue.poll(); + app.queue.lock(); + defer app.queue.unlock(); + break :blk app.queue.len(); + }; + + // handle events + for (0..len) |_| { + const event = app.queue.pop(); + log.debug("received event: {s}", .{@tagName(event)}); + + // 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`? + .line => |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 + .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), + else => {}, + } + + // NOTE returned errors should be propagated back to the application + container.handle(&app.model, event) catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Container Event handling failed", + }, + }); + + // post event handling + switch (event) { + .quit => break :event, + else => {}, + } + } + // if there are more events to process continue handling them otherwise I can render the next frame + if (app.queue.len() > 0) continue :event; + + container.resize(&app.model, try renderer.resize()); + container.reposition(&app.model, .{}); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); + try renderer.flush(); + } +} + +pub const panic = App.panic_handler; +const log = std.log.scoped(.default); + +const std = @import("std"); +const assert = std.debug.assert; +const zterm = @import("zterm"); +const input = zterm.input; +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/elements/alignment.zig b/examples/elements/alignment.zig index 3a35bcd..a96ed90 100644 --- a/examples/elements/alignment.zig +++ b/examples/elements/alignment.zig @@ -55,7 +55,7 @@ pub fn main() !void { var alignment: App.Alignment = .init(quit_container, .center); try container.append(try .init(allocator, .{}, alignment.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/elements/button.zig b/examples/elements/button.zig index e150ae8..4214177 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -102,7 +102,7 @@ pub fn main() !void { try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .lightgrey } }, element)); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/elements/input.zig b/examples/elements/input.zig index bf04e3d..cd2e782 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -112,7 +112,7 @@ pub fn main() !void { }, second_mouse_draw.element())); try container.append(nested_container); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/elements/progress.zig b/examples/elements/progress.zig index dd899f4..1023637 100644 --- a/examples/elements/progress.zig +++ b/examples/elements/progress.zig @@ -85,7 +85,7 @@ pub fn main() !void { }); try container.append(try App.Container.init(allocator, .{}, progress.element())); } - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); var framerate: u64 = 60; diff --git a/examples/elements/radio-button.zig b/examples/elements/radio-button.zig index 15b882d..abcdf9d 100644 --- a/examples/elements/radio-button.zig +++ b/examples/elements/radio-button.zig @@ -51,7 +51,7 @@ pub fn main() !void { try container.append(try .init(allocator, .{}, radiobutton.element())); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index ebb6305..a02a37b 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -151,7 +151,7 @@ pub fn main() !void { var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true)); try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/elements/selection.zig b/examples/elements/selection.zig index c19a910..4d5f611 100644 --- a/examples/elements/selection.zig +++ b/examples/elements/selection.zig @@ -52,7 +52,7 @@ pub fn main() !void { defer container.deinit(); try container.append(try .init(allocator, .{}, selection.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/errors.zig b/examples/errors.zig index 3a7bffb..91f1bac 100644 --- a/examples/errors.zig +++ b/examples/errors.zig @@ -116,7 +116,7 @@ pub fn main() !void { try container.append(try App.Container.init(allocator, .{}, info_text.element())); try container.append(try App.Container.init(allocator, .{}, error_notification.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); while (true) { diff --git a/examples/layouts/grid.zig b/examples/layouts/grid.zig index f6f3f08..5c73d25 100644 --- a/examples/layouts/grid.zig +++ b/examples/layouts/grid.zig @@ -69,7 +69,7 @@ pub fn main() !void { } defer container.deinit(); // also de-initializes the children - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/layouts/horizontal.zig b/examples/layouts/horizontal.zig index 06c6154..116571e 100644 --- a/examples/layouts/horizontal.zig +++ b/examples/layouts/horizontal.zig @@ -61,7 +61,7 @@ pub fn main() !void { }, .{})); defer container.deinit(); // also de-initializes the children - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/layouts/mixed.zig b/examples/layouts/mixed.zig index c5bb09e..5ab93e3 100644 --- a/examples/layouts/mixed.zig +++ b/examples/layouts/mixed.zig @@ -77,7 +77,7 @@ pub fn main() !void { } defer container.deinit(); // also de-initializes the children - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/layouts/vertical.zig b/examples/layouts/vertical.zig index fc5c6b3..a828256 100644 --- a/examples/layouts/vertical.zig +++ b/examples/layouts/vertical.zig @@ -60,7 +60,7 @@ pub fn main() !void { }, .{})); defer container.deinit(); // also de-initializes the children - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index 63ceb6c..d0e06cd 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -58,7 +58,7 @@ pub fn main() !void { var scrollable: App.Scrollable = .init(box, .disabled); try container.append(try App.Container.init(allocator, .{}, scrollable.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); while (true) { diff --git a/examples/styles/text.zig b/examples/styles/text.zig index f0f01b8..58f94ac 100644 --- a/examples/styles/text.zig +++ b/examples/styles/text.zig @@ -209,7 +209,7 @@ pub fn main() !void { }, text_styles.element()), .enabled(.white, true)); try container.append(try App.Container.init(allocator, .{}, scrollable.element())); - try app.start(); + try app.start(.full); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop diff --git a/src/app.zig b/src/app.zig index 92e9eae..9acc49e 100644 --- a/src/app.zig +++ b/src/app.zig @@ -17,7 +17,7 @@ /// ); /// // later on create an `App` instance and start the event loop /// var app: App = .init(io, .{}); // provide instance of the `std.Io` and `App` model that shall be used -/// try app.start(); +/// try app.start(.full); /// defer app.stop() catch unreachable; // does not clean-up the resources used in the model /// ``` pub fn App(comptime M: type, comptime E: type) type { @@ -30,7 +30,29 @@ pub fn App(comptime M: type, comptime E: type) type { thread: ?Thread = null, quit_event: Thread.ResetEvent, termios: ?posix.termios = null, - winch_registered: bool = false, + handler_registered: bool = false, + config: TerminalConfiguration, + + pub const TerminalConfiguration = struct { + altScreen: bool, + saveScreen: bool, + rawMode: bool, + hideCursor: bool, + + pub const full: @This() = .{ + .altScreen = true, + .saveScreen = true, + .rawMode = true, + .hideCursor = true, + }; + + pub const direct: @This() = .{ + .altScreen = false, + .saveScreen = false, + .rawMode = false, + .hideCursor = false, + }; + }; // global variable for the registered handler for WINCH var handler_ctx: *anyopaque = undefined; @@ -41,6 +63,13 @@ pub fn App(comptime M: type, comptime E: type) type { // -> the signal might not work correctly when hosting the application over ssh! this.postEvent(.resize); } + /// registered CONT handler to force a complete redraw + fn handleCont(_: i32) callconv(.c) void { + const this: *@This() = @ptrCast(@alignCast(handler_ctx)); + // NOTE this does not have to be done if in-band resize events are supported + // -> the signal might not work correctly when hosting the application over ssh! + this.postEvent(.resize); + } pub fn init(io: std.Io, model: Model) @This() { return .{ @@ -48,44 +77,50 @@ pub fn App(comptime M: type, comptime E: type) type { .model = model, .queue = .{}, .quit_event = .{}, + .config = undefined, }; } - pub fn start(this: *@This()) !void { + pub fn start(this: *@This(), config: TerminalConfiguration) !void { + this.config = config; if (this.thread) |_| return; // post init event (as the very first element to be in the queue - event loop) this.postEvent(.init); - if (!this.winch_registered) { + if (!this.handler_registered) { handler_ctx = this; - var act = posix.Sigaction{ + posix.sigaction(posix.SIG.WINCH, &.{ .handler = .{ .handler = handleWinch }, .mask = posix.sigemptyset(), .flags = 0, - }; - posix.sigaction(posix.SIG.WINCH, &act, null); - this.winch_registered = true; + }, null); + posix.sigaction(posix.SIG.CONT, &.{ + .handler = .{ .handler = handleCont }, + .mask = posix.sigemptyset(), + .flags = 0, + }, null); + this.handler_registered = true; } this.quit_event.reset(); this.thread = try Thread.spawn(.{}, @This().run, .{this}); var termios: posix.termios = undefined; - try terminal.enableRawMode(&termios); + if (this.config.rawMode) try terminal.enableRawMode(&termios); if (this.termios) |_| {} else this.termios = termios; - try terminal.enterAltScreen(); - try terminal.saveScreen(); - try terminal.hideCursor(); - try terminal.enableMouseSupport(); + if (this.config.altScreen) try terminal.enterAltScreen(); + if (this.config.saveScreen) try terminal.saveScreen(); + if (this.config.hideCursor) try terminal.hideCursor(); + if (this.config.altScreen and this.config.rawMode) try terminal.enableMouseSupport(); } pub fn interrupt(this: *@This()) !void { this.quit_event.set(); - try terminal.disableMouseSupport(); - try terminal.restoreScreen(); - try terminal.exitAltScreen(); + if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport(); + if (this.config.saveScreen) try terminal.restoreScreen(); + if (this.config.altScreen) try terminal.exitAltScreen(); if (this.thread) |*thread| { thread.join(); this.thread = null; @@ -94,13 +129,10 @@ pub fn App(comptime M: type, comptime E: type) type { pub fn stop(this: *@This()) !void { try this.interrupt(); + if (this.config.hideCursor) try terminal.showCursor(); + if (this.config.saveScreen) try terminal.resetCursor(); if (this.termios) |termios| { - try terminal.disableMouseSupport(); - try terminal.showCursor(); - try terminal.resetCursor(); - try terminal.restoreScreen(); - try terminal.disableRawMode(&termios); - try terminal.exitAltScreen(); + if (this.config.rawMode) try terminal.disableRawMode(&termios); this.termios = null; } } @@ -416,9 +448,14 @@ 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; - var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 }; - while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } }); - continue; + 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 { + this.postEvent(.{ .line = buf[0..len] }); + continue; + } }, }; this.postEvent(.{ .key = key }); diff --git a/src/container.zig b/src/container.zig index 1a0b069..5afd7d1 100644 --- a/src/container.zig +++ b/src/container.zig @@ -764,23 +764,31 @@ pub fn Container(Model: type, Event: type) type { const sides = this.properties.border.sides; switch (layout.direction) { - .horizontal => { + .vertical => { if (sides.top) { - available -|= 1; remainder -|= 1; } if (sides.bottom) { - available -|= 1; remainder -|= 1; } - }, - .vertical => { if (sides.left) { available -|= 1; - remainder -|= 1; } if (sides.right) { available -|= 1; + } + }, + .horizontal => { + if (sides.top) { + available -|= 1; + } + if (sides.bottom) { + available -|= 1; + } + if (sides.left) { + remainder -|= 1; + } + if (sides.right) { remainder -|= 1; } }, @@ -815,7 +823,7 @@ pub fn Container(Model: type, Event: type) type { .horizontal => if (child.properties.size.grow == .vertical or child.properties.size.grow == .vertical_only or child.properties.size.grow == .both) { child.size.y = available; }, - .vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .vertical_only or child.properties.size.grow == .both) { + .vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .horizontal_only or child.properties.size.grow == .both) { child.size.x = available; }, } @@ -862,23 +870,23 @@ pub fn Container(Model: type, Event: type) type { }; if (child.properties.size.grow != .fixed and child_size == smallest_size) { switch (layout.direction) { - .horizontal => if (child.properties.size.grow != .vertical) { + .horizontal => if (child.properties.size.grow != .vertical and child.properties.size.grow != .vertical_only) { child.size.x += size_to_correct; remainder -|= size_to_correct; }, - .vertical => if (child.properties.size.grow != .horizontal) { + .vertical => if (child.properties.size.grow != .horizontal and child.properties.size.grow != .horizontal_only) { child.size.y += size_to_correct; remainder -|= size_to_correct; }, } if (overflow > 0) { switch (layout.direction) { - .horizontal => if (child.properties.size.grow != .vertical) { + .horizontal => if (child.properties.size.grow != .vertical and child.properties.size.grow != .vertical_only) { child.size.x += 1; overflow -|= 1; remainder -|= 1; }, - .vertical => if (child.properties.size.grow != .horizontal) { + .vertical => if (child.properties.size.grow != .horizontal and child.properties.size.grow != .horizontal_only) { child.size.y += 1; overflow -|= 1; remainder -|= 1; diff --git a/src/event.zig b/src/event.zig index e97cb67..ed0d1f6 100644 --- a/src/event.zig +++ b/src/event.zig @@ -23,6 +23,8 @@ pub const SystemEvent = union(enum) { /// associated error message msg: []const u8, }, + /// Input line event received in non *raw mode* (instead of individual `key` events) + line: []const u8, /// Input key event received from the user key: Key, /// Mouse input event diff --git a/src/render.zig b/src/render.zig index 74f167d..4c3e01e 100644 --- a/src/render.zig +++ b/src/render.zig @@ -91,6 +91,7 @@ pub const Buffered = struct { var writer = terminal.writer(); const s = this.screen; const vs = this.virtual_screen; + for (0..this.size.y) |row| { for (0..this.size.x) |col| { const idx = (row * this.size.x) + col; @@ -123,6 +124,85 @@ pub const Buffered = struct { } }; +pub const Direct = struct { + gpa: Allocator, + size: Point, + resized: bool, + screen: []Cell, + + pub fn init(gpa: Allocator) @This() { + return .{ + .gpa = gpa, + .size = .{}, + .resized = true, + .screen = undefined, + }; + } + + pub fn deinit(this: *@This()) void { + this.gpa.free(this.screen); + } + + pub fn resize(this: *@This()) !Point { + this.size = .{}; + if (!this.resized) { + this.gpa.free(this.screen); + this.screen = undefined; + } + this.resized = true; + return terminal.getTerminalSize(); + } + + pub fn clear(this: *@This()) !void { + _ = this; + try terminal.clearScreen(); + } + + /// 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; + const origin: Point = container.origin; + + if (this.resized) { + this.size = size; + const n = @as(usize, this.size.x) * @as(usize, this.size.y); + this.screen = try this.gpa.alloc(Cell, n); + @memset(this.screen, .{}); + this.resized = false; + } + + const cells: []const Cell = try container.content(model); + + var idx: usize = 0; + var vs = this.screen; + const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x); + + blk: for (0..size.y) |row| { + for (0..size.x) |col| { + vs[anchor + (row * this.size.x) + col] = cells[idx]; + idx += 1; + + if (cells.len == idx) break :blk; + } + } + // free immediately + container.allocator.free(cells); + for (container.elements.items) |*element| try this.render(Container, element, Model, model); + } + + pub fn flush(this: *@This()) !void { + var writer = terminal.writer(); + for (0..this.size.y) |row| { + for (0..this.size.x) |col| { + const idx = (row * this.size.x) + col; + const cvs = this.screen[idx]; + try cvs.value(&writer); + if (cvs.style.cursor) return; // that's where the cursor should be left! + } + } + } +}; + const std = @import("std"); const meta = std.meta; const assert = std.debug.assert; diff --git a/src/terminal.zig b/src/terminal.zig index 6d7811b..38bd78d 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -98,7 +98,7 @@ pub fn setCursorPosition(pos: Point) !void { _ = try posix.write(posix.STDIN_FILENO, value); } -pub fn getCursorPosition() !Size.Position { +pub fn getCursorPosition() !Size { // Needs Raw mode (no wait for \n) to work properly cause // control sequence will not be written without it. _ = try posix.write(posix.STDIN_FILENO, "\x1b[6n");