From 6fea65a3597bd973c6465ec82175246074743f26 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Mon, 7 Oct 2024 19:28:01 +0200 Subject: [PATCH] setup project using and --- .gitignore | 3 ++ build.zig | 90 ++++++++++++++++++++++++++++++++ build.zig.zon | 43 ++++++++++++++++ src/main.zig | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6abcf3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.zig-cache +zig-out +log diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..4f45ba7 --- /dev/null +++ b/build.zig @@ -0,0 +1,90 @@ +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 vaxis_dep = b.dependency("vaxis", .{ + .optimize = optimize, + .target = target, + }); + const zlog_dep = b.dependency("zlog", .{ + .optimize = optimize, + .target = target, + .timestamp = true, + }); + + const exe = b.addExecutable(.{ + .name = "tui-website", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis")); + exe.root_module.addImport("zlog", zlog_dep.module("zlog")); + + // 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); + + // 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_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + 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); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + 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_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..1aea534 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,43 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .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", + + // 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#73991389f6f4156e5ccbc6afd611bc34fe738a72", + .hash = "1220d86aec0bf263dd4df028e017f094f4c530cc2367a3cd031989bbe94f466da1e0", + }, + .vaxis = .{ + .url = "git+https://github.com/rockorager/libvaxis#e3062d62b60bf8a351f267a77e1a94e2614394aa", + .hash = "1220479d09031168a100748e103ef5a8ffec5d4b0ff74a7e45cf2a9b09f82342b91f", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..b57d8a8 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,138 @@ +const std = @import("std"); + +const vaxis = @import("vaxis"); +const zlog = @import("zlog"); + +const TextInput = vaxis.widgets.TextInput; + +pub const std_options = zlog.std_options; + +const Event = union(enum) { + key_press: vaxis.Key, + winsize: vaxis.Winsize, +}; + +pub fn main() !void { + 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) { + std.log.err("memory leak", .{}); + } + } + const alloc = gpa.allocator(); + + // Initialize a tty + var tty = try vaxis.Tty.init(); + defer tty.deinit(); + + // Initialize Vaxis + var vx = try vaxis.init(alloc, .{}); + // deinit takes an optional allocator. If your program is exiting, you can + // choose to pass a null allocator to save some exit time. + defer vx.deinit(alloc, tty.anyWriter()); + + // The event loop requires an intrusive init. We create an instance with + // stable pointers to Vaxis and our TTY, then init the instance. Doing so + // installs a signal handler for SIGWINCH on posix TTYs + // + // This event loop is thread safe. It reads the tty in a separate thread + var loop: vaxis.Loop(Event) = .{ + .tty = &tty, + .vaxis = &vx, + }; + try loop.init(); + + // Start the read loop. This puts the terminal in raw mode and begins + // reading user input + try loop.start(); + defer loop.stop(); + + // Optionally enter the alternate screen + try vx.enterAltScreen(tty.anyWriter()); + + // We'll adjust the color index every keypress for the border + var color_idx: u8 = 0; + + // init our text input widget. The text input widget needs an allocator to + // store the contents of the input + var text_input = TextInput.init(alloc, &vx.unicode); + defer text_input.deinit(); + + // Sends queries to terminal to detect certain features. This should always + // be called after entering the alt screen, if you are using the alt screen + try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); + + while (true) { + // nextEvent blocks until an event is in the queue + const event = loop.nextEvent(); + // exhaustive switching ftw. Vaxis will send events if your Event enum + // has the fields for those events (ie "key_press", "winsize") + switch (event) { + .key_press => |key| { + color_idx = switch (color_idx) { + 255 => 0, + else => color_idx + 1, + }; + if (key.matches('c', .{ .ctrl = true })) { + break; + } else if (key.matches('l', .{ .ctrl = true })) { + vx.queueRefresh(); + } else { + try text_input.update(.{ .key_press = key }); + } + }, + + // winsize events are sent to the application to ensure that all + // resizes occur in the main thread. This lets us avoid expensive + // locks on the screen. All applications must handle this event + // unless they aren't using a screen (IE only detecting features) + // + // The allocations are because we keep a copy of each cell to + // optimize renders. When resize is called, we allocated two slices: + // one for the screen, and one for our buffered screen. Each cell in + // the buffered screen contains an ArrayList(u8) to be able to store + // the grapheme for that cell. Each cell is initialized with a size + // of 1, which is sufficient for all of ASCII. Anything requiring + // more than one byte will incur an allocation on the first render + // after it is drawn. Thereafter, it will not allocate unless the + // screen is resized + .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), + } + + // vx.window() returns the root window. This window is the size of the + // terminal and can spawn child windows as logical areas. Child windows + // cannot draw outside of their bounds + const win = vx.window(); + + // Clear the entire space because we are drawing in immediate mode. + // vaxis double buffers the screen. This new frame will be compared to + // the old and only updated cells will be drawn + win.clear(); + + // Create a style + const style: vaxis.Style = .{ + .fg = .{ .index = color_idx }, + }; + + // Create a bordered child window + const child = win.child(.{ + .x_off = win.width / 2 - 20, + .y_off = win.height / 2 - 3, + .width = .{ .limit = 40 }, + .height = .{ .limit = 3 }, + .border = .{ + .where = .all, + .style = style, + }, + }); + + // Draw the text_input in the child window + text_input.draw(child); + + // Render the screen. Using a buffered writer will offer much better + // performance, but is not required + try vx.render(tty.anyWriter()); + } +}