From 11531e9d4a4eefd740899384e48d00267a524535 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Thu, 6 Feb 2025 22:19:27 +0100 Subject: [PATCH] mod: remove `min_size` argument from `App.start` --- examples/container.zig | 48 ++++++++++++++++------------- src/app.zig | 69 +++++++++++------------------------------- src/container.zig | 56 ++++++++++++++-------------------- src/render.zig | 35 ++++++++------------- 4 files changed, 80 insertions(+), 128 deletions(-) diff --git a/examples/container.zig b/examples/container.zig index 728f744..5bd6be6 100644 --- a/examples/container.zig +++ b/examples/container.zig @@ -9,6 +9,7 @@ const log = std.log.scoped(.example); pub fn main() !void { errdefer |err| log.err("Application Error: {any}", .{err}); + // TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer { const deinit_status = gpa.deinit(); @@ -24,31 +25,26 @@ pub fn main() !void { var container = try App.Container.init(allocator, .{ .border = .{ - .color = .green, + .color = .blue, + .sides = .{ + .top = false, + .bottom = false, + }, .corners = .rounded, .separator = .{ .enabled = false }, }, - .layout = .{ .gap = 10, .direction = .horizontal }, + .layout = .{ .gap = 3, .direction = .horizontal }, }); try container.append(try App.Container.init(allocator, .{ - .border = .{ .color = .light_green, .corners = .squared }, + .border = .{ .color = .light_blue, .corners = .squared }, })); try container.append(try App.Container.init(allocator, .{ - .border = .{ .color = .light_green, .corners = .squared }, - })); - try container.append(try App.Container.init(allocator, .{ - .border = .{ .color = .light_green, .corners = .squared }, - })); - try container.append(try App.Container.init(allocator, .{ - .border = .{ .color = .light_green, .corners = .squared }, - })); - try container.append(try App.Container.init(allocator, .{ - .border = .{ .color = .light_red, .corners = .squared }, + .border = .{ .color = .light_blue, .corners = .squared }, })); defer container.deinit(); // NOTE: should the min-size here be required? - try app.start(null); + try app.start(); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); // event loop @@ -58,14 +54,25 @@ pub fn main() !void { switch (event) { .init => { - if (container.handle(event)) |e| app.postEvent(e); - continue; + try container.handle(event); + continue; // do not render }, .quit => break, .resize => |size| try renderer.resize(size), .key => |key| { - if (key.matches(.{ .cp = 'q' })) - app.quit(); + if (key.matches(.{ .cp = 'q' })) app.quit(); + + if (key.matches(.{ .cp = 'n', .mod = .{ .ctrl = true } })) { + try app.interrupt(); + defer app.start() catch @panic("could not start app event loop"); + var child = std.process.Child.init(&.{"hx"}, allocator); + _ = child.spawnAndWait() catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Spawning $EDITOR failed", + }, + }); + } }, .err => |err| { log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg }); @@ -73,9 +80,8 @@ pub fn main() !void { else => {}, } - // TODO: should instead use tryPost because it may block the main loop from actually removing events from the queue, deadlocking itself - if (container.handle(event)) |e| app.postEvent(e); - renderer.render(@TypeOf(container), &container); + try container.handle(event); + try renderer.render(@TypeOf(container), &container); try renderer.flush(); } } diff --git a/src/app.zig b/src/app.zig index 067e140..2762e09 100644 --- a/src/app.zig +++ b/src/app.zig @@ -44,7 +44,6 @@ pub fn App(comptime E: type) type { quit_event: std.Thread.ResetEvent = .{}, termios: ?std.posix.termios = null, attached_handler: bool = false, - min_size: ?Size = null, prev_size: Size = .{ .cols = 0, .rows = 0 }, pub const SignalHandler = struct { @@ -52,8 +51,7 @@ pub fn App(comptime E: type) type { callback: *const fn (context: *anyopaque) void, }; - pub fn start(this: *@This(), min_size: ?Size) !void { - this.min_size = min_size; + pub fn start(this: *@This()) !void { if (this.thread) |_| return; if (!this.attached_handler) { @@ -69,6 +67,9 @@ pub fn App(comptime E: type) type { .callback = @This().winsizeCallback, }); this.attached_handler = true; + + // post init event (as the very first element to be in the queue - event loop) + this.postEvent(.init); } this.quit_event.reset(); @@ -76,14 +77,16 @@ pub fn App(comptime E: type) type { var termios: std.posix.termios = undefined; try terminal.enableRawMode(&termios); - if (this.termios) |_| {} else { - this.termios = termios; - } + if (this.termios) |_| {} else this.termios = termios; + try terminal.saveScreen(); try terminal.enterAltScreen(); try terminal.hideCursor(); - // post init event (as the very first element to be in the queue - event loop) - this.postEvent(.init); + + // send initial size afterwards + const size = terminal.getTerminalSize(); + this.postEvent(.{ .resize = size }); + this.prev_size = size; } pub fn interrupt(this: *@This()) !void { @@ -127,15 +130,6 @@ pub fn App(comptime E: type) type { fn winsizeCallback(ptr: *anyopaque) void { const this: *@This() = @ptrCast(@alignCast(ptr)); const size = terminal.getTerminalSize(); - // check for minimal size (if any was provided) - if (this.min_size) |min_size| { - if (size.cols < min_size.cols or size.rows < min_size.rows) { - this.postEvent(.{ - .err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" }, - }); - return; - } - } if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) { this.postEvent(.{ .resize = size }); this.prev_size = size; @@ -161,19 +155,7 @@ pub fn App(comptime E: type) type { // send initial terminal size // changes are handled by the winch signal handler // see `App.start` and `App.registerWinch` for details - { - // TODO: what should happen if the initial window size is too small? - // -> currently the first render call will then crash the application (which happens anyway) - const size = terminal.getTerminalSize(); - if (this.min_size) |min_size| { - if (size.cols < min_size.cols or size.rows < min_size.rows) { - this.postEvent(.{ - .err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" }, - }); - } - } - this.postEvent(.{ .resize = size }); - } + {} // thread to read user inputs var buf: [256]u8 = undefined; @@ -185,9 +167,8 @@ pub fn App(comptime E: type) type { if (buf[0] == 0x1b and read_bytes > 1) { switch (buf[1]) { 0x4F => { // ss3 - if (read_bytes < 3) { - continue; - } + if (read_bytes < 3) continue; + const key: ?Key = switch (buf[2]) { 0x1B => null, 'A' => .{ .cp = Key.up }, @@ -203,14 +184,11 @@ pub fn App(comptime E: type) type { 'S' => .{ .cp = Key.f4 }, else => null, }; - if (key) |k| { - this.postEvent(.{ .key = k }); - } + if (key) |k| this.postEvent(.{ .key = k }); }, 0x5B => { // csi - if (read_bytes < 3) { - continue; - } + if (read_bytes < 3) continue; + // We start iterating at index 2 to get past the '[' const sequence = for (buf[2..], 2..) |b, i| { switch (b) { @@ -309,15 +287,6 @@ pub fn App(comptime E: type) type { .rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break, .cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break, }; - // check for minimal size (if any was provided) - if (this.min_size) |min_size| { - if (size.cols < min_size.cols or size.rows < min_size.rows) { - this.postEvent(.{ - .err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" }, - }); - break; - } - } if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) { this.postEvent(.{ .resize = size }); this.prev_size = size; @@ -355,9 +324,7 @@ pub fn App(comptime E: type) type { 0x7f => .{ .cp = Key.backspace }, else => { var iter = terminal.code_point.Iterator{ .bytes = buf[0..read_bytes] }; - while (iter.next()) |cp| { - this.postEvent(.{ .key = .{ .cp = cp.code } }); - } + while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } }); continue; }, }; diff --git a/src/container.zig b/src/container.zig index c45fe9d..982bf83 100644 --- a/src/container.zig +++ b/src/container.zig @@ -52,32 +52,36 @@ pub const Border = struct { // render top and bottom border for (0..size.cols) |col| { const last_row = (size.rows - 1) * size.cols; - if (col == 0) { + if (this.sides.left and col == 0) { // top left corner - cells[col].cp = frame[0]; + if (this.sides.top) cells[col].cp = frame[0]; // bottom left corner - cells[last_row + col].cp = frame[4]; - } else if (col == size.cols - 1) { + if (this.sides.bottom) cells[last_row + col].cp = frame[4]; + } else if (this.sides.right and col == size.cols - 1) { // top right corner - cells[col].cp = frame[2]; + if (this.sides.top) cells[col].cp = frame[2]; // bottom left corner - cells[last_row + col].cp = frame[5]; + if (this.sides.bottom) cells[last_row + col].cp = frame[5]; } else { // top side - cells[col].cp = frame[1]; + if (this.sides.top) cells[col].cp = frame[1]; // bottom side - cells[last_row + col].cp = frame[1]; + if (this.sides.bottom) cells[last_row + col].cp = frame[1]; } - cells[col].style.fg = this.color; - cells[last_row + col].style.fg = this.color; + if (this.sides.top) cells[col].style.fg = this.color; + if (this.sides.bottom) cells[last_row + col].style.fg = this.color; } // render left and right border for (1..size.rows - 1) |row| { const idx = (row * size.cols); - cells[idx].cp = frame[3]; // left - cells[idx].style.fg = this.color; - cells[idx + size.cols - 1].cp = frame[3]; // right - cells[idx + size.cols - 1].style.fg = this.color; + if (this.sides.left) { + cells[idx].cp = frame[3]; // left + cells[idx].style.fg = this.color; + } + if (this.sides.right) { + cells[idx + size.cols - 1].cp = frame[3]; // right + cells[idx + size.cols - 1].style.fg = this.color; + } } // TODO: the separator should be rendered regardless of the gap @@ -258,7 +262,7 @@ pub fn Container(comptime Event: type) type { try this.elements.append(element); } - pub fn handle(this: *@This(), event: Event) ?Event { + pub fn handle(this: *@This(), event: Event) !void { switch (event) { .init => log.debug(".init event", .{}), .resize => |size| resize: { @@ -359,29 +363,15 @@ pub fn Container(comptime Event: type) type { element_size.anchor.col += 1; } - // TODO: adjust size according to the layout of the `Container` - if (element.handle(.{ .resize = element_size })) |e| { - _ = e; - } + try element.handle(.{ .resize = element_size }); } - return null; }, - else => {}, + else => for (this.elements.items) |*element| try element.handle(event), } - for (this.elements.items) |*element| { - if (element.handle(event)) |e| { - // TODO: if only the top level container returns a single - // event (i.e. as a reaction to a certain other event) what - // should happen to potential other events? - _ = e; - } - } - return null; } - pub fn contents(this: *const @This()) []const Cell { - // TODO: use the size and the corresponding contents to determine what should be show in form of a `Cell` array - const cells = this.allocator.alloc(Cell, this.size.cols * this.size.rows) catch @panic("Container::contents: Out of memory."); + pub fn contents(this: *const @This()) ![]const Cell { + const cells = try this.allocator.alloc(Cell, this.size.cols * this.size.rows); @memset(cells, .{}); // reset all cells this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len)); return cells; diff --git a/src/render.zig b/src/render.zig index 8efb048..3d9c9a1 100644 --- a/src/render.zig +++ b/src/render.zig @@ -1,12 +1,3 @@ -//! Renderer which holds the screen to compare with the previous screen for efficient rendering. -//! Each renderer should at least implement these functions: -//! - resize(this: *@This(), size: Size) void {} -//! - clear(this: *@This(), size: Size) !void {} -//! - render(this: *@This(), size: Size, contents: []u8) !void {} -//! -//! Each `Renderer` should be able to be used interchangeable without having to -//! change any code of any `Layout` or `Widget`. The only change should be the -//! passed type to `zterm.App` _R_ parameter. const std = @import("std"); const terminal = @import("terminal.zig"); @@ -45,14 +36,12 @@ pub const Buffered = struct { log.debug("renderer::resize", .{}); defer this.size = size; - if (this.size.cols >= size.cols or this.size.rows >= size.rows) { - if (!this.created) { - this.screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory."); - @memset(this.screen, .{}); - this.virtual_screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory."); - @memset(this.virtual_screen, .{}); - this.created = true; - } + if (!this.created) { + this.screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory."); + @memset(this.screen, .{}); + this.virtual_screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory."); + @memset(this.virtual_screen, .{}); + this.created = true; } else { this.allocator.free(this.screen); this.screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory."); @@ -72,9 +61,9 @@ pub const Buffered = struct { } /// Render provided cells at size (anchor and dimension) into the *virtual screen*. - pub fn render(this: *@This(), comptime T: type, container: *T) void { + pub fn render(this: *@This(), comptime T: type, container: *T) !void { const size: Size = container.size; - const cells: []const Cell = container.contents(); + const cells: []const Cell = try container.contents(); if (cells.len == 0) return; @@ -93,10 +82,11 @@ pub const Buffered = struct { if (cells.len == idx) break :blk; } } + // free immediately container.allocator.free(cells); for (container.elements.items) |*element| { - this.render(T, element); + try this.render(T, element); } } @@ -112,10 +102,9 @@ pub const Buffered = struct { const idx = (row * this.size.cols) + col; const cs = s[idx]; const cvs = vs[idx]; - if (cs.eql(cvs)) - continue; + if (cs.eql(cvs)) continue; + // render differences found in virtual screen - // TODO: improve the writing speed (many unnecessary writes (i.e. the style for every character..)) try terminal.setCursorPosition(.{ .row = @truncate(row + 1), .col = @truncate(col + 1) }); try cvs.value(writer); // update screen to be the virtual screen for the next frame