23 Commits

Author SHA1 Message Date
a83e86f8d9 feat(element/scrollable): background configuration
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m30s
2025-11-02 21:00:17 +01:00
4567963ff2 mod(example/text): improve event loop; add stick table heading for used emphasis
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 59s
This should be the way it should be done all the time, such that you
are not rendering for every input, but instead handle all `App.Event`s
that happened between the last render and the current. This shares
similarities with the continuous event loops, which also batches
the events only with the exception that it instead blocks (see
`App.Queue.poll`).
2025-11-02 16:10:11 +01:00
d4cc520826 fix(element/scrollable): display and user interaction
Fix initial render to show scrollbar immediately if required. Show and
hide scrollbar correctly when content size or terminal size changes. Add
keybindings for scrolling similar to pager keybindings.
2025-11-02 16:05:53 +01:00
b3dc8096d7 mod(event): do not care about enum declarations
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 56s
The union type can not be constructed with declarations anyway:
`error: reified unions must have no decls`; meaning there is no point in
even trying to do so.
2025-11-01 21:44:53 +01:00
7cd1fb139f mod(element/scrollable): ensure minSize returns correct dimensions
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 56s
Enforce that `minSize` returns a `Point` with corresponding minimal
dimensions with respect to the provided available size. This means that
an `Element` implementation that provides a `minSize` function may even
return a too small dimension, which would automatically be resized to be
at least as big as the provided size.
2025-11-01 14:37:14 +01:00
f70ea05244 mod(element/scrollable): introduce minSize function for Element
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 3m24s
The minimal required size can the queried from the containing `Element`
of the `Container` that is provided to the `Scrollable` to dynamically
adjust its size.

Currently abritrary nesting is not supported / tested.
2025-11-01 13:55:16 +01:00
c645c2efee fix: changes from newest zig version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m3s
2025-11-01 00:03:04 +01:00
89aeac1e96 doc: correct alert blocks
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m0s
2025-10-26 21:46:00 +01:00
feae9fa1a4 feat(model): implement Elm architecture
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m2s
Now the `App` contains a state which is a user-defined `struct` which
is passed to the `handle` and `contents` callbacks for `Container`'s and
`Element`'s. Built-in `Element`'s shall not access the `App.Model` and
should therefore never cause any side-effects.

User-defined events shall be used to act as *messages* to cause
potential side-effects for the model. This is the reason why only
the `handle` callback has a non-const pointer to the `App.Model`. The
`contents` callback can only access the `App.Model` read-only to use for
generating the *view* (in context of the elm architecture).
2025-10-26 15:58:07 +01:00
8f90f57f44 fix(terminal): correctly restore termios and screen's during panic_handler
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m1s
2025-10-19 11:44:17 +02:00
fee3c796d9 doc(terminal): remove resolved issue
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m0s
2025-10-18 23:31:49 +02:00
547f553404 fix(terminal): restore screen correctly with alt screen
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m3s
2025-10-18 23:30:15 +02:00
1f6bbcc45e mod(examples/progress): show multiple different configuration options
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 59s
2025-10-01 11:14:22 +02:00
832fc45c3e chor: use new Writer interface for terminal's Writer; fix test cases
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 3m38s
2025-10-01 10:59:29 +02:00
aa17e13b99 lint: correct typo
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 58s
2025-09-29 23:11:28 +02:00
cba07b119c chor: upgrade to latest zig; remove zg dependency
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 55s
2025-09-29 23:09:42 +02:00
f256a79da0 chor: bumb zig version to 0.16.0-dev
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m41s
2025-08-27 12:17:20 +02:00
c50b10f32d fix: zig update build errors
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m35s
2025-08-06 16:14:22 +02:00
4f6cabf898 chor: update zig version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m3s
2025-08-04 21:09:56 +02:00
853f4d9769 mod(event): documentation of unsupported creation of declarations during comptime
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 56s
The functionality remains in the code, but will cause an compilation
error when the user provided tagged union for the application events
has declarations. This way it is at least explicit that no declarations
are supported (even if this is actually a nice to have).

For details see this [issue](https://github.com/ziglang/zig/issues/6709).
2025-07-17 23:15:45 +02:00
edbca39c38 mod(event): merge the declarations of the tagged unions alongside the fields
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 55s
2025-07-17 22:27:02 +02:00
0de50e7016 feat(element/selection): Element implementation for selecting an enum variant
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m35s
2025-07-17 21:00:00 +02:00
088e1a9246 add(element/radio-button): RadioButton Element implementation
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 2m6s
This can be used to visualize the values of `bool`'s, which is relevant
when creating form's based on `struct`'s automatically.
2025-07-13 21:02:28 +02:00
31 changed files with 1317 additions and 765 deletions

View File

@@ -2,7 +2,7 @@
`zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications. `zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications.
> [!CAUTION] > [!caution]
> Only builds using the zig master version are tested to work. > Only builds using the zig master version are tested to work.
## Demo ## Demo
@@ -13,7 +13,7 @@ Clone this repository and run `zig build --help` to see the available examples.
zig build --release=safe -Dexample=demo run zig build --release=safe -Dexample=demo run
``` ```
> [!TIP] > [!tip]
> Every example application can be quit using `ctrl+c`. > Every example application can be quit using `ctrl+c`.
See the [wiki](https://gitea.yves-biener.de/yves-biener/zterm/wiki) for a showcase of the examples and the further details. See the [wiki](https://gitea.yves-biener.de/yves-biener/zterm/wiki) for a showcase of the examples and the further details.
@@ -33,8 +33,6 @@ const zterm: *Dependency = b.dependency("zterm", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
// ...
exe.root_module.addImport("zterm", zterm.module("zterm"));
``` ```
### Documentation ### Documentation

View File

@@ -10,9 +10,10 @@ pub fn build(b: *std.Build) void {
alignment, alignment,
button, button,
input, input,
popup,
progress, progress,
radio_button,
scrollable, scrollable,
selection,
// layouts: // layouts:
vertical, vertical,
horizontal, horizontal,
@@ -34,20 +35,15 @@ pub fn build(b: *std.Build) void {
options.addOption(bool, "debug", debug_rendering); options.addOption(bool, "debug", debug_rendering);
const options_module = options.createModule(); const options_module = options.createModule();
// dependencies
const zg = b.dependency("zg", .{
.target = target,
.optimize = optimize,
});
// library // library
const lib = b.addModule("zterm", .{ const lib = b.addModule("zterm", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.imports = &.{
.{ .name = "build_options", .module = options_module },
},
}); });
lib.addImport("code_point", zg.module("code_point"));
lib.addImport("build_options", options_module);
//--- Examples --- //--- Examples ---
const examples = std.meta.fields(Examples); const examples = std.meta.fields(Examples);
@@ -55,6 +51,7 @@ pub fn build(b: *std.Build) void {
if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry
const demo = b.addExecutable(.{ const demo = b.addExecutable(.{
.name = e.name, .name = e.name,
.root_module = b.createModule(.{
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) { .root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
.demo => "examples/demo.zig", .demo => "examples/demo.zig",
.continuous => "examples/continuous.zig", .continuous => "examples/continuous.zig",
@@ -62,9 +59,10 @@ pub fn build(b: *std.Build) void {
.alignment => "examples/elements/alignment.zig", .alignment => "examples/elements/alignment.zig",
.button => "examples/elements/button.zig", .button => "examples/elements/button.zig",
.input => "examples/elements/input.zig", .input => "examples/elements/input.zig",
.popup => "examples/elements/popup.zig",
.progress => "examples/elements/progress.zig", .progress => "examples/elements/progress.zig",
.radio_button => "examples/elements/radio-button.zig",
.scrollable => "examples/elements/scrollable.zig", .scrollable => "examples/elements/scrollable.zig",
.selection => "examples/elements/selection.zig",
// layouts: // layouts:
.vertical => "examples/layouts/vertical.zig", .vertical => "examples/layouts/vertical.zig",
.horizontal => "examples/layouts/horizontal.zig", .horizontal => "examples/layouts/horizontal.zig",
@@ -79,22 +77,26 @@ pub fn build(b: *std.Build) void {
}), }),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.imports = &.{
.{ .name = "zterm", .module = lib },
},
}),
}); });
// import dependencies
demo.root_module.addImport("zterm", lib);
// mapping of user selected example to compile step // mapping of user selected example to compile step
if (@intFromEnum(example) == e.value or example == .all) b.installArtifact(demo); if (@intFromEnum(example) == e.value or example == .all) b.installArtifact(demo);
} }
// zig build test // zig build test
const lib_unit_tests = b.addTest(.{ const lib_unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.imports = &.{
.{ .name = "build_options", .module = options_module },
},
}),
}); });
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
lib_unit_tests.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
lib_unit_tests.root_module.addImport("build_options", options_module);
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);

View File

@@ -35,12 +35,7 @@
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively. // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires // Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity. // internet connectivity.
.dependencies = .{ .dependencies = .{},
.zg = .{
.url = "git+https://codeberg.org/atman/zg#9427a9e53aaa29ee071f4dcb35b809a699d75aa9",
.hash = "zg-0.14.1-oGqU3IQ_tALZIiBN026_NTaPJqU-Upm8P_C7QED2Rzm8",
},
},
.paths = .{ .paths = .{
"LICENSE", "LICENSE",
"build.zig", "build.zig",

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -41,7 +41,7 @@ const Spinner = struct {
}; };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -57,18 +57,20 @@ const Spinner = struct {
}; };
const InputField = struct { const InputField = struct {
allocator: std.mem.Allocator,
input: std.ArrayList(u21), input: std.ArrayList(u21),
queue: *App.Queue, queue: *App.Queue,
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() { pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
return .{ return .{
.input = .init(allocator), .allocator = allocator,
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
.queue = queue, .queue = queue,
}; };
} }
pub fn deinit(this: @This()) void { pub fn deinit(this: *@This()) void {
this.input.deinit(); this.input.deinit(this.allocator);
} }
pub fn element(this: *@This()) App.Element { pub fn element(this: *@This()) App.Element {
@@ -81,14 +83,14 @@ const InputField = struct {
}; };
} }
fn handle(ctx: *anyopaque, event: App.Event) !void { fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.key => |key| { .key => |key| {
if (key.isAscii()) try this.input.append(key.cp); if (key.isAscii()) try this.input.append(this.allocator, key.cp);
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
this.queue.push(.{ .accept = try this.input.toOwnedSlice() }); this.queue.push(.{ .accept = try this.input.toOwnedSlice(this.allocator) });
if (key.eql(.{ .cp = zterm.input.Backspace })) if (key.eql(.{ .cp = zterm.input.Backspace }))
_ = this.input.pop(); _ = this.input.pop();
@@ -100,7 +102,7 @@ const InputField = struct {
} }
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -131,7 +133,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -176,7 +178,7 @@ pub fn main() !void {
if (now_ms >= next_frame_ms) { if (now_ms >= next_frame_ms) {
next_frame_ms = now_ms + tick_ms; next_frame_ms = now_ms + tick_ms;
} else { } else {
time.sleep((next_frame_ms - now_ms) * time.ns_per_ms); std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
next_frame_ms += tick_ms; next_frame_ms += tick_ms;
} }
@@ -212,7 +214,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -228,7 +230,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -240,6 +242,6 @@ const std = @import("std");
const time = std.time; const time = std.time;
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) { const App = zterm.App(struct {}, union(enum) {
accept: []u21, accept: []u21,
}); });

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -33,7 +33,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -168,7 +168,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -183,7 +183,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -195,4 +195,4 @@ const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const input = zterm.input; const input = zterm.input;
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -70,7 +70,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -85,7 +85,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -96,4 +96,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -40,7 +40,7 @@ const Clickable = struct {
}; };
} }
fn handle(ctx: *anyopaque, event: App.Event) !void { fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) { .mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
@@ -55,7 +55,7 @@ const Clickable = struct {
} }
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -82,7 +82,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -119,7 +119,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -134,7 +134,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -145,7 +145,10 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) { const App = zterm.App(
struct {},
union(enum) {
click: [:0]const u8, click: [:0]const u8,
accept, accept,
}); },
);

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -37,7 +37,7 @@ const MouseDraw = struct {
}; };
} }
fn handle(ctx: *anyopaque, event: App.Event) !void { fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y }, .mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
@@ -45,7 +45,7 @@ const MouseDraw = struct {
} }
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
@@ -65,7 +65,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -133,7 +133,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -148,7 +148,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -161,6 +161,9 @@ const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const Color = zterm.Color; const Color = zterm.Color;
const App = zterm.App(union(enum) { const App = zterm.App(
struct {},
union(enum) {
accept: []u8, accept: []u8,
}); },
);

View File

@@ -1,247 +0,0 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, 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;
}
}
};
const MouseDraw = struct {
position: ?zterm.Point = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
else => this.position = null,
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.position) |pos| {
const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x);
cells[idx].cp = 'x';
cells[idx].style.fg = .red;
}
}
};
const Popup = struct {
container: ?*App.Container = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.resize = resize,
.reposition = reposition,
.handle = handle,
.content = content,
},
};
}
fn resize(ctx: *anyopaque, size: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.resize(size);
}
fn reposition(ctx: *anyopaque, _: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.reposition(.{});
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
// TODO should the `Element` handle the pop_down element triggering (i.e. by defining a usual key for this?)
.pop_up => |optional| if (optional) |container| {
this.container = @ptrCast(@alignCast(container));
} else {
this.container = null;
},
else => if (this.container) |container| try container.handle(event),
}
}
fn render_container(container: App.Container, cells: []zterm.Cell, container_size: zterm.Point) !void {
const size = container.size;
const origin = container.origin;
const contents = try container.content();
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
var idx: usize = 0;
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
cells[anchor + (row * container_size.x) + col] = contents[idx];
idx += 1;
if (contents.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(contents);
for (container.elements.items) |child| try render_container(child, cells, size);
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.container) |container| {
assert(cells.len == @as(usize, container.size.x) * @as(usize, container.size.y));
const popup_cells = try container.content();
for (container.elements.items) |child| try render_container(child, popup_cells, size);
assert(cells.len == popup_cells.len);
@memcpy(cells, popup_cells);
container.allocator.free(popup_cells);
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var popup: Popup = .{};
var quit_text: QuitText = .{};
// TODO
// - rendering of first Container contents will be overwritten by contents of the appended Container's (see the missing blue rectangle)
// - when rendering "nothing" it causes the below Container to be overwritten to "nothing" too!
// - not sure how this should be done?
// - provide the pop-up with a area where to draw? (i.e. somewhere to draw the provided `Container`)
// - This however will not suffice as the contents will be overwritten!
var container = try App.Container.init(allocator, .{
.rectangle = .{
.fill = .blue,
},
}, .{});
defer container.deinit();
var popup_root_container = try App.Container.init(allocator, .{
.layout = .{
.padding = .{
.top = -17,
.left = -40,
.right = 5,
.bottom = 5,
},
},
}, .{});
try popup_root_container.append(try App.Container.init(allocator, .{}, popup.element()));
try container.append(try .init(allocator, .{}, quit_text.element()));
try container.append(popup_root_container); // FIXME it should not be appended (as it would become part of the layout)
var mouse: MouseDraw = .{};
var popup_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.layout = .{
.padding = .{
.top = -4,
.bottom = 1,
.left = 3,
.right = 3,
},
},
}, .{});
// showcase that inner `Container`s handle `Element`s accordingly
try popup_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, mouse.element()));
defer popup_container.deinit();
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = zterm.input.Space })) app.postEvent(.{ .pop_up = &popup_container });
if (key.eql(.{ .cp = zterm.input.Escape })) app.postEvent(.{ .pop_up = null });
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
pop_up: ?*anyopaque,
});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,25 +32,59 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
var progress_percent: u8 = 0; var progress_percent: u8 = 0;
var progress: App.Progress(.progress) = .init(&app.queue, .{
.percent = .{ .enabled = true },
.fg = .green,
.bg = .grey,
});
var quit_text: QuitText = .{}; var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{ var container = try App.Container.init(allocator, .{
.layout = .{ .padding = .all(5) }, .layout = .{ .padding = .all(5), .direction = .vertical },
}, quit_text.element()); }, quit_text.element());
defer container.deinit(); defer container.deinit();
{
var progress: App.Progress(.progress) = .init(&app.queue, .{
.percent = .{
.enabled = true,
.alignment = .left,
},
.fg = .blue,
.bg = .grey,
});
try container.append(try App.Container.init(allocator, .{}, progress.element())); 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(); try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
@@ -67,7 +101,7 @@ pub fn main() !void {
if (now_ms >= next_frame_ms) { if (now_ms >= next_frame_ms) {
next_frame_ms = now_ms + tick_ms; next_frame_ms = now_ms + tick_ms;
} else { } else {
time.sleep((next_frame_ms - now_ms) * time.ns_per_ms); std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
next_frame_ms += tick_ms; next_frame_ms += tick_ms;
} }
@@ -104,7 +138,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -120,7 +154,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -132,6 +166,9 @@ const std = @import("std");
const time = std.time; const time = std.time;
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) { const App = zterm.App(
struct {},
union(enum) {
progress: u8, progress: u8,
}); },
);

View File

@@ -0,0 +1,98 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
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.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var radiobutton: App.RadioButton = .init(false, .{
.label = "Test Radio Button",
.style = .squared,
});
var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button"));
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
.layout = .{ .padding = .all(5) },
}, quit_text.element());
defer container.deinit();
try container.append(try .init(allocator, .{}, radiobutton.element()));
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.accept => log.info("Clicked built-in button using the mouse", .{}),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
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,
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 assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {
accept,
});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -34,7 +34,7 @@ const HelloWorldText = packed struct {
}; };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -66,7 +66,7 @@ pub fn main() !void {
} }
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -148,10 +148,10 @@ pub fn main() !void {
defer container.deinit(); defer container.deinit();
// place empty container containing the element of the scrollable Container. // place empty container containing the element of the scrollable Container.
var scrollable_top: App.Scrollable = .init(top_box, .enabled(.grey)); var scrollable_top: App.Scrollable = .init(top_box, .enabled(.default, false));
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element())); try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white)); var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true));
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element())); try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start(); try app.start();
@@ -169,7 +169,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -184,7 +184,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -196,4 +196,4 @@ const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const input = zterm.input; const input = zterm.input;
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -0,0 +1,97 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
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.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var selection: App.Selection(enum {
one,
two,
three,
}) = .init(.{
.label = "Selection",
});
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
.layout = .{ .padding = .all(5) },
}, quit_text.element());
defer container.deinit();
try container.append(try .init(allocator, .{}, selection.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received 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 }),
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,
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 assert = std.debug.assert;
const zterm = @import("zterm");
const Error = zterm.Error;
const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -31,7 +31,7 @@ const InfoText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -57,7 +57,7 @@ const ErrorNotification = struct {
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } }; return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
} }
fn handle(ctx: *anyopaque, event: App.Event) !void { fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall, .key => |key| if (!key.isAscii()) return zterm.Error.TooSmall,
@@ -66,7 +66,7 @@ const ErrorNotification = struct {
} }
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -97,7 +97,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -130,7 +130,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -145,7 +145,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -156,4 +156,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -8,7 +8,7 @@ const QuitText = struct {
}; };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -86,7 +86,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -101,7 +101,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -112,4 +112,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -8,7 +8,7 @@ const QuitText = struct {
}; };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -78,7 +78,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -93,7 +93,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -104,4 +104,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -8,7 +8,7 @@ const QuitText = struct {
}; };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -94,7 +94,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -109,7 +109,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -120,4 +120,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -8,7 +8,7 @@ const QuitText = struct {
}; };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -77,7 +77,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -92,7 +92,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -103,4 +103,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -73,7 +73,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -88,7 +88,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -99,4 +99,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{ .ptr = this, .vtable = &.{ .content = content } };
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -24,14 +24,109 @@ const QuitText = struct {
} }
}; };
const TableText = struct {
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.content = content,
},
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
_ = size;
var idx: usize = 0;
{
const text = "Normal ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Bold ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Dim ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Italic ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Underl ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Blink ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Invert ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Hidden ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Strikethrough";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
}
};
const TextStyles = struct { const TextStyles = struct {
const text = "Example"; const text = "Example";
pub fn element(this: *@This()) App.Element { pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } }; return .{
.ptr = this,
.vtable = &.{
.minSize = minSize,
.content = content,
},
};
} }
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { fn minSize(ctx: *anyopaque, size: zterm.Point) zterm.Point {
_ = ctx;
_ = size;
return .{
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
@setEvalBranchQuota(10000); @setEvalBranchQuota(10000);
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -83,7 +178,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -94,20 +189,23 @@ pub fn main() !void {
var container = try App.Container.init(allocator, .{ var container = try App.Container.init(allocator, .{
.layout = .{ .layout = .{
.gap = 2, // .gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 }, .padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .vertical,
}, },
}, element); }, element);
defer container.deinit(); defer container.deinit();
var table_head: TableText = .{};
try container.append(try .init(allocator, .{
.size = .{
.dim = .{ .y = 1 },
.grow = .horizontal,
},
}, table_head.element()));
var box = try App.Container.init(allocator, .{ var box = try App.Container.init(allocator, .{
.layout = .{ .direction = .vertical }, .layout = .{ .direction = .vertical },
.size = .{
.dim = .{
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
},
},
}, text_styles.element()); }, text_styles.element());
defer box.deinit(); defer box.deinit();
@@ -117,10 +215,20 @@ pub fn main() !void {
try app.start(); try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) { // event loop
const event = app.nextEvent(); loop: while (true) {
log.debug("received event: {s}", .{@tagName(event)}); // batch events since last iteration
const len = blk: {
app.queue.poll();
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
// handle events
for (0..len) |_| {
const event = app.queue.pop();
log.debug("handling event: {s}", .{@tagName(event)});
// pre event handling // pre event handling
switch (event) { switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
@@ -128,7 +236,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -137,13 +245,14 @@ pub fn main() !void {
// post event handling // post event handling
switch (event) { switch (event) {
.quit => break, .quit => break :loop,
else => {}, else => {},
} }
}
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -154,4 +263,5 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const App = zterm.App(struct {}, union(enum) {});

View File

@@ -12,18 +12,19 @@
/// ```zig /// ```zig
/// const zterm = @import("zterm"); /// const zterm = @import("zterm");
/// const App = zterm.App( /// const App = zterm.App(
/// union(enum) {}, /// struct {}, // empty model
/// union(enum) {}, // no additional user event's
/// ); /// );
/// // later on create an `App` instance and start the event loop /// // later on create an `App` instance and start the event loop
/// var app: App = .init; /// var app: App = .init(.{}); // provide instance of the model that shall be used
/// try app.start(); /// try app.start();
/// defer app.stop() catch unreachable; /// defer app.stop() catch unreachable; // does not clean-up the resources used in the model
/// ``` /// ```
pub fn App(comptime E: type) type { pub fn App(comptime M: type, comptime E: type) type {
if (!isTaggedUnion(E)) { if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`");
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`."); if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`.");
}
return struct { return struct {
model: Model,
queue: Queue, queue: Queue,
thread: ?Thread = null, thread: ?Thread = null,
quit_event: Thread.ResetEvent, quit_event: Thread.ResetEvent,
@@ -33,17 +34,20 @@ pub fn App(comptime E: type) type {
// global variable for the registered handler for WINCH // global variable for the registered handler for WINCH
var handler_ctx: *anyopaque = undefined; var handler_ctx: *anyopaque = undefined;
/// registered WINCH handler to report resize events /// registered WINCH handler to report resize events
fn handleWinch(_: c_int) callconv(.C) void { fn handleWinch(_: std.os.linux.SIG) callconv(.c) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx)); const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE this does not have to be done if in-band resize events are supported // NOTE this does not have to be done if in-band resize events are supported
// -> the signal might not work correctly when hosting the application over ssh! // -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize); this.postEvent(.resize);
} }
pub const init: @This() = .{ pub fn init(model: Model) @This() {
return .{
.model = model,
.queue = .{}, .queue = .{},
.quit_event = .{}, .quit_event = .unset,
}; };
}
pub fn start(this: *@This()) !void { pub fn start(this: *@This()) !void {
if (this.thread) |_| return; if (this.thread) |_| return;
@@ -69,8 +73,8 @@ pub fn App(comptime E: type) type {
try terminal.enableRawMode(&termios); 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.enterAltScreen();
try terminal.saveScreen();
try terminal.hideCursor(); try terminal.hideCursor();
try terminal.enableMouseSupport(); try terminal.enableMouseSupport();
} }
@@ -78,8 +82,8 @@ pub fn App(comptime E: type) type {
pub fn interrupt(this: *@This()) !void { pub fn interrupt(this: *@This()) !void {
this.quit_event.set(); this.quit_event.set();
try terminal.disableMouseSupport(); try terminal.disableMouseSupport();
try terminal.exitAltScreen();
try terminal.restoreScreen(); try terminal.restoreScreen();
try terminal.exitAltScreen();
if (this.thread) |thread| { if (this.thread) |thread| {
thread.join(); thread.join();
this.thread = null; this.thread = null;
@@ -88,12 +92,12 @@ pub fn App(comptime E: type) type {
pub fn stop(this: *@This()) !void { pub fn stop(this: *@This()) !void {
try this.interrupt(); try this.interrupt();
if (this.termios) |*termios| { if (this.termios) |termios| {
try terminal.disableMouseSupport(); try terminal.disableMouseSupport();
try terminal.showCursor(); try terminal.showCursor();
try terminal.exitAltScreen();
try terminal.disableRawMode(termios);
try terminal.restoreScreen(); try terminal.restoreScreen();
try terminal.disableRawMode(&termios);
try terminal.exitAltScreen();
} }
this.termios = null; this.termios = null;
} }
@@ -376,8 +380,8 @@ pub fn App(comptime E: type) type {
}, },
0x7f => .{ .cp = input.Backspace }, 0x7f => .{ .cp = input.Backspace },
else => { else => {
var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] }; var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..read_bytes], .i = 0 };
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } }); while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } });
continue; continue;
}, },
}; };
@@ -391,9 +395,9 @@ pub fn App(comptime E: type) type {
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
terminal.disableMouseSupport() catch {}; terminal.disableMouseSupport() catch {};
terminal.exitAltScreen() catch {};
terminal.showCursor() catch {}; terminal.showCursor() catch {};
var termios: posix.termios = .{ terminal.restoreScreen() catch {};
terminal.disableRawMode(&.{
.iflag = .{}, .iflag = .{},
.lflag = .{}, .lflag = .{},
.cflag = .{}, .cflag = .{},
@@ -402,21 +406,23 @@ pub fn App(comptime E: type) type {
.line = 0, .line = 0,
.ispeed = undefined, .ispeed = undefined,
.ospeed = undefined, .ospeed = undefined,
}; }) catch {};
terminal.disableRawMode(&termios) catch {}; terminal.exitAltScreen() catch {};
terminal.restoreScreen() catch {};
std.debug.defaultPanic(msg, ret_addr); std.debug.defaultPanic(msg, ret_addr);
} }
const element = @import("element.zig"); const element = @import("element.zig");
pub const Model = M;
pub const Event = mergeTaggedUnions(event.SystemEvent, E); pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Container = @import("container.zig").Container(Event); pub const Container = @import("container.zig").Container(Model, Event);
pub const Element = element.Element(Event); pub const Element = element.Element(Model, Event);
pub const Alignment = element.Alignment(Event); pub const Alignment = element.Alignment(Model, Event);
pub const Button = element.Button(Event, Queue); pub const Button = element.Button(Model, Event, Queue);
pub const Input = element.Input(Event, Queue); pub const Input = element.Input(Model, Event, Queue);
pub const Progress = element.Progress(Event, Queue); pub const Progress = element.Progress(Model, Event, Queue);
pub const Scrollable = element.Scrollable(Event); pub const RadioButton = element.RadioButton(Model, Event);
pub const Scrollable = element.Scrollable(Model, Event);
pub const Selection = element.Selection(Model, Event);
pub const Queue = queue.Queue(Event, 256); pub const Queue = queue.Queue(Event, 256);
}; };
} }
@@ -429,13 +435,13 @@ const fmt = std.fmt;
const posix = std.posix; const posix = std.posix;
const Thread = std.Thread; const Thread = std.Thread;
const assert = std.debug.assert; const assert = std.debug.assert;
const code_point = @import("code_point");
const event = @import("event.zig"); const event = @import("event.zig");
const input = @import("input.zig"); const input = @import("input.zig");
const terminal = @import("terminal.zig"); const terminal = @import("terminal.zig");
const queue = @import("queue.zig"); const queue = @import("queue.zig");
const mergeTaggedUnions = event.mergeTaggedUnions; const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion; const isTaggedUnion = event.isTaggedUnion;
const isStruct = event.isStruct;
const Mouse = input.Mouse; const Mouse = input.Mouse;
const Key = input.Key; const Key = input.Key;
const Point = @import("point.zig").Point; const Point = @import("point.zig").Point;

View File

@@ -13,7 +13,7 @@ pub fn reset(this: *Cell) void {
this.cp = ' '; this.cp = ' ';
} }
pub fn value(this: Cell, writer: anytype) !void { pub fn value(this: Cell, writer: *std.Io.Writer) !void {
try this.style.value(writer, this.cp); try this.style.value(writer, this.cp);
} }
@@ -29,17 +29,15 @@ test "ascii styled text" {
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } }, .{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
}; };
var string = std.ArrayList(u8).init(std.testing.allocator); var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer string.deinit(); defer writer.deinit();
const writer = string.writer();
for (cells) |cell| { for (cells) |cell| {
try cell.value(writer); try cell.value(&writer.writer);
} }
try std.testing.expectEqualSlices( try std.testing.expectEqualSlices(
u8, u8,
"\x1b[38;5;10;48;5;8;59mY\x1b[0m\x1b[39;49;59;1;4mv\x1b[0m\x1b[39;49;59;3me\x1b[0m\x1b[38;5;2;48;5;16;59;4ms\x1b[0m", "\x1b[38;5;10;48;5;8;59mY\x1b[0m\x1b[39;49;59;1;4mv\x1b[0m\x1b[39;49;59;3me\x1b[0m\x1b[38;5;2;48;5;16;59;4ms\x1b[0m",
string.items, writer.writer.buffer[0..writer.writer.end],
); );
} }
@@ -51,16 +49,14 @@ test "utf-8 styled text" {
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } }, .{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
}; };
var string = std.ArrayList(u8).init(std.testing.allocator); var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer string.deinit(); defer writer.deinit();
const writer = string.writer();
for (cells) |cell| { for (cells) |cell| {
try cell.value(writer); try cell.value(&writer.writer);
} }
try std.testing.expectEqualSlices( try std.testing.expectEqualSlices(
u8, u8,
"\x1b[38;5;10;48;5;8;59m╭\x1b[0m\x1b[39;49;59m─\x1b[0m\x1b[39;49;59m┄\x1b[0m\x1b[38;5;2;48;5;16;59;4m┘\x1b[0m", "\x1b[38;5;10;48;5;8;59m╭\x1b[0m\x1b[39;49;59m─\x1b[0m\x1b[39;49;59m┄\x1b[0m\x1b[38;5;2;48;5;16;59;4m┘\x1b[0m",
string.items, writer.writer.buffer[0..writer.writer.end],
); );
} }

View File

@@ -20,22 +20,21 @@ pub const Color = enum(u8) {
// TODO might be useful to use the std.ascii stuff! // TODO might be useful to use the std.ascii stuff!
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void { pub inline fn write(this: Color, writer: *std.Io.Writer, comptime coloring: enum { fg, bg, ul }) !void {
if (this == .default) { if (this == .default) {
switch (coloring) { switch (coloring) {
.fg => try format(writer, "39", .{}), .fg => try writer.printAscii("39", .{}),
.bg => try format(writer, "49", .{}), .bg => try writer.printAscii("49", .{}),
.ul => try format(writer, "59", .{}), .ul => try writer.printAscii("59", .{}),
} }
} else { } else {
switch (coloring) { switch (coloring) {
.fg => try format(writer, "38;5;{d}", .{@intFromEnum(this)}), .fg => try writer.print("38;5;{d}", .{@intFromEnum(this)}),
.bg => try format(writer, "48;5;{d}", .{@intFromEnum(this)}), .bg => try writer.print("48;5;{d}", .{@intFromEnum(this)}),
.ul => try format(writer, "58;5;{d}", .{@intFromEnum(this)}), .ul => try writer.print("58;5;{d}", .{@intFromEnum(this)}),
} }
} }
} }
}; };
const std = @import("std"); const std = @import("std");
const format = std.fmt.format;

View File

@@ -1,3 +1,6 @@
// FIX known issues:
// - hold fewer instances of the `Allocator`
/// Border configuration struct /// Border configuration struct
pub const Border = packed struct { pub const Border = packed struct {
/// Color to use for the border /// Color to use for the border
@@ -77,8 +80,9 @@ pub const Border = packed struct {
test "all sides" { test "all sides" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{ .border = .{
.color = .green, .color = .green,
.sides = .all, .sides = .all,
@@ -89,14 +93,15 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/border.all.zon")); }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
} }
test "vertical sides" { test "vertical sides" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{ .border = .{
.color = .green, .color = .green,
.sides = .vertical, .sides = .vertical,
@@ -107,14 +112,15 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/border.vertical.zon")); }, @TypeOf(container), &container, Model, @import("test/container/border.vertical.zon"));
} }
test "horizontal sides" { test "horizontal sides" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{ .border = .{
.color = .green, .color = .green,
.sides = .horizontal, .sides = .horizontal,
@@ -125,7 +131,7 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/border.horizontal.zon")); }, @TypeOf(container), &container, Model, @import("test/container/border.horizontal.zon"));
} }
}; };
@@ -157,8 +163,9 @@ pub const Rectangle = packed struct {
test "fill color overwrite parent fill" { test "fill color overwrite parent fill" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{ .fill = .green }, .rectangle = .{ .fill = .green },
}, .{}); }, .{});
try container.append(try .init(std.testing.allocator, .{ try container.append(try .init(std.testing.allocator, .{
@@ -170,14 +177,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/rectangle_with_parent_fill_without_padding.zon")); }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_fill_without_padding.zon"));
} }
test "fill color padding to show parent fill" { test "fill color padding to show parent fill" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{ .layout = .{
.padding = .all(2), .padding = .all(2),
}, },
@@ -192,14 +200,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/rectangle_with_parent_padding.zon")); }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon"));
} }
test "fill color padding to show parent fill (negative padding)" { test "fill color padding to show parent fill (negative padding)" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{ .layout = .{
.padding = .{ .padding = .{
.top = -18, .top = -18,
@@ -219,14 +228,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/rectangle_with_parent_padding.zon")); }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon"));
} }
test "fill color spacer with padding" { test "fill color spacer with padding" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{ .rectangle = .{
.fill = .black, .fill = .black,
}, },
@@ -247,14 +257,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/rectangle_with_padding.zon")); }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_padding.zon"));
} }
test "fill color with gap" { test "fill color with gap" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{ .rectangle = .{
.fill = .black, .fill = .black,
}, },
@@ -278,14 +289,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/rectangle_with_gap.zon")); }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_gap.zon"));
} }
test "fill color with separator" { test "fill color with separator" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{ .rectangle = .{
.fill = .black, .fill = .black,
}, },
@@ -310,7 +322,7 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/rectangle_with_separator.zon")); }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_separator.zon"));
} }
}; };
@@ -400,8 +412,9 @@ pub const Layout = packed struct {
test "separator without gaps" { test "separator without gaps" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{ .layout = .{
.separator = .{ .separator = .{
.enabled = true, .enabled = true,
@@ -415,14 +428,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/separator_no_gaps.zon")); }, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps.zon"));
} }
test "separator without gaps with padding" { test "separator without gaps with padding" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{ .layout = .{
.padding = .all(1), .padding = .all(1),
.separator = .{ .separator = .{
@@ -437,14 +451,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/separator_no_gaps_with_padding.zon")); }, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps_with_padding.zon"));
} }
test "separator(2x) without gaps" { test "separator(2x) without gaps" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{ .layout = .{
.direction = .vertical, .direction = .vertical,
.separator = .{ .separator = .{
@@ -461,14 +476,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/separator_2x_no_gaps.zon")); }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps.zon"));
} }
test "separator(2x) with border(all)" { test "separator(2x) with border(all)" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -488,14 +504,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/separator_2x_no_gaps_with_border.zon")); }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_border.zon"));
} }
test "separator(2x) with border(all) and padding(all(1))" { test "separator(2x) with border(all) and padding(all(1))" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -516,14 +533,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/separator_2x_no_gaps_with_padding.zon")); }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
} }
test "separator(2x) with border(all) and gap" { test "separator(2x) with border(all) and gap" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -544,14 +562,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/separator_2x_with_gaps_with_border.zon")); }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border.zon"));
} }
test "separator(2x) with border(all) and gap and padding" { test "separator(2x) with border(all) and gap and padding" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -573,7 +592,7 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon")); }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
} }
}; };
@@ -588,10 +607,10 @@ pub const Size = packed struct {
} = .both, } = .both,
}; };
pub fn Container(comptime Event: type) type { pub fn Container(Model: type, Event: type) type {
if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`"); if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`");
const Element = @import("element.zig").Element(Event); const Element = @import("element.zig").Element(Model, Event);
return struct { return struct {
allocator: Allocator, allocator: Allocator,
origin: Point, origin: Point,
@@ -622,17 +641,17 @@ pub fn Container(comptime Event: type) type {
.size = .{}, .size = .{},
.properties = properties, .properties = properties,
.element = element, .element = element,
.elements = std.ArrayList(@This()).init(allocator), .elements = try std.ArrayList(@This()).initCapacity(allocator, 2),
}; };
} }
pub fn deinit(this: *const @This()) void { pub fn deinit(this: *@This()) void {
for (this.elements.items) |*element| element.deinit(); for (this.elements.items) |*element| element.deinit();
this.elements.deinit(); this.elements.deinit(this.allocator);
} }
pub fn append(this: *@This(), element: @This()) !void { pub fn append(this: *@This(), element: @This()) !void {
try this.elements.append(element); try this.elements.append(this.allocator, element);
} }
pub fn reposition(this: *@This(), origin: Point) void { pub fn reposition(this: *@This(), origin: Point) void {
@@ -886,7 +905,7 @@ pub fn Container(comptime Event: type) type {
this.grow_resize(this.size); this.grow_resize(this.size);
} }
pub fn handle(this: *const @This(), event: Event) !void { pub fn handle(this: *const @This(), model: *Model, event: Event) !void {
switch (event) { switch (event) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) { .mouse => |mouse| if (mouse.in(this.origin, this.size)) {
// the element receives the mouse event with relative position // the element receives the mouse event with relative position
@@ -894,17 +913,14 @@ pub fn Container(comptime Event: type) type {
var relative_mouse: input.Mouse = mouse; var relative_mouse: input.Mouse = mouse;
relative_mouse.x -= this.origin.x; relative_mouse.x -= this.origin.x;
relative_mouse.y -= this.origin.y; relative_mouse.y -= this.origin.y;
try this.element.handle(.{ .mouse = relative_mouse }); try this.element.handle(model, .{ .mouse = relative_mouse });
for (this.elements.items) |*element| try element.handle(event);
},
else => {
try this.element.handle(event);
for (this.elements.items) |*element| try element.handle(event);
}, },
else => try this.element.handle(model, event),
} }
for (this.elements.items) |*element| try element.handle(model, event);
} }
pub fn content(this: *const @This()) ![]Cell { pub fn content(this: *const @This(), model: *const Model) ![]Cell {
if (this.size.x == 0 or this.size.y == 0) return Error.TooSmall; if (this.size.x == 0 or this.size.y == 0) return Error.TooSmall;
const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y)); const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y));
@@ -915,7 +931,7 @@ pub fn Container(comptime Event: type) type {
this.properties.border.content(cells, this.size); this.properties.border.content(cells, this.size);
this.properties.rectangle.content(cells, this.size); this.properties.rectangle.content(cells, this.size);
try this.element.content(cells, this.size); try this.element.content(model, cells, this.size);
// DEBUG render corresponding corners (except top left) of this `Container` *red* // DEBUG render corresponding corners (except top left) of this `Container` *red*
if (comptime build_options.debug) { if (comptime build_options.debug) {
@@ -959,8 +975,9 @@ test {
test "Container Fixed and Grow Size Vertical" { test "Container Fixed and Grow Size Vertical" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{ .direction = .vertical }, .layout = .{ .direction = .vertical },
}, .{}); }, .{});
try container.append(try .init(std.testing.allocator, .{ try container.append(try .init(std.testing.allocator, .{
@@ -978,14 +995,15 @@ test "Container Fixed and Grow Size Vertical" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/fixed_grow_vertical.zon")); }, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_vertical.zon"));
} }
test "Container Fixed and Grow Size Horizontal" { test "Container Fixed and Grow Size Horizontal" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Model = struct {};
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{}, .{}); var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{}, .{});
try container.append(try .init(std.testing.allocator, .{ try container.append(try .init(std.testing.allocator, .{
.size = .{ .size = .{
.dim = .{ .x = 5 }, .dim = .{ .x = 5 },
@@ -1001,5 +1019,5 @@ test "Container Fixed and Grow Size Horizontal" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/fixed_grow_horizontal.zon")); }, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_horizontal.zon"));
} }

File diff suppressed because it is too large Load Diff

View File

@@ -21,15 +21,19 @@ pub const SystemEvent = union(enum) {
key: Key, key: Key,
/// Mouse input event /// Mouse input event
mouse: Mouse, mouse: Mouse,
/// Focus event for mouse interaction /// Focus event indicating that the application has gained the focus of the user
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
focus: bool, focus: bool,
}; };
/// Merge the two provided `union(enum)` `A` and `B` to a tagged union containing all fields of both tagged unions in `comptime`.
/// Declarations are not supported for `comptime` created types, see https://github.com/ziglang/zig/issues/6709 for details.
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
if (!isTaggedUnion(A) or !isTaggedUnion(B)) { // TODO maybe it makes sense to have a nested tagged union type (i.e. system: union(enum) and event: union(enum))
@compileError("Both types for merging tagged unions need to be of type `union(enum)`."); // - allows re-definition of system / built-in events
} // - clearly shows which events are system / built-in ones and which are user defined events
// - the memory footprint for the nesting is not really harmful
if (!isTaggedUnion(A) or !isTaggedUnion(B)) @compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
const a_fields = @typeInfo(A).@"union".fields; const a_fields = @typeInfo(A).@"union".fields;
const a_fields_tag = @typeInfo(A).@"union".tag_type.?; const a_fields_tag = @typeInfo(A).@"union".tag_type.?;
const a_enum_fields = @typeInfo(a_fields_tag).@"enum".fields; const a_enum_fields = @typeInfo(a_fields_tag).@"enum".fields;
@@ -54,11 +58,27 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
i += 1; i += 1;
} }
const log2_i = @bitSizeOf(@TypeOf(i)) - @clz(i); // NOTE declarations are not supported for `comptime` types: https://github.com/ziglang/zig/issues/6709
// -> will lead to a compilation error when constructing the tagged union
// at the end of this function in case at least one of the provided tagged
// unions to merge contains declarations (which in this case can only be the
// user provided one)
const a_enum_decls = @typeInfo(A).@"union".decls;
const b_enum_decls = @typeInfo(B).@"union".decls;
var decls: [a_enum_decls.len + b_enum_decls.len]std.builtin.Type.Declaration = undefined;
var j: usize = 0;
for (a_enum_decls) |decl| {
decls[j] = decl;
j += 1;
}
for (b_enum_decls) |decl| {
decls[j] = decl;
j += 1;
}
const EventType = @Type(.{ .int = .{ const EventType = @Type(.{ .int = .{
.signedness = .unsigned, .signedness = .unsigned,
.bits = log2_i, .bits = @bitSizeOf(@TypeOf(i)) - @clz(i),
} }); } });
const Event = @Type(.{ .@"enum" = .{ const Event = @Type(.{ .@"enum" = .{
@@ -76,19 +96,25 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
} }); } });
} }
// Determine at `comptime` whether the provided type `E` is an `union(enum)`. /// Determine whether the provided type `T` is a tagged union: `union(enum)`.
pub fn isTaggedUnion(comptime E: type) bool { pub fn isTaggedUnion(comptime T: type) bool {
switch (@typeInfo(E)) { switch (@typeInfo(T)) {
.@"union" => |u| { .@"union" => |u| if (u.tag_type) |_| {} else {
if (u.tag_type) |_| {} else {
return false; return false;
}
}, },
else => return false, else => return false,
} }
return true; return true;
} }
/// Determine whether the provided type `T` is a `struct`.
pub fn isStruct(comptime T: type) bool {
return switch (@typeInfo(T)) {
.@"struct" => |_| true,
else => false,
};
}
const std = @import("std"); const std = @import("std");
const input = @import("input.zig"); const input = @import("input.zig");
const terminal = @import("terminal.zig"); const terminal = @import("terminal.zig");

View File

@@ -215,7 +215,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
// still full and the push in the other thread is still blocked // still full and the push in the other thread is still blocked
// waiting for space. // waiting for space.
try Thread.yield(); try Thread.yield();
std.time.sleep(std.time.ns_per_s); std.Thread.sleep(std.time.ns_per_s);
// Finally, let that other thread go. // Finally, let that other thread go.
try testing.expectEqual(1, q.pop()); try testing.expectEqual(1, q.pop());
@@ -225,7 +225,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
try Thread.yield(); try Thread.yield();
// But we want to ensure that there's a second push waiting, so // But we want to ensure that there's a second push waiting, so
// here's another sleep. // here's another sleep.
std.time.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Another spurious wake... // Another spurious wake...
q.not_full.signal(); q.not_full.signal();
@@ -233,7 +233,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
// And another chance for the other thread to see that it's // And another chance for the other thread to see that it's
// spurious and go back to sleep. // spurious and go back to sleep.
try Thread.yield(); try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Pop that thing and we're done. // Pop that thing and we're done.
try testing.expectEqual(2, q.pop()); try testing.expectEqual(2, q.pop());
@@ -270,14 +270,14 @@ test "Fill, block, fill, block" {
fn sleepyPush(q: *Queue(u8, 1)) !void { fn sleepyPush(q: *Queue(u8, 1)) !void {
// Try to ensure the other thread has already started trying to pop. // Try to ensure the other thread has already started trying to pop.
try Thread.yield(); try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Spurious wake // Spurious wake
q.not_full.signal(); q.not_full.signal();
q.not_empty.signal(); q.not_empty.signal();
try Thread.yield(); try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Stick something in the queue so it can be popped. // Stick something in the queue so it can be popped.
q.push(1); q.push(1);
@@ -286,7 +286,7 @@ fn sleepyPush(q: *Queue(u8, 1)) !void {
try Thread.yield(); try Thread.yield();
// Give the other thread time to block again. // Give the other thread time to block again.
try Thread.yield(); try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Spurious wake // Spurious wake
q.not_full.signal(); q.not_full.signal();
@@ -317,7 +317,7 @@ test "2 readers" {
const t1 = try Thread.spawn(cfg, readerThread, .{&queue}); const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
const t2 = try Thread.spawn(cfg, readerThread, .{&queue}); const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
try Thread.yield(); try Thread.yield();
std.time.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
queue.push(1); queue.push(1);
queue.push(1); queue.push(1);
t1.join(); t1.join();

View File

@@ -58,10 +58,10 @@ pub const Buffered = struct {
} }
/// Render provided cells at size (anchor and dimension) into the *virtual screen*. /// 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 Container: type, container: *Container, comptime Model: type, model: *const Model) !void {
const size: Point = container.size; const size: Point = container.size;
const origin: Point = container.origin; const origin: Point = container.origin;
const cells: []const Cell = try container.content(); const cells: []const Cell = try container.content(model);
if (cells.len == 0) return; if (cells.len == 0) return;
@@ -80,7 +80,7 @@ pub const Buffered = struct {
// free immediately // free immediately
container.allocator.free(cells); container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(T, element); for (container.elements.items) |*element| try this.render(Container, element, Model, model);
} }
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop). /// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
@@ -88,7 +88,7 @@ pub const Buffered = struct {
try terminal.hideCursor(); try terminal.hideCursor();
// TODO measure timings of rendered frames? // TODO measure timings of rendered frames?
var cursor_position: ?Point = null; var cursor_position: ?Point = null;
const writer = terminal.writer(); var writer = terminal.writer();
const s = this.screen; const s = this.screen;
const vs = this.virtual_screen; const vs = this.virtual_screen;
for (0..this.size.y) |row| { for (0..this.size.y) |row| {
@@ -110,7 +110,7 @@ pub const Buffered = struct {
// render differences found in virtual screen // render differences found in virtual screen
try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) }); try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) });
try cvs.value(writer); try cvs.value(&writer);
// update screen to be the virtual screen for the next frame // update screen to be the virtual screen for the next frame
s[idx] = vs[idx]; s[idx] = vs[idx];
} }

View File

@@ -42,27 +42,27 @@ pub fn eql(this: Style, other: Style) bool {
// TODO might be useful to use the std.ascii stuff! // TODO might be useful to use the std.ascii stuff!
pub fn value(this: Style, writer: anytype, cp: u21) !void { pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void {
var buffer: [4]u8 = undefined; var buffer: [4]u8 = undefined;
const bytes = try unicode.utf8Encode(cp, &buffer); const bytes = try unicode.utf8Encode(cp, &buffer);
assert(bytes > 0); assert(bytes > 0);
// build ansi sequence for 256 colors ... // build ansi sequence for 256 colors ...
// foreground // foreground
try format(writer, "\x1b[", .{}); try writer.printAscii("\x1b[", .{});
try this.fg.write(writer, .fg); try this.fg.write(writer, .fg);
// background // background
try format(writer, ";", .{}); try writer.printAsciiChar(';', .{});
try this.bg.write(writer, .bg); try this.bg.write(writer, .bg);
// underline // underline
// FIX assert that if the underline property is set that the ul style and the attribute for underlining is available // FIX assert that if the underline property is set that the ul style and the attribute for underlining is available
try format(writer, ";", .{}); try writer.printAsciiChar(';', .{});
try this.ul.write(writer, .ul); try this.ul.write(writer, .ul);
// append styles (aka attributes like bold, italic, strikethrough, etc.) // append styles (aka attributes like bold, italic, strikethrough, etc.)
for (this.emphasis) |attribute| try format(writer, ";{d}", .{@intFromEnum(attribute)}); for (this.emphasis) |attribute| try writer.print(";{d}", .{@intFromEnum(attribute)});
try format(writer, "m", .{}); try writer.printAsciiChar('m', .{});
// content // content
try format(writer, "{s}", .{buffer[0..bytes]}); try writer.printAscii(buffer[0..bytes], .{});
try format(writer, "\x1b[0m", .{}); try writer.printAscii("\x1b[0m", .{});
} }
// TODO implement helper functions for terminal capabilities: // TODO implement helper functions for terminal capabilities:
@@ -73,6 +73,5 @@ const std = @import("std");
const unicode = std.unicode; const unicode = std.unicode;
const meta = std.meta; const meta = std.meta;
const assert = std.debug.assert; const assert = std.debug.assert;
const format = std.fmt.format;
const Color = @import("color.zig").Color; const Color = @import("color.zig").Color;
const Style = @This(); const Style = @This();

View File

@@ -62,19 +62,25 @@ pub fn write(buf: []const u8) !usize {
return try posix.write(posix.STDIN_FILENO, buf); return try posix.write(posix.STDIN_FILENO, buf);
} }
fn contextWrite(context: @This(), data: []const u8) anyerror!usize { fn drainFn(w: *std.Io.Writer, data: []const []const u8, splat: usize) error{WriteFailed}!usize {
_ = context; _ = w;
return try posix.write(posix.STDOUT_FILENO, data); if (data.len == 0 or splat == 0) return 0;
var len: usize = 0;
for (data) |bytes| len += posix.write(posix.STDOUT_FILENO, bytes) catch return error.WriteFailed;
return len;
} }
const Writer = std.io.Writer( // TODO I now need to add that much, for just the one function above?
@This(), pub fn writer() std.Io.Writer {
anyerror, return .{
contextWrite, .vtable = &.{
); .drain = drainFn,
.flush = std.Io.Writer.noopFlush,
pub fn writer() Writer { },
return .{ .context = .{} }; .buffer = &.{},
};
} }
pub fn setCursorPosition(pos: Point) !void { pub fn setCursorPosition(pos: Point) !void {
@@ -191,7 +197,7 @@ pub fn enableRawMode(bak: *posix.termios) !void {
} }
/// Reverts `enableRawMode` to restore initial functionality. /// Reverts `enableRawMode` to restore initial functionality.
pub fn disableRawMode(bak: *posix.termios) !void { pub fn disableRawMode(bak: *const posix.termios) !void {
try posix.tcsetattr( try posix.tcsetattr(
posix.STDIN_FILENO, posix.STDIN_FILENO,
.FLUSH, .FLUSH,
@@ -232,7 +238,7 @@ const log = std.log.scoped(.terminal);
const std = @import("std"); const std = @import("std");
const mem = std.mem; const mem = std.mem;
const posix = std.posix; const posix = std.posix;
const code_point = @import("code_point"); const assert = std.debug.assert;
const ctlseqs = @import("ctlseqs.zig"); const ctlseqs = @import("ctlseqs.zig");
const input = @import("input.zig"); const input = @import("input.zig");
const Key = input.Key; const Key = input.Key;

View File

@@ -34,10 +34,10 @@ pub const Renderer = struct {
@memset(this.screen, .{}); @memset(this.screen, .{});
} }
pub fn render(this: *@This(), comptime T: type, container: *const T) !void { pub fn render(this: *@This(), comptime T: type, container: *const T, comptime Model: type, model: *const Model) !void {
const size: Point = container.size; const size: Point = container.size;
const origin: Point = container.origin; const origin: Point = container.origin;
const cells: []const Cell = try container.content(); const cells: []const Cell = try container.content(model);
if (cells.len == 0) return; if (cells.len == 0) return;
@@ -58,7 +58,7 @@ pub const Renderer = struct {
// free immediately // free immediately
container.allocator.free(cells); container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(T, element); for (container.elements.items) |*element| try this.render(T, element, Model, model);
} }
pub fn save(this: @This(), writer: anytype) !void { pub fn save(this: @This(), writer: anytype) !void {
@@ -74,6 +74,8 @@ pub const Renderer = struct {
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method: /// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
/// ///
/// ```zig /// ```zig
/// const Model = struct {};
/// var model: Model = .{};
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true }); /// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
/// defer file.close(); /// defer file.close();
/// ///
@@ -81,8 +83,8 @@ pub const Renderer = struct {
/// var renderer: testing.Renderer = .init(allocator, size); /// var renderer: testing.Renderer = .init(allocator, size);
/// defer renderer.deinit(); /// defer renderer.deinit();
/// ///
/// try container.handle(.{ .size = size }); /// try container.handle(&model, .{ .size = size });
/// try renderer.render(Container(event.SystemEvent), &container); /// try renderer.render(@TypeOf(container), &container, Model, &.{});
/// try renderer.save(file.writer()); /// try renderer.save(file.writer());
/// ``` /// ```
/// ///
@@ -91,7 +93,8 @@ pub const Renderer = struct {
/// Then later load that .zon file at compile time and run your test against this `Cell` slice. /// Then later load that .zon file at compile time and run your test against this `Cell` slice.
/// ///
/// ```zig /// ```zig
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ /// const Model = struct {};
/// var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
/// .border = .{ /// .border = .{
/// .color = .green, /// .color = .green,
/// .sides = .all, /// .sides = .all,
@@ -102,20 +105,55 @@ pub const Renderer = struct {
/// try testing.expectContainerScreen(.{ /// try testing.expectContainerScreen(.{
/// .rows = 20, /// .rows = 20,
/// .cols = 30, /// .cols = 30,
/// }, &container, @import("test/container/border.all.zon")); /// }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
/// ``` /// ```
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void { pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, comptime Model: type, expected: []const Cell) !void {
const allocator = testing.allocator; const allocator = testing.allocator;
var renderer: Renderer = .init(allocator, size); var renderer: Renderer = .init(allocator, size);
defer renderer.deinit(); defer renderer.deinit();
container.resize(size); container.resize(size);
container.reposition(.{}); container.reposition(.{});
try renderer.render(Container(event.SystemEvent), container); try renderer.render(T, container, Model, &.{});
try expectEqualCells(.{}, renderer.size, expected, renderer.screen); try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
} }
/// Taken from: https://codeberg.org/atman/zg/src/branch/master/src/DisplayWidth.zig
/// Owned by https://codeberg.org/atman licensed under MIT all credits for this function go to him
fn center(allocator: Allocator, str: []const u8, total_width: usize, pad: []const u8) ![]u8 {
if (str.len > total_width) return error.StrTooLong;
if (str.len == total_width) return try allocator.dupe(u8, str);
if (pad.len > total_width or str.len + pad.len > total_width) return error.PadTooLong;
const margin_width = @divFloor((total_width - str.len), 2);
if (pad.len > margin_width) return error.PadTooLong;
const extra_pad: usize = if (total_width % 2 != str.len % 2) 1 else 0;
const pads = @divFloor(margin_width, pad.len) * 2 + extra_pad;
var result = try allocator.alloc(u8, pads * pad.len + str.len);
var bytes_index: usize = 0;
var pads_index: usize = 0;
while (pads_index < pads / 2) : (pads_index += 1) {
@memcpy(result[bytes_index..][0..pad.len], pad);
bytes_index += pad.len;
}
@memcpy(result[bytes_index..][0..str.len], str);
bytes_index += str.len;
pads_index = 0;
while (pads_index < pads / 2 + extra_pad) : (pads_index += 1) {
@memcpy(result[bytes_index..][0..pad.len], pad);
bytes_index += pad.len;
}
return result;
}
/// This function is intended to be used only in tests. Test if the two /// This function is intended to be used only in tests. Test if the two
/// provided cell arrays are identical. Usually the `Cell` slices are /// provided cell arrays are identical. Usually the `Cell` slices are
/// the contents of a given screen from the `zterm.testing.Renderer`. See /// the contents of a given screen from the `zterm.testing.Renderer`. See
@@ -127,28 +165,21 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x)); try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x); var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer expected_cps.deinit(); defer expected_cps.deinit(allocator);
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x); var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer actual_cps.deinit(); defer actual_cps.deinit(allocator);
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y); var allocating_writer = std.Io.Writer.Allocating.init(allocator);
defer output.deinit(); defer allocating_writer.deinit();
var buffer = std.io.bufferedWriter(output.writer()); var writer = &allocating_writer.writer;
defer buffer.flush() catch {};
const writer = buffer.writer();
var differ = false; var differ = false;
const dwd = try DisplayWidth.DisplayWidthData.init(allocator); const expected_centered = try center(allocator, "Expected Screen", size.x, " ");
defer dwd.deinit();
const dw: DisplayWidth = .{ .data = &dwd };
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
defer allocator.free(expected_centered); defer allocator.free(expected_centered);
const actual_centered = try dw.center(allocator, "Actual Screen", size.x, " "); const actual_centered = try center(allocator, "Actual Screen", size.x, " ");
defer allocator.free(actual_centered); defer allocator.free(actual_centered);
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered }); try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
@@ -164,8 +195,8 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
if (!expected_cell.eql(actual_cell)) differ = true; if (!expected_cell.eql(actual_cell)) differ = true;
try expected_cps.append(expected_cell); try expected_cps.append(allocator, expected_cell);
try actual_cps.append(actual_cell); try actual_cps.append(allocator, actual_cell);
} }
// write screens both formatted to buffer // write screens both formatted to buffer
@@ -178,13 +209,14 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
if (!differ) return; if (!differ) return;
// test failed // test failed
try buffer.flush();
debug.lockStdErr(); debug.lockStdErr();
defer debug.unlockStdErr(); defer debug.unlockStdErr();
const std_writer = std.io.getStdErr().writer(); var stdout_buffer: [1024]u8 = undefined;
try std_writer.writeAll(output.items); var stdout = std.fs.File.stdout().writer(&stdout_buffer);
const stdout_writer = &stdout.interface;
try stdout_writer.writeAll(writer.buffer[0..writer.end]);
try stdout_writer.flush();
return error.TestExpectEqualCells; return error.TestExpectEqualCells;
} }
@@ -195,5 +227,4 @@ const Allocator = std.mem.Allocator;
const event = @import("event.zig"); const event = @import("event.zig");
const Container = @import("container.zig").Container; const Container = @import("container.zig").Container;
const Cell = @import("cell.zig"); const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point; const Point = @import("point.zig").Point;