From 5227a33d0ae17547bb0312aa135fbbe798f63e66 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 28 Oct 2025 19:57:10 +0100 Subject: [PATCH] mod: bump `zig` to master version; bump `zterm` dependency Use progress example for initial tui-website test application. --- .gitea/workflows/release.yaml | 4 +- .gitea/workflows/test.yaml | 4 +- .typos-config | 2 +- README.md | 3 + build.zig | 81 +++------- build.zig.zon | 33 +++-- src/main.zig | 271 ++++++++++++++++++++-------------- 7 files changed, 204 insertions(+), 194 deletions(-) diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index bc0ba46..bb255fc 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -15,9 +15,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Setup zig installation - uses: mlugg/setup-zig@v1 + uses: mlugg/setup-zig@v2 with: - version: 0.13.0 + version: master - name: Run tests run: zig build --release=fast - name: Release build artifacts diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index 0f6bf71..58791a9 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -14,9 +14,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Setup zig installation - uses: mlugg/setup-zig@v1 + uses: mlugg/setup-zig@v2 with: - version: 0.13.0 + version: master - name: Lint check run: zig fmt --check . - name: Spell checking diff --git a/.typos-config b/.typos-config index 7b30bb8..4188944 100644 --- a/.typos-config +++ b/.typos-config @@ -1,2 +1,2 @@ [files] -extend-exclude = [] +extend-exclude = ["build.zig.zon"] diff --git a/README.md b/README.md index e801e41..e2e9686 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ This is my terminal based website. It is served as a tui application via ssh and It contains information about me and my projects as well as blog entries about something I feel like writing something about. +> [!caution] +> Only builds using the zig master version are tested to work. + ## zterm This tui application also serves as an example implementation of the [zterm](https://gitea.yves-biener.de/yves-biener/zterm) library. diff --git a/build.zig b/build.zig index 2299d3c..8b537e8 100644 --- a/build.zig +++ b/build.zig @@ -1,66 +1,32 @@ const std = @import("std"); -// Although this function looks imperative, note that its job is to -// declaratively construct a build graph that will be executed by an external -// runner. pub fn build(b: *std.Build) void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); - - // Standard optimization options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not - // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); - // Dependencies - const zlog = b.dependency("zlog", .{ - .optimize = optimize, - .target = target, - .timestamp = true, - }); const zterm = b.dependency("zterm", .{ - .optimize = optimize, .target = target, + .optimize = optimize, }); const exe = b.addExecutable(.{ - .name = "tui-website", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, + .name = "tui_website", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "zterm", .module = zterm.module("zterm") }, + }, + }), }); - exe.root_module.addImport("zlog", zlog.module("zlog")); - exe.root_module.addImport("zterm", zterm.module("zterm")); - // This declares intent for the executable to be installed into the - // standard location when the user invokes the "install" step (the default - // step when running `zig build`). b.installArtifact(exe); - const exe_check = b.addExecutable(.{ - .name = "check", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - exe_check.root_module.addImport("zlog", zlog.module("zlog")); - exe_check.root_module.addImport("zterm", zterm.module("zterm")); + const run_step = b.step("run", "Run the app"); - const check = b.step("check", "Check if project compiles"); - check.dependOn(&exe_check.step); - - // This *creates* a Run step in the build graph, to be executed when another - // step is evaluated that depends on it. The next line below will establish - // such a dependency. const run_cmd = b.addRunArtifact(exe); - - // By making the run step depend on the install step, it will be run from the - // installation directory rather than directly from within the cache directory. - // This is not necessary, however, if the application depends on other installed - // files, this ensures they will be present and in the expected location. + run_step.dependOn(&run_cmd.step); run_cmd.step.dependOn(b.getInstallStep()); // This allows the user to pass arguments to the application in the build @@ -69,23 +35,12 @@ pub fn build(b: *std.Build) void { run_cmd.addArgs(args); } - // This creates a build step. It will be visible in the `zig build --help` menu, - // and can be selected like this: `zig build run` - // This will evaluate the `run` step rather than the default, which is "install". - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); - - const exe_unit_tests = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, }); - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); - - // Similar to creating the run step earlier, this exposes a `test` step to - // the `zig build --help` menu, providing a way for the user to request - // running the unit tests. - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_exe_unit_tests.step); + // A run step that will run the test executable. + const run_exe_tests = b.addRunArtifact(exe_tests); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_exe_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 586c99c..b9c37d1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,30 +6,35 @@ // // It is redundant to include "zig" in this name because it is already // within the Zig package namespace. - .name = "tui-website", - + .name = .tui_website, // This is a [Semantic Version](https://semver.org/). // In a future version of Zig it will be used for package deduplication. .version = "0.0.0", - - // This field is optional. - // This is currently advisory only; Zig does not yet do anything - // with this value. - //.minimum_zig_version = "0.11.0", - + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0x93d98a4d9d000e9c, // Changing this has security and trust implications. + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.16.0-dev.463+f624191f9", // This field is optional. // Each dependency must either provide a `url` and `hash`, or a `path`. // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ - .zlog = .{ - .url = "git+https://gitea.yves-biener.de/yves-biener/zlog#06752299be5eabf191d65d1fda29bc439fea2b1e", - .hash = "1220fd395b4770eb6bdc6468aece6bd80af53752ae10b52419cba44cf84f2427a49b", - }, .zterm = .{ - .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#6cd78d04182977013f6864963473e28cd21ae5f9", - .hash = "1220136f9f07b702c473a102036cd059e85e2d29c9f94c5f103a8fa3355084b86cd7", + .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#89aeac1e968f1390bd945f734aac8612efbab179", + .hash = "zterm-0.3.0-1xmmELjzGwBxVlqXRHn7p-sXFU9xPxqFMxF0PY2CkzFn", }, }, .paths = .{ diff --git a/src/main.zig b/src/main.zig index adb72cf..c7b7c77 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,131 +1,178 @@ -const std = @import("std"); -const zlog = @import("zlog"); -const zterm = @import("zterm"); +const QuitText = struct { + const text = "Press ctrl+c to quit."; -const App = zterm.App( - union(enum) {}, - zterm.Renderer.Direct, - true, -); -const Cell = zterm.Cell; -const Key = zterm.Key; -const Layout = App.Layout; -const Widget = App.Widget; + pub fn element(this: *@This()) App.Element { + return .{ .ptr = this, .vtable = &.{ .content = content } }; + } -pub const std_options = zlog.std_options; -const log = std.log.scoped(.default); + 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 row = 2; + const col = size.x / 2 -| (text.len / 2); + const anchor = (row * size.x) + col; + + for (text, 0..) |cp, idx| { + cells[anchor + idx].style.fg = .white; + cells[anchor + idx].style.bg = .black; + cells[anchor + idx].cp = cp; + + // NOTE do not write over the contents of this `Container`'s `Size` + if (anchor + idx == cells.len - 1) break; + } + } +}; pub fn main() !void { errdefer |err| log.err("Application Error: {any}", .{err}); - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; - defer { - const deinit_status = gpa.deinit(); - // fail test; can't try in defer as defer is executed after we return - if (deinit_status == .leak) { - log.err("memory leak", .{}); - } - } + var gpa: std.heap.DebugAllocator(.{}) = .init; + defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); + const allocator = gpa.allocator(); - var app: App = .{}; - var renderer: App.Renderer = .{}; + var app: App = .init(.{}); + var renderer = zterm.Renderer.Buffered.init(allocator); + defer renderer.deinit(); - var layout = Layout.createFrom(vcontainer: { - var vcontainer = Layout.VContainer.init(allocator, .{ - .{ - Layout.createFrom(framing: { - var framing = Layout.Framing.init(allocator, .{ - .title = .{ - .str = "Welcome to my terminal website", - .style = .{ - .ul = .{ .index = 6 }, - .ul_style = .single, - }, - }, - }, .{ - .layout = Layout.createFrom(hcontainer: { - var hcontainer = Layout.HContainer.init(allocator, .{ - .{ - Widget.createFrom(header: { - var header = Widget.Text.init(.left, &[1]Cell{ - .{ .content = "Yves Biener", .style = .{ .bold = true } }, - }); - break :header &header; - }), - 25, - }, - .{ - Widget.createFrom(name: { - var name = Widget.Text.init(.center, &[1]Cell{ - .{ .content = "File name", .style = .{ .bold = true } }, - }); - break :name &name; - }), - 50, - }, - .{ - Widget.createFrom(contacts: { - var contacts = Widget.Text.init(.right, &[1]Cell{ - .{ .content = "Contact", .style = .{ .bold = true, .ul_style = .single } }, - }); - break :contacts &contacts; - }), - 25, - }, - }); - break :hcontainer &hcontainer; - }), - }); - break :framing &framing; - }), - 10, - }, - .{ - Layout.createFrom(margin: { - var margin = Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{ - .widget = Widget.createFrom(body: { - const doc = try std.fs.cwd().openFile("./doc/test.md", .{}); - defer doc.close(); - var body = Widget.RawText.init(allocator, doc); - break :body &body; - }), - }); - break :margin &margin; - }), - 90, + var progress_percent: u8 = 0; + var quit_text: QuitText = .{}; + + var container = try App.Container.init(allocator, .{ + .layout = .{ .padding = .all(5), .direction = .vertical }, + }, quit_text.element()); + defer container.deinit(); + + { + var progress: App.Progress(.progress) = .init(&app.queue, .{ + .percent = .{ + .enabled = true, + .alignment = .left, }, + .fg = .blue, + .bg = .grey, }); - break :vcontainer &vcontainer; - }); - defer layout.deinit(); - + try container.append(try App.Container.init(allocator, .{}, progress.element())); + } + { + var progress: App.Progress(.progress) = .init(&app.queue, .{ + .percent = .{ + .enabled = true, + .alignment = .middle, // default + }, + .fg = .red, + .bg = .grey, + }); + try container.append(try App.Container.init(allocator, .{}, progress.element())); + } + { + var progress: App.Progress(.progress) = .init(&app.queue, .{ + .percent = .{ + .enabled = true, + .alignment = .right, + }, + .fg = .green, + .bg = .grey, + }); + try container.append(try App.Container.init(allocator, .{}, progress.element())); + } + { + var progress: App.Progress(.progress) = .init(&app.queue, .{ + .percent = .{ .enabled = false }, + .fg = .default, + .bg = .grey, + }); + try container.append(try App.Container.init(allocator, .{}, progress.element())); + } try app.start(); - defer app.stop() catch unreachable; + defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); - // App.Event loop - while (true) { - const event = app.nextEvent(); + var framerate: u64 = 60; + var tick_ms: u64 = @divFloor(time.ms_per_s, framerate); + var next_frame_ms: u64 = 0; - switch (event) { - .quit => break, - .key => |key| { - // ctrl+c to quit - if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { - app.quit(); - break; // no need to render this frame anyway - } - }, - .err => |err| { - log.err("Received {any} with message: {s}", .{ err.err, err.msg }); - }, - else => {}, + var increase_progress: u64 = 10; + + // Continuous drawing + // draw loop + draw: while (true) { + const now_ms: u64 = @intCast(time.milliTimestamp()); + if (now_ms >= next_frame_ms) { + next_frame_ms = now_ms + tick_ms; + } else { + std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms); + next_frame_ms += tick_ms; } - const events = try layout.handle(event); - for (events.items) |e| { - app.postEvent(e); + // NOTE time based progress increasion + increase_progress -= 1; + if (increase_progress == 0) { + increase_progress = 10; + progress_percent += 1; + if (progress_percent > 100) progress_percent = 0; + app.postEvent(.{ .progress = progress_percent }); } - try layout.render(&renderer); + + const len = blk: { + app.queue.lock(); + defer app.queue.unlock(); + break :blk app.queue.len(); + }; + + // handle events + for (0..len) |_| { + const event = app.queue.drain() orelse break; + log.debug("handling event: {s}", .{@tagName(event)}); + // pre event handling + switch (event) { + .key => |key| { + if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(); + }, + .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), + .focus => |b| { + // NOTE reduce framerate in case the window is not focused and restore again when focused + framerate = if (b) 60 else 15; + tick_ms = @divFloor(time.ms_per_s, framerate); + }, + else => {}, + } + + container.handle(&app.model, event) catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Container Event handling failed", + }, + }); + + // post event handling + switch (event) { + .quit => break :draw, + else => {}, + } + } + + container.resize(try renderer.resize()); + container.reposition(.{}); + 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 time = std.time; +const assert = std.debug.assert; +const zterm = @import("zterm"); +const App = zterm.App( + struct {}, + union(enum) { + progress: u8, + }, +); + +test { + std.testing.refAllDeclsRecursive(@This()); +}