1 Commits

Author SHA1 Message Date
1ad7bf0d48 WIP: exec element with initial implementation
Problem is that the main application actually does not create a pty and
instead uses the current one (for better compatability for ssh based
hosting).

The current problem is the fact that the child process should not
take over (and never give back too) the input / output handling of the
current pts. Such that both applications can receive inputs accordingly
(in best case actually controlled by the main application).
2025-07-12 20:26:47 +02:00
30 changed files with 775 additions and 1375 deletions

View File

@@ -2,7 +2,7 @@
`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.
## 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
```
> [!tip]
> [!TIP]
> 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.
@@ -33,6 +33,8 @@ const zterm: *Dependency = b.dependency("zterm", .{
.target = target,
.optimize = optimize,
});
// ...
exe.root_module.addImport("zterm", zterm.module("zterm"));
```
### Documentation

View File

@@ -11,9 +11,7 @@ pub fn build(b: *std.Build) void {
button,
input,
progress,
radio_button,
scrollable,
selection,
// layouts:
vertical,
horizontal,
@@ -35,15 +33,20 @@ pub fn build(b: *std.Build) void {
options.addOption(bool, "debug", debug_rendering);
const options_module = options.createModule();
// dependencies
const zg = b.dependency("zg", .{
.target = target,
.optimize = optimize,
});
// library
const lib = b.addModule("zterm", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "build_options", .module = options_module },
},
});
lib.addImport("code_point", zg.module("code_point"));
lib.addImport("build_options", options_module);
//--- Examples ---
const examples = std.meta.fields(Examples);
@@ -51,52 +54,45 @@ pub fn build(b: *std.Build) void {
if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry
const demo = b.addExecutable(.{
.name = e.name,
.root_module = b.createModule(.{
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
.demo => "examples/demo.zig",
.continuous => "examples/continuous.zig",
// elements:
.alignment => "examples/elements/alignment.zig",
.button => "examples/elements/button.zig",
.input => "examples/elements/input.zig",
.progress => "examples/elements/progress.zig",
.radio_button => "examples/elements/radio-button.zig",
.scrollable => "examples/elements/scrollable.zig",
.selection => "examples/elements/selection.zig",
// layouts:
.vertical => "examples/layouts/vertical.zig",
.horizontal => "examples/layouts/horizontal.zig",
.grid => "examples/layouts/grid.zig",
.mixed => "examples/layouts/mixed.zig",
// styles:
.text => "examples/styles/text.zig",
.palette => "examples/styles/palette.zig",
// error handling
.errors => "examples/errors.zig",
.all => unreachable, // should never happen
}),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zterm", .module = lib },
},
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
.demo => "examples/demo.zig",
.continuous => "examples/continuous.zig",
// elements:
.alignment => "examples/elements/alignment.zig",
.button => "examples/elements/button.zig",
.input => "examples/elements/input.zig",
.progress => "examples/elements/progress.zig",
.scrollable => "examples/elements/scrollable.zig",
// layouts:
.vertical => "examples/layouts/vertical.zig",
.horizontal => "examples/layouts/horizontal.zig",
.grid => "examples/layouts/grid.zig",
.mixed => "examples/layouts/mixed.zig",
// styles:
.text => "examples/styles/text.zig",
.palette => "examples/styles/palette.zig",
// error handling
.errors => "examples/errors.zig",
.all => unreachable, // should never happen
}),
.target = target,
.optimize = optimize,
});
// import dependencies
demo.root_module.addImport("zterm", lib);
// mapping of user selected example to compile step
if (@intFromEnum(example) == e.value or example == .all) b.installArtifact(demo);
}
// zig build test
const lib_unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "build_options", .module = options_module },
},
}),
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
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);

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -33,7 +33,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -79,54 +79,29 @@ pub fn main() !void {
}, quit_text.element());
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
var nested_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
.separator = .{
.enabled = true,
},
},
}, .{});
var inner_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
var environment = try std.process.getEnvMap(allocator);
defer environment.deinit();
var editor: App.Exec = .init(
allocator,
&.{
"tty",
},
&environment,
&app.queue,
);
defer editor.deinit();
try container.append(try App.Container.init(allocator, .{
.border = .{
.color = .light_blue,
.sides = .all,
},
}, .{});
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .blue,
},
.size = .{
.grow = .horizontal,
.dim = .{ .y = 5 },
.dim = .{ .x = 100 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .red,
},
.size = .{
.grow = .horizontal,
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .green,
},
}, .{}));
try nested_container.append(inner_container);
try nested_container.append(try .init(allocator, .{
.size = .{
.grow = .horizontal,
.dim = .{ .y = 1 },
},
}, .{}));
try container.append(nested_container);
}, editor.element()));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.size = .{
@@ -138,6 +113,11 @@ pub fn main() !void {
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
var process_in: std.ArrayListUnmanaged(u8) = .empty;
defer process_in.deinit(allocator);
var process_out: std.ArrayListUnmanaged(u8) = .empty;
defer process_out.deinit(allocator);
// event loop
while (true) {
const event = app.nextEvent();
@@ -145,30 +125,14 @@ pub fn main() !void {
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
renderer.size = .{}; // reset size, such that next resize will cause a full re-draw!
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Spawning $EDITOR failed",
},
});
continue;
}
},
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(&app.model, event) catch |err| app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -183,7 +147,7 @@ pub fn main() !void {
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -195,4 +159,4 @@ const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -24,109 +24,14 @@ 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 text = "Example";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.minSize = minSize,
.content = content,
},
};
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
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 {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
@setEvalBranchQuota(10000);
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -178,7 +83,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -189,23 +94,20 @@ pub fn main() !void {
var container = try App.Container.init(allocator, .{
.layout = .{
// .gap = 2,
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .vertical,
},
}, element);
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, .{
.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());
defer box.deinit();
@@ -215,44 +117,33 @@ pub fn main() !void {
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
loop: while (true) {
// batch events since last iteration
const len = blk: {
app.queue.poll();
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// handle events
for (0..len) |_| {
const event = app.queue.pop();
log.debug("handling event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// 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",
},
});
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break :loop,
else => {},
}
// 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.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -263,5 +154,4 @@ 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) {});
const App = zterm.App(union(enum) {});

View File

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

View File

@@ -13,7 +13,7 @@ pub fn reset(this: *Cell) void {
this.cp = ' ';
}
pub fn value(this: Cell, writer: *std.Io.Writer) !void {
pub fn value(this: Cell, writer: anytype) !void {
try this.style.value(writer, this.cp);
}
@@ -29,15 +29,17 @@ test "ascii styled text" {
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
};
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer writer.deinit();
var string = std.ArrayList(u8).init(std.testing.allocator);
defer string.deinit();
const writer = string.writer();
for (cells) |cell| {
try cell.value(&writer.writer);
try cell.value(writer);
}
try std.testing.expectEqualSlices(
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",
writer.writer.buffer[0..writer.writer.end],
string.items,
);
}
@@ -49,14 +51,16 @@ test "utf-8 styled text" {
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
};
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer writer.deinit();
var string = std.ArrayList(u8).init(std.testing.allocator);
defer string.deinit();
const writer = string.writer();
for (cells) |cell| {
try cell.value(&writer.writer);
try cell.value(writer);
}
try std.testing.expectEqualSlices(
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",
writer.writer.buffer[0..writer.writer.end],
string.items,
);
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -21,19 +21,17 @@ pub const SystemEvent = union(enum) {
key: Key,
/// Mouse input event
mouse: Mouse,
/// Focus event indicating that the application has gained the focus of the user
/// Focus event for mouse interaction
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
focus: bool,
/// Exit event for a completed `Exec` with the associated pid. Fired and handled by `Exec` `Element`s.
exited: std.posix.pid_t,
};
/// 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 {
// TODO maybe it makes sense to have a nested tagged union type (i.e. system: union(enum) and event: 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)`.");
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_tag = @typeInfo(A).@"union".tag_type.?;
const a_enum_fields = @typeInfo(a_fields_tag).@"enum".fields;
@@ -58,27 +56,11 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
i += 1;
}
// 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 log2_i = @bitSizeOf(@TypeOf(i)) - @clz(i);
const EventType = @Type(.{ .int = .{
.signedness = .unsigned,
.bits = @bitSizeOf(@TypeOf(i)) - @clz(i),
.bits = log2_i,
} });
const Event = @Type(.{ .@"enum" = .{
@@ -96,25 +78,19 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
} });
}
/// Determine whether the provided type `T` is a tagged union: `union(enum)`.
pub fn isTaggedUnion(comptime T: type) bool {
switch (@typeInfo(T)) {
.@"union" => |u| if (u.tag_type) |_| {} else {
return false;
// Determine at `comptime` whether the provided type `E` is an `union(enum)`.
pub fn isTaggedUnion(comptime E: type) bool {
switch (@typeInfo(E)) {
.@"union" => |u| {
if (u.tag_type) |_| {} else {
return false;
}
},
else => return false,
}
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 input = @import("input.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
// waiting for space.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s);
std.time.sleep(std.time.ns_per_s);
// Finally, let that other thread go.
try testing.expectEqual(1, q.pop());
@@ -225,7 +225,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
try Thread.yield();
// But we want to ensure that there's a second push waiting, so
// here's another sleep.
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Another spurious wake...
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
// spurious and go back to sleep.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Pop that thing and we're done.
try testing.expectEqual(2, q.pop());
@@ -270,14 +270,14 @@ test "Fill, block, fill, block" {
fn sleepyPush(q: *Queue(u8, 1)) !void {
// Try to ensure the other thread has already started trying to pop.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Spurious wake
q.not_full.signal();
q.not_empty.signal();
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Stick something in the queue so it can be popped.
q.push(1);
@@ -286,7 +286,7 @@ fn sleepyPush(q: *Queue(u8, 1)) !void {
try Thread.yield();
// Give the other thread time to block again.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Spurious wake
q.not_full.signal();
@@ -317,7 +317,7 @@ test "2 readers" {
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
queue.push(1);
queue.push(1);
t1.join();

View File

@@ -58,10 +58,10 @@ pub const Buffered = struct {
}
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime Container: type, container: *Container, comptime Model: type, model: *const Model) !void {
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.content(model);
const cells: []const Cell = try container.content();
if (cells.len == 0) return;
@@ -80,7 +80,7 @@ pub const Buffered = struct {
// free immediately
container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(Container, element, Model, model);
for (container.elements.items) |*element| try this.render(T, element);
}
/// 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();
// TODO measure timings of rendered frames?
var cursor_position: ?Point = null;
var writer = terminal.writer();
const writer = terminal.writer();
const s = this.screen;
const vs = this.virtual_screen;
for (0..this.size.y) |row| {
@@ -110,7 +110,7 @@ pub const Buffered = struct {
// render differences found in virtual screen
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
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!
pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void {
pub fn value(this: Style, writer: anytype, cp: u21) !void {
var buffer: [4]u8 = undefined;
const bytes = try unicode.utf8Encode(cp, &buffer);
assert(bytes > 0);
// build ansi sequence for 256 colors ...
// foreground
try writer.printAscii("\x1b[", .{});
try format(writer, "\x1b[", .{});
try this.fg.write(writer, .fg);
// background
try writer.printAsciiChar(';', .{});
try format(writer, ";", .{});
try this.bg.write(writer, .bg);
// underline
// FIX assert that if the underline property is set that the ul style and the attribute for underlining is available
try writer.printAsciiChar(';', .{});
try format(writer, ";", .{});
try this.ul.write(writer, .ul);
// append styles (aka attributes like bold, italic, strikethrough, etc.)
for (this.emphasis) |attribute| try writer.print(";{d}", .{@intFromEnum(attribute)});
try writer.printAsciiChar('m', .{});
for (this.emphasis) |attribute| try format(writer, ";{d}", .{@intFromEnum(attribute)});
try format(writer, "m", .{});
// content
try writer.printAscii(buffer[0..bytes], .{});
try writer.printAscii("\x1b[0m", .{});
try format(writer, "{s}", .{buffer[0..bytes]});
try format(writer, "\x1b[0m", .{});
}
// TODO implement helper functions for terminal capabilities:
@@ -73,5 +73,6 @@ const std = @import("std");
const unicode = std.unicode;
const meta = std.meta;
const assert = std.debug.assert;
const format = std.fmt.format;
const Color = @import("color.zig").Color;
const Style = @This();

View File

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

View File

@@ -34,10 +34,10 @@ pub const Renderer = struct {
@memset(this.screen, .{});
}
pub fn render(this: *@This(), comptime T: type, container: *const T, comptime Model: type, model: *const Model) !void {
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.content(model);
const cells: []const Cell = try container.content();
if (cells.len == 0) return;
@@ -58,7 +58,7 @@ pub const Renderer = struct {
// free immediately
container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(T, element, Model, model);
for (container.elements.items) |*element| try this.render(T, element);
}
pub fn save(this: @This(), writer: anytype) !void {
@@ -74,8 +74,6 @@ pub const Renderer = struct {
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
///
/// ```zig
/// const Model = struct {};
/// var model: Model = .{};
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
/// defer file.close();
///
@@ -83,8 +81,8 @@ pub const Renderer = struct {
/// var renderer: testing.Renderer = .init(allocator, size);
/// defer renderer.deinit();
///
/// try container.handle(&model, .{ .size = size });
/// try renderer.render(@TypeOf(container), &container, Model, &.{});
/// try container.handle(.{ .size = size });
/// try renderer.render(Container(event.SystemEvent), &container);
/// try renderer.save(file.writer());
/// ```
///
@@ -93,8 +91,7 @@ pub const Renderer = struct {
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
///
/// ```zig
/// const Model = struct {};
/// var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
/// .border = .{
/// .color = .green,
/// .sides = .all,
@@ -105,55 +102,20 @@ pub const Renderer = struct {
/// try testing.expectContainerScreen(.{
/// .rows = 20,
/// .cols = 30,
/// }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
/// }, &container, @import("test/container/border.all.zon"));
/// ```
pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, comptime Model: type, expected: []const Cell) !void {
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void {
const allocator = testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(T, container, Model, &.{});
try renderer.render(Container(event.SystemEvent), container);
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
/// provided cell arrays are identical. Usually the `Cell` slices are
/// the contents of a given screen from the `zterm.testing.Renderer`. See
@@ -165,21 +127,28 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer expected_cps.deinit(allocator);
defer expected_cps.deinit();
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer actual_cps.deinit(allocator);
defer actual_cps.deinit();
var allocating_writer = std.Io.Writer.Allocating.init(allocator);
defer allocating_writer.deinit();
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y);
defer output.deinit();
var writer = &allocating_writer.writer;
var buffer = std.io.bufferedWriter(output.writer());
defer buffer.flush() catch {};
const writer = buffer.writer();
var differ = false;
const expected_centered = try center(allocator, "Expected Screen", size.x, " ");
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
defer dwd.deinit();
const dw: DisplayWidth = .{ .data = &dwd };
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
defer allocator.free(expected_centered);
const actual_centered = try center(allocator, "Actual Screen", size.x, " ");
const actual_centered = try dw.center(allocator, "Actual Screen", size.x, " ");
defer allocator.free(actual_centered);
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
@@ -195,8 +164,8 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
if (!expected_cell.eql(actual_cell)) differ = true;
try expected_cps.append(allocator, expected_cell);
try actual_cps.append(allocator, actual_cell);
try expected_cps.append(expected_cell);
try actual_cps.append(actual_cell);
}
// write screens both formatted to buffer
@@ -209,14 +178,13 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
if (!differ) return;
// test failed
try buffer.flush();
debug.lockStdErr();
defer debug.unlockStdErr();
var stdout_buffer: [1024]u8 = undefined;
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();
const std_writer = std.io.getStdErr().writer();
try std_writer.writeAll(output.items);
return error.TestExpectEqualCells;
}
@@ -227,4 +195,5 @@ const Allocator = std.mem.Allocator;
const event = @import("event.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point;