From 97a240c54d6f2efcc027e10e63d6bd0a3a94ff67 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 20 Jan 2026 13:57:55 +0100 Subject: [PATCH] mod: example shows how dynamic sizing is achived that can be independent form the reported terminal size Setting the cursor with the `Direct` handler will cause the rendering to halt at that point and leave the cursor at point. Due to not enabling *raw mode* with the newly introduced `App.start` configuration options corresponding inputs are only visible to `zterm` once the input has been completed with a newline. With this it is not necessary for the renderer to know nothing more than the width of the terminal (which is implied through the `Container` sizes). Making it very trivial to implement. --- examples/direct.zig | 108 +++++++++++++++++++++++++++++++++++++++----- src/app.zig | 55 +++++++++++++++------- src/render.zig | 16 +++---- src/terminal.zig | 2 +- 4 files changed, 143 insertions(+), 38 deletions(-) diff --git a/examples/direct.zig b/examples/direct.zig index 817a2e4..f85aa84 100644 --- a/examples/direct.zig +++ b/examples/direct.zig @@ -1,3 +1,75 @@ +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 = size.x / 2 -| (text.len / 2); + const anchor = (y * size.x) + x; + + for (text, 0..) |cp, idx| { + cells[anchor + idx].style.fg = .white; + cells[anchor + idx].style.emphasis = &.{.bold}; + 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 = 5, + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + // .minSize = minSize, + .content = content, + }, + }; + } + + // 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 = .red; + cells[idx].style.fg = .black; + cells[idx].style.emphasis = &.{.bold}; + } + cells[this.len - 1].style.cursor = true; // marks the actual end of the rendering! + + const anchor = 1; + cells[anchor + 0].cp = 'N'; + cells[anchor + 1].cp = 'O'; + cells[anchor + 2].cp = 'R'; + } +}; + pub fn main() !void { errdefer |err| log.err("Application Error: {any}", .{err}); @@ -11,29 +83,42 @@ pub fn main() !void { defer renderer.deinit(); var container: App.Container = try .init(gpa, .{ - .border = .{ - .sides = .all, - }, .layout = .{ - .direction = .horizontal, - .separator = .{ .enabled = true }, + .direction = .vertical, }, .size = .{ - .dim = .{ .y = 30 }, .grow = .horizontal_only, }, }, .{}); defer container.deinit(); - try container.append(try .init(gpa, .{ + var quit_text: QuitText = .{}; + var intermediate: App.Container = try .init(gpa, .{ + .border = .{ + .sides = .all, + }, + .layout = .{ + .direction = .horizontal, + }, + }, quit_text.element()); + try intermediate.append(try .init(gpa, .{ .rectangle = .{ .fill = .blue }, }, .{})); - try container.append(try .init(gpa, .{ + try intermediate.append(try .init(gpa, .{ .rectangle = .{ .fill = .green }, }, .{})); + try container.append(intermediate); - try app.start(); // needs to become configurable, as what should be enabled / disabled (i.e. show cursor, hide cursor, use alternate screen, etc.) + var prompt: Prompt = .{}; + try container.append(try .init(gpa, .{ + .rectangle = .{ .fill = .red }, + .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 @@ -49,14 +134,15 @@ pub fn main() !void { // handle events for (0..len) |_| { const event = app.queue.pop(); - //log.debug("received event: {s}", .{@tagName(event)}); + 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`? .key => |key| { - if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(); + // if (key.eql(.{ .cp = 'q' })) app.quit(); + _ = key; }, // 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 }), diff --git a/src/app.zig b/src/app.zig index d79db80..7137c7f 100644 --- a/src/app.zig +++ b/src/app.zig @@ -31,6 +31,28 @@ pub fn App(comptime M: type, comptime E: type) type { quit_event: Thread.ResetEvent, termios: ?posix.termios = null, winch_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; @@ -48,10 +70,12 @@ 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) @@ -71,21 +95,21 @@ pub fn App(comptime M: type, comptime E: type) type { this.quit_event.reset(); this.thread = try Thread.spawn(.{}, @This().run, .{this}); - //var termios: posix.termios = undefined; - //try terminal.enableRawMode(&termios); - //if (this.termios) |_| {} else this.termios = termios; + var termios: posix.termios = undefined; + 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 +118,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; } } diff --git a/src/render.zig b/src/render.zig index 48b5ee8..4c3e01e 100644 --- a/src/render.zig +++ b/src/render.zig @@ -144,8 +144,6 @@ pub const Direct = struct { } pub fn resize(this: *@This()) !Point { - // TODO the size in the y-axis is *not fixed* and *not limited* - // how can I achive this? this.size = .{}; if (!this.resized) { this.gpa.free(this.screen); @@ -160,17 +158,17 @@ pub const Direct = struct { try terminal.clearScreen(); } - /// Render provided cells at size (anchor and dimension) into the *virtual screen*. + /// 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; + 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); @@ -179,7 +177,6 @@ pub const Direct = struct { 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]; @@ -200,6 +197,7 @@ pub const Direct = struct { 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! } } } 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");