a modern tui library written in zig

refactor: make code more idiomatic

- Added standard .gitattributes file for Zig projects.
- Reworked build.zig a little, hopefully it's a bit clearer. Also, now zig build will run all steps.
- outer: while in examples was redundant since there's only one loop to break from. switch expressions don't allow breaking from them, so breaking is only for loops, i.e. while and for.
- When returning a struct instance from a function, the compiler infers the return type from function signature, so instead of return MyType{...}; , it's more idiomatic to write return .{...};.
- Logging adds a new line by default, so you don't usually need to write \n like here: log.debug("event: {}\r\n", .{event});.

authored by Jora Troosh and committed by GitHub 4a463cfa a733860a

+2
.gitattributes
··· 1 + *.zig text eol=lf 2 + *.zon text eol=lf
+6 -6
README.md
··· 53 53 54 54 const log = std.log.scoped(.main); 55 55 56 - // Our EventType. This can contain internal events as well as Vaxis events. 56 + // This can contain internal events as well as Vaxis events. 57 57 // Internal events can be posted into the same queue as vaxis events to allow 58 58 // for a single event loop with exhaustive switching. Booya 59 59 const Event = union(enum) { ··· 102 102 103 103 // The main event loop. Vaxis provides a thread safe, blocking, buffered 104 104 // queue which can serve as the primary event queue for an application 105 - outer: while (true) { 105 + while (true) { 106 106 // nextEvent blocks until an event is in the queue 107 107 const event = vx.nextEvent(); 108 - log.debug("event: {}\r\n", .{event}); 109 - // exhaustive switching ftw. Vaxis will send events if your EventType 110 - // enum has the fields for those events (ie "key_press", "winsize") 108 + log.debug("event: {}", .{event}); 109 + // exhaustive switching ftw. Vaxis will send events if your Event enum 110 + // has the fields for those events (ie "key_press", "winsize") 111 111 switch (event) { 112 112 .key_press => |key| { 113 113 color_idx = switch (color_idx) { ··· 115 115 else => color_idx + 1, 116 116 }; 117 117 if (key.matches('c', .{ .ctrl = true })) { 118 - break :outer; 118 + break; 119 119 } else if (key.matches('l', .{ .ctrl = true })) { 120 120 vx.queueRefresh(); 121 121 } else {
+38 -29
build.zig
··· 3 3 pub fn build(b: *std.Build) void { 4 4 const target = b.standardTargetOptions(.{}); 5 5 const optimize = b.standardOptimizeOption(.{}); 6 + const root_source_file = std.Build.LazyPath.relative("src/main.zig"); 6 7 7 - const vaxis = b.addModule("vaxis", .{ .root_source_file = .{ .path = "src/main.zig" } }); 8 - 9 - const ziglyph = b.dependency("ziglyph", .{ 8 + // Dependencies 9 + const ziglyph_dep = b.dependency("ziglyph", .{ 10 10 .optimize = optimize, 11 11 .target = target, 12 12 }); 13 - vaxis.addImport("ziglyph", ziglyph.module("ziglyph")); 14 - 15 - const zigimg = b.dependency("zigimg", .{ 13 + const zigimg_dep = b.dependency("zigimg", .{ 16 14 .optimize = optimize, 17 15 .target = target, 18 16 }); 19 - vaxis.addImport("zigimg", zigimg.module("zigimg")); 20 17 21 - const exe = b.addExecutable(.{ 22 - .name = "vaxis", 23 - .root_source_file = .{ .path = "examples/pathological.zig" }, 18 + // Module 19 + const vaxis_mod = b.addModule("vaxis", .{ .root_source_file = root_source_file }); 20 + vaxis_mod.addImport("ziglyph", ziglyph_dep.module("ziglyph")); 21 + vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg")); 22 + 23 + // Examples 24 + const example_step = b.step("example", "Run examples"); 25 + 26 + const example = b.addExecutable(.{ 27 + .name = "vaxis_pathological_example", 28 + .root_source_file = std.Build.LazyPath.relative("examples/pathological.zig"), 24 29 .target = target, 25 30 .optimize = optimize, 26 31 }); 27 - exe.root_module.addImport("vaxis", vaxis); 32 + example.root_module.addImport("vaxis", vaxis_mod); 28 33 29 - const run_cmd = b.addRunArtifact(exe); 34 + const example_run = b.addRunArtifact(example); 35 + example_step.dependOn(&example_run.step); 36 + b.default_step.dependOn(example_step); 30 37 31 - run_cmd.step.dependOn(b.getInstallStep()); 38 + // Tests 39 + const tests_step = b.step("test", "Run tests"); 32 40 33 - if (b.args) |args| { 34 - run_cmd.addArgs(args); 35 - } 36 - 37 - const run_step = b.step("run", "Run the app"); 38 - run_step.dependOn(&run_cmd.step); 39 - 40 - // Creates a step for unit testing. This only builds the test executable 41 - // but does not run it. 42 - const lib_unit_tests = b.addTest(.{ 43 - .root_source_file = .{ .path = "src/main.zig" }, 41 + const tests = b.addTest(.{ 42 + .root_source_file = root_source_file, 44 43 .target = target, 45 44 .optimize = optimize, 46 45 }); 47 - lib_unit_tests.root_module.addImport("ziglyph", ziglyph.module("ziglyph")); 48 - lib_unit_tests.root_module.addImport("zigimg", zigimg.module("zigimg")); 46 + tests.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph")); 47 + tests.root_module.addImport("zigimg", zigimg_dep.module("zigimg")); 48 + 49 + const tests_run = b.addRunArtifact(tests); 50 + tests_step.dependOn(&tests_run.step); 51 + b.default_step.dependOn(tests_step); 49 52 50 - const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 53 + // Lints 54 + const lints_step = b.step("lint", "Run lints"); 51 55 52 - const test_step = b.step("test", "Run unit tests"); 53 - test_step.dependOn(&run_lib_unit_tests.step); 56 + const lints = b.addFmt(.{ 57 + .paths = &.{ "src", "build.zig" }, 58 + .check = true, 59 + }); 60 + 61 + lints_step.dependOn(&lints.step); 62 + b.default_step.dependOn(lints_step); 54 63 }
+7 -28
build.zig.zon
··· 1 1 .{ 2 2 .name = "vaxis", 3 - // This is a [Semantic Version](https://semver.org/). 4 - // In a future version of Zig it will be used for package deduplication. 5 3 .version = "0.1.0", 6 - 7 - // This field is optional. 8 - // This is currently advisory only; Zig does not yet do anything 9 - // with this value. 10 - //.minimum_zig_version = "0.11.0", 11 - 4 + .paths = .{""}, 12 5 .dependencies = .{ 13 6 .ziglyph = .{ 14 - .url = "https://codeberg.org/dude_the_builder/ziglyph/archive/ac50ab06c91d2dd632ff4573c035dafe3b374aba.tar.gz", 15 - .hash = "1220e097fbfb3a15a6f3484cf507f1f10ab571d1bcf519c3b5447ca727782b7a5264", 7 + .url = "https://codeberg.org/dude_the_builder/ziglyph/archive/ac50ab06c9.tar.gz", 8 + .hash = "1220e097fbfb3a15a6f3484cf507f1f10ab571d1bcf519c3b5447ca727782b7a5264", 9 + }, 10 + .zigimg = .{ 11 + .url = "https://github.com/zigimg/zigimg/archive/2224f91.tar.gz", 12 + .hash = "12207067e4892c48369415268648380859baa89c324748ae5bfda414a12868c9fc8b", 16 13 }, 17 - .zigimg = .{ 18 - .url = "https://github.com/zigimg/zigimg/archive/f6998808f283f8d3c2ef34e8b4af423bc1786f32.tar.gz", 19 - .hash = "12202ee5d22ade0c300e9e7eae4c1951bda3d5f236fe1a139eb3613b43e2f12a88db", 20 - } 21 - }, 22 - 23 - .paths = .{ 24 - // This makes *all* files, recursively, included in this package. It is generally 25 - // better to explicitly list the files and directories instead, to insure that 26 - // fetching from tarballs, file system paths, and version control all result 27 - // in the same contents hash. 28 - "", 29 - // For example... 30 - //"build.zig", 31 - //"build.zig.zon", 32 - //"src", 33 - //"LICENSE", 34 - //"README.md", 35 14 }, 36 15 }
+1 -1
examples/image.zig
··· 56 56 57 57 const img = imgs[n]; 58 58 const dims = try img.cellSize(win); 59 - const center = vaxis.alignment.center(win, dims.cols, dims.rows); 59 + const center = vaxis.widgets.alignment.center(win, dims.cols, dims.rows); 60 60 const scale = false; 61 61 const z_index = 0; 62 62 img.draw(center, scale, z_index);
+5 -5
examples/main.zig
··· 32 32 33 33 // The main event loop. Vaxis provides a thread safe, blocking, buffered 34 34 // queue which can serve as the primary event queue for an application 35 - outer: while (true) { 35 + while (true) { 36 36 // nextEvent blocks until an event is in the queue 37 37 const event = vx.nextEvent(); 38 - log.debug("event: {}\r\n", .{event}); 39 - // exhaustive switching ftw. Vaxis will send events if your EventType 38 + log.debug("event: {}", .{event}); 39 + // exhaustive switching ftw. Vaxis will send events if your Event 40 40 // enum has the fields for those events (ie "key_press", "winsize") 41 41 switch (event) { 42 42 .key_press => |key| { ··· 45 45 else => color_idx + 1, 46 46 }; 47 47 if (key.codepoint == 'c' and key.mods.ctrl) { 48 - break :outer; 48 + break; 49 49 } 50 50 }, 51 51 .winsize => |ws| { ··· 87 87 } 88 88 } 89 89 90 - // Our EventType. This can contain internal events as well as Vaxis events. 90 + // Our Event. This can contain internal events as well as Vaxis events. 91 91 // Internal events can be posted into the same queue as vaxis events to allow 92 92 // for a single event loop with exhaustive switching. Booya 93 93 const Event = union(enum) {
+2 -2
examples/pathological.zig
··· 19 19 try vx.enterAltScreen(); 20 20 try vx.queryTerminal(); 21 21 22 - outer: while (true) { 22 + while (true) { 23 23 const event = vx.nextEvent(); 24 24 switch (event) { 25 25 .winsize => |ws| { 26 26 try vx.resize(alloc, ws); 27 - break :outer; 27 + break; 28 28 }, 29 29 } 30 30 }
+5 -5
examples/text_input.zig
··· 6 6 7 7 const log = std.log.scoped(.main); 8 8 9 - // Our EventType. This can contain internal events as well as Vaxis events. 9 + // Our Event. This can contain internal events as well as Vaxis events. 10 10 // Internal events can be posted into the same queue as vaxis events to allow 11 11 // for a single event loop with exhaustive switching. Booya 12 12 const Event = union(enum) { ··· 59 59 60 60 // The main event loop. Vaxis provides a thread safe, blocking, buffered 61 61 // queue which can serve as the primary event queue for an application 62 - outer: while (true) { 62 + while (true) { 63 63 // nextEvent blocks until an event is in the queue 64 64 const event = vx.nextEvent(); 65 - log.debug("event: {}\r\n", .{event}); 66 - // exhaustive switching ftw. Vaxis will send events if your EventType 65 + log.debug("event: {}", .{event}); 66 + // exhaustive switching ftw. Vaxis will send events if your Event 67 67 // enum has the fields for those events (ie "key_press", "winsize") 68 68 switch (event) { 69 69 .key_press => |key| { ··· 72 72 else => color_idx + 1, 73 73 }; 74 74 if (key.matches('c', .{ .ctrl = true })) { 75 - break :outer; 75 + break; 76 76 } else if (key.matches('l', .{ .ctrl = true })) { 77 77 vx.queueRefresh(); 78 78 } else if (key.matches('n', .{ .ctrl = true })) {
+2 -2
src/InternalScreen.zig
··· 1 1 const std = @import("std"); 2 2 const assert = std.debug.assert; 3 - const Style = @import("cell.zig").Style; 4 - const Cell = @import("cell.zig").Cell; 3 + const Style = @import("Cell.zig").Style; 4 + const Cell = @import("Cell.zig"); 5 5 const Shape = @import("Mouse.zig").Shape; 6 6 7 7 const log = std.log.scoped(.internal_screen);
+1 -1
src/Screen.zig
··· 1 1 const std = @import("std"); 2 2 const assert = std.debug.assert; 3 3 4 - const Cell = @import("cell.zig").Cell; 4 + const Cell = @import("Cell.zig"); 5 5 const Shape = @import("Mouse.zig").Shape; 6 6 const Image = @import("Image.zig"); 7 7 const Winsize = @import("Tty.zig").Winsize;
+13 -16
src/Tty.zig
··· 1 1 const std = @import("std"); 2 2 const builtin = @import("builtin"); 3 3 const os = std.os; 4 - const vaxis = @import("main.zig"); 5 - const Vaxis = vaxis.Vaxis; 6 - const Event = @import("event.zig").Event; 4 + const Vaxis = @import("vaxis.zig").Vaxis; 7 5 const Parser = @import("Parser.zig"); 8 - const Key = vaxis.Key; 9 6 const GraphemeCache = @import("GraphemeCache.zig"); 10 7 11 8 const log = std.log.scoped(.tty); ··· 68 65 /// read input from the tty 69 66 pub fn run( 70 67 self: *Tty, 71 - comptime EventType: type, 72 - vx: *Vaxis(EventType), 68 + comptime Event: type, 69 + vx: *Vaxis(Event), 73 70 ) !void { 74 71 // create a pipe so we can signal to exit the run loop 75 72 const pipe = try os.pipe(); ··· 78 75 79 76 // get our initial winsize 80 77 const winsize = try getWinsize(self.fd); 81 - if (@hasField(EventType, "winsize")) { 78 + if (@hasField(Event, "winsize")) { 82 79 vx.postEvent(.{ .winsize = winsize }); 83 80 } 84 81 ··· 91 88 const WinchHandler = struct { 92 89 const Self = @This(); 93 90 94 - var vx_winch: *Vaxis(EventType) = undefined; 91 + var vx_winch: *Vaxis(Event) = undefined; 95 92 var fd: os.fd_t = undefined; 96 93 97 - fn init(vx_arg: *Vaxis(EventType), fd_arg: os.fd_t) !void { 94 + fn init(vx_arg: *Vaxis(Event), fd_arg: os.fd_t) !void { 98 95 vx_winch = vx_arg; 99 96 fd = fd_arg; 100 97 var act = os.Sigaction{ ··· 114 111 const ws = getWinsize(fd) catch { 115 112 return; 116 113 }; 117 - if (@hasField(EventType, "winsize")) { 114 + if (@hasField(Event, "winsize")) { 118 115 vx_winch.postEvent(.{ .winsize = ws }); 119 116 } 120 117 } ··· 156 153 const event = result.event orelse continue; 157 154 switch (event) { 158 155 .key_press => |key| { 159 - if (@hasField(EventType, "key_press")) { 156 + if (@hasField(Event, "key_press")) { 160 157 // HACK: yuck. there has to be a better way 161 158 var mut_key = key; 162 159 if (key.text) |text| { ··· 166 163 } 167 164 }, 168 165 .mouse => |mouse| { 169 - if (@hasField(EventType, "mouse")) { 166 + if (@hasField(Event, "mouse")) { 170 167 vx.postEvent(.{ .mouse = mouse }); 171 168 } 172 169 }, 173 170 .focus_in => { 174 - if (@hasField(EventType, "focus_in")) { 171 + if (@hasField(Event, "focus_in")) { 175 172 vx.postEvent(.focus_in); 176 173 } 177 174 }, 178 175 .focus_out => { 179 - if (@hasField(EventType, "focus_out")) { 176 + if (@hasField(Event, "focus_out")) { 180 177 vx.postEvent(.focus_out); 181 178 } 182 179 }, 183 180 .paste_start => { 184 - if (@hasField(EventType, "paste_start")) { 181 + if (@hasField(Event, "paste_start")) { 185 182 vx.postEvent(.paste_start); 186 183 } 187 184 }, 188 185 .paste_end => { 189 - if (@hasField(EventType, "paste_end")) { 186 + if (@hasField(Event, "paste_end")) { 190 187 vx.postEvent(.paste_end); 191 188 } 192 189 },
+3 -15
src/Window.zig
··· 4 4 const GraphemeIterator = ziglyph.GraphemeIterator; 5 5 6 6 const Screen = @import("Screen.zig"); 7 - const Cell = @import("cell.zig").Cell; 8 - const Segment = @import("cell.zig").Segment; 7 + const Cell = @import("Cell.zig"); 8 + const Segment = @import("Cell.zig").Segment; 9 9 const gw = @import("gwidth.zig"); 10 10 11 11 const log = std.log.scoped(.window); ··· 118 118 var word_iter = try WordIterator.init(segment.text); 119 119 while (word_iter.next()) |word| { 120 120 // break lines when we need 121 - if (isLineBreak(word.bytes)) { 121 + if (word.bytes[0] == '\r' or word.bytes[0] == '\n') { 122 122 row += 1; 123 123 col = 0; 124 124 wrapped = false; ··· 155 155 col += w; 156 156 } 157 157 } 158 - } 159 - } 160 - 161 - fn isLineBreak(str: []const u8) bool { 162 - if (std.mem.eql(u8, str, "\r\n")) { 163 - return true; 164 - } else if (std.mem.eql(u8, str, "\r")) { 165 - return true; 166 - } else if (std.mem.eql(u8, str, "\n")) { 167 - return true; 168 - } else { 169 - return false; 170 158 } 171 159 } 172 160
+5 -7
src/cell.zig src/Cell.zig
··· 1 1 const Image = @import("Image.zig"); 2 2 3 - pub const Cell = struct { 4 - char: Character = .{}, 5 - style: Style = .{}, 6 - link: Hyperlink = .{}, 7 - image: ?Image.Placement = null, 8 - }; 3 + char: Character = .{}, 4 + style: Style = .{}, 5 + link: Hyperlink = .{}, 6 + image: ?Image.Placement = null, 9 7 10 8 /// Segment is a contiguous run of text that has a constant style 11 9 pub const Segment = struct { ··· 17 15 pub const Character = struct { 18 16 grapheme: []const u8 = " ", 19 17 /// width should only be provided when the application is sure the terminal 20 - /// will meeasure the same width. This can be ensure by using the gwidth method 18 + /// will measure the same width. This can be ensure by using the gwidth method 21 19 /// included in libvaxis. If width is 0, libvaxis will measure the glyph at 22 20 /// render time 23 21 width: usize = 1,
+8 -27
src/main.zig
··· 1 + const std = @import("std"); 2 + 1 3 pub const Vaxis = @import("vaxis.zig").Vaxis; 2 4 pub const Options = @import("Options.zig"); 3 5 4 - const cell = @import("cell.zig"); 5 - pub const Cell = cell.Cell; 6 - pub const Style = cell.Style; 7 - pub const Segment = cell.Segment; 8 - pub const Color = cell.Color; 9 - 10 6 pub const Key = @import("Key.zig"); 7 + pub const Cell = @import("Cell.zig"); 8 + pub const Image = @import("Image.zig"); 11 9 pub const Mouse = @import("Mouse.zig"); 12 10 pub const Winsize = @import("Tty.zig").Winsize; 13 11 14 - pub const widgets = @import("widgets/main.zig"); 15 - pub const alignment = widgets.alignment; 16 - pub const border = widgets.border; 17 - 18 - pub const Image = @import("Image.zig"); 12 + pub const widgets = @import("widgets.zig"); 19 13 20 14 /// Initialize a Vaxis application. 21 - pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) { 22 - return Vaxis(EventType).init(opts); 15 + pub fn init(comptime Event: type, opts: Options) !Vaxis(Event) { 16 + return Vaxis(Event).init(opts); 23 17 } 24 18 25 19 test { 26 - _ = @import("GraphemeCache.zig"); 27 - _ = @import("Key.zig"); 28 - _ = @import("Mouse.zig"); 29 - _ = @import("Options.zig"); 30 - _ = @import("Parser.zig"); 31 - _ = @import("Screen.zig"); 32 - _ = @import("Tty.zig"); 33 - _ = @import("Window.zig"); 34 - _ = @import("cell.zig"); 35 - _ = @import("ctlseqs.zig"); 36 - _ = @import("event.zig"); 37 - _ = @import("gwidth.zig"); 38 - _ = @import("queue.zig"); 39 - _ = @import("vaxis.zig"); 20 + std.testing.refAllDecls(@This()); 40 21 }
+9 -9
src/vaxis.zig
··· 11 11 const InternalScreen = @import("InternalScreen.zig"); 12 12 const Window = @import("Window.zig"); 13 13 const Options = @import("Options.zig"); 14 - const Style = @import("cell.zig").Style; 15 - const Hyperlink = @import("cell.zig").Hyperlink; 14 + const Style = @import("Cell.zig").Style; 15 + const Hyperlink = @import("Cell.zig").Hyperlink; 16 16 const gwidth = @import("gwidth.zig"); 17 17 const Shape = @import("Mouse.zig").Shape; 18 18 const Image = @import("Image.zig"); ··· 34 34 35 35 const log = std.log.scoped(.vaxis); 36 36 37 - pub const EventType = T; 37 + pub const Event = T; 38 38 39 39 pub const Capabilities = struct { 40 40 kitty_keyboard: bool = false, ··· 83 83 84 84 /// Initialize Vaxis with runtime options 85 85 pub fn init(_: Options) !Self { 86 - return Self{ 86 + return .{ 87 87 .queue = .{}, 88 88 .tty = null, 89 89 .screen = .{}, ··· 151 151 self.queue.push(event); 152 152 } 153 153 154 - /// resize allocates a slice of cellsequal to the number of cells 154 + /// resize allocates a slice of cells equal to the number of cells 155 155 /// required to display the screen (ie width x height). Any previous screen is 156 156 /// freed when resizing 157 157 pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void { ··· 169 169 170 170 /// returns a Window comprising of the entire terminal screen 171 171 pub fn window(self: *Self) Window { 172 - return Window{ 172 + return .{ 173 173 .x_off = 0, 174 174 .y_off = 0, 175 175 .width = self.screen.width, ··· 207 207 if (std.mem.eql(u8, colorterm, "truecolor") or 208 208 std.mem.eql(u8, colorterm, "24bit")) 209 209 { 210 - if (@hasField(EventType, "cap_rgb")) { 210 + if (@hasField(Event, "cap_rgb")) { 211 211 self.postEvent(.cap_rgb); 212 212 } 213 213 } ··· 348 348 } 349 349 } 350 350 351 - // something is different, so let's loop throuugh everything and 351 + // something is different, so let's loop through everything and 352 352 // find out what 353 353 354 354 // foreground ··· 642 642 } 643 643 } 644 644 try tty.buffered_writer.flush(); 645 - return Image{ 645 + return .{ 646 646 .id = id, 647 647 .width = img.width, 648 648 .height = img.height,
+3
src/widgets.zig
··· 1 + pub const border = @import("widgets/border.zig"); 2 + pub const alignment = @import("widgets/alignment.zig"); 3 + pub const TextInput = @import("widgets/TextInput.zig");
+1 -1
src/widgets/TextInput.zig
··· 1 1 const std = @import("std"); 2 - const Cell = @import("../cell.zig").Cell; 3 2 const Key = @import("../Key.zig"); 3 + const Cell = @import("../Cell.zig"); 4 4 const Window = @import("../Window.zig"); 5 5 const GraphemeIterator = @import("ziglyph").GraphemeIterator; 6 6
src/widgets/align.zig src/widgets/alignment.zig
+4 -3
src/widgets/border.zig
··· 1 + const Cell = @import("../Cell.zig"); 1 2 const Window = @import("../Window.zig"); 2 - const cell = @import("../cell.zig"); 3 - const Character = cell.Character; 4 - const Style = cell.Style; 3 + 4 + const Style = Cell.Style; 5 + const Character = Cell.Character; 5 6 6 7 const horizontal = Character{ .grapheme = "─", .width = 1 }; 7 8 const vertical = Character{ .grapheme = "│", .width = 1 };
-3
src/widgets/main.zig
··· 1 - pub const TextInput = @import("TextInput.zig"); 2 - pub const border = @import("border.zig"); 3 - pub const alignment = @import("align.zig");