Compare commits
59 Commits
0.1.0
...
b3dc8096d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
b3dc8096d7
|
|||
|
7cd1fb139f
|
|||
|
f70ea05244
|
|||
|
c645c2efee
|
|||
|
89aeac1e96
|
|||
|
feae9fa1a4
|
|||
|
8f90f57f44
|
|||
|
fee3c796d9
|
|||
|
547f553404
|
|||
|
1f6bbcc45e
|
|||
|
832fc45c3e
|
|||
|
aa17e13b99
|
|||
|
cba07b119c
|
|||
|
f256a79da0
|
|||
|
c50b10f32d
|
|||
|
4f6cabf898
|
|||
|
853f4d9769
|
|||
|
edbca39c38
|
|||
|
0de50e7016
|
|||
|
088e1a9246
|
|||
|
df78c7d6eb
|
|||
|
66b3a77805
|
|||
|
b401b5ece8
|
|||
|
9f33c902ee
|
|||
|
9f29ac6a77
|
|||
|
f775a6ab2d
|
|||
|
a39cee7ccb
|
|||
|
7875db0aea
|
|||
|
7595e3b5bb
|
|||
|
2ba0ed85fb
|
|||
|
439520d4fe
|
|||
|
ded1f2c17e
|
|||
|
92ae8c9681
|
|||
|
743cdca174
|
|||
|
7d8e902ce2
|
|||
|
ed0010c8af
|
|||
|
a8e138deb7
|
|||
|
d0453d08b8
|
|||
|
825fb63bc8
|
|||
|
76f708d9d7
|
|||
|
0d2644f476
|
|||
|
9a818117d7
|
|||
|
5ba5b2b372
|
|||
|
3cb0d11e71
|
|||
|
4cc749facc
|
|||
|
c6d8eec287
|
|||
|
2572b57697
|
|||
|
80a36a9947
|
|||
|
4cde0640c8
|
|||
|
e9a9c2b680
|
|||
|
ba25e6056c
|
|||
|
aa4adf20f9
|
|||
|
50adf32f14
|
|||
|
a4293ff243
|
|||
| 50450f3bbc | |||
| bce134f052 | |||
| 962a384ecf | |||
| 0b7d032b11 | |||
| 7e20dd73d9 |
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup zig installation
|
||||
uses: mlugg/setup-zig@v1
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: master
|
||||
- name: Run tests
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup zig installation
|
||||
uses: mlugg/setup-zig@v1
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: master
|
||||
- name: Lint check
|
||||
|
||||
@@ -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.
|
||||
|
||||
199
build.zig
199
build.zig
@@ -1,5 +1,3 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
@@ -7,10 +5,15 @@ pub fn build(b: *std.Build) void {
|
||||
const Examples = enum {
|
||||
all,
|
||||
demo,
|
||||
continuous,
|
||||
// elements:
|
||||
alignment,
|
||||
button,
|
||||
input,
|
||||
progress,
|
||||
radio_button,
|
||||
scrollable,
|
||||
selection,
|
||||
// layouts:
|
||||
vertical,
|
||||
horizontal,
|
||||
@@ -24,175 +27,81 @@ pub fn build(b: *std.Build) void {
|
||||
};
|
||||
|
||||
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
|
||||
const debug_rendering = b.option(bool, "debug", "Enable debug rendering. Highlight origin's, size's, padding's, gap's, etc. (default: false)") orelse false;
|
||||
// NOTE do not support debug rendering in release builds
|
||||
if (debug_rendering == true and optimize != .Debug) @panic("Cannot enable debug rendering in non-debug builds.");
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(Examples, "example", example);
|
||||
|
||||
// dependencies
|
||||
const zg = b.dependency("zg", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
options.addOption(bool, "debug", debug_rendering);
|
||||
const options_module = options.createModule();
|
||||
|
||||
// library
|
||||
const lib = b.addModule("zterm", .{
|
||||
.root_source_file = b.path("src/zterm.zig"),
|
||||
.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"));
|
||||
|
||||
//--- Examples ---
|
||||
|
||||
// demo:
|
||||
const examples = std.meta.fields(Examples);
|
||||
inline for (examples) |e| {
|
||||
if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry
|
||||
const demo = b.addExecutable(.{
|
||||
.name = "demo",
|
||||
.root_source_file = b.path("examples/demo.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
demo.root_module.addImport("zterm", lib);
|
||||
|
||||
.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:
|
||||
const button = b.addExecutable(.{
|
||||
.name = "button",
|
||||
.root_source_file = b.path("examples/elements/button.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
button.root_module.addImport("zterm", lib);
|
||||
|
||||
const input = b.addExecutable(.{
|
||||
.name = "input",
|
||||
.root_source_file = b.path("examples/elements/input.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
input.root_module.addImport("zterm", lib);
|
||||
|
||||
const scrollable = b.addExecutable(.{
|
||||
.name = "scrollable",
|
||||
.root_source_file = b.path("examples/elements/scrollable.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
scrollable.root_module.addImport("zterm", lib);
|
||||
|
||||
.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:
|
||||
const vertical = b.addExecutable(.{
|
||||
.name = "vertical",
|
||||
.root_source_file = b.path("examples/layouts/vertical.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
vertical.root_module.addImport("zterm", lib);
|
||||
|
||||
const horizontal = b.addExecutable(.{
|
||||
.name = "horizontal",
|
||||
.root_source_file = b.path("examples/layouts/horizontal.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
horizontal.root_module.addImport("zterm", lib);
|
||||
|
||||
const grid = b.addExecutable(.{
|
||||
.name = "grid",
|
||||
.root_source_file = b.path("examples/layouts/grid.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
grid.root_module.addImport("zterm", lib);
|
||||
|
||||
const mixed = b.addExecutable(.{
|
||||
.name = "mixed",
|
||||
.root_source_file = b.path("examples/layouts/mixed.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
mixed.root_module.addImport("zterm", lib);
|
||||
|
||||
.vertical => "examples/layouts/vertical.zig",
|
||||
.horizontal => "examples/layouts/horizontal.zig",
|
||||
.grid => "examples/layouts/grid.zig",
|
||||
.mixed => "examples/layouts/mixed.zig",
|
||||
// styles:
|
||||
const palette = b.addExecutable(.{
|
||||
.name = "palette",
|
||||
.root_source_file = b.path("examples/styles/palette.zig"),
|
||||
.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,
|
||||
});
|
||||
palette.root_module.addImport("zterm", lib);
|
||||
|
||||
const text = b.addExecutable(.{
|
||||
.name = "text",
|
||||
.root_source_file = b.path("examples/styles/text.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
text.root_module.addImport("zterm", lib);
|
||||
|
||||
// error handling:
|
||||
const errors = b.addExecutable(.{
|
||||
.name = "errors",
|
||||
.root_source_file = b.path("examples/errors.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
errors.root_module.addImport("zterm", lib);
|
||||
|
||||
// mapping of user selected example to compile step
|
||||
const exe = switch (example) {
|
||||
.demo => demo,
|
||||
// elements:
|
||||
.button => button,
|
||||
.input => input,
|
||||
.scrollable => scrollable,
|
||||
// layouts:
|
||||
.vertical => vertical,
|
||||
.horizontal => horizontal,
|
||||
.grid => grid,
|
||||
.mixed => mixed,
|
||||
// styles:
|
||||
.text => text,
|
||||
.palette => palette,
|
||||
// error handling:
|
||||
.errors => errors,
|
||||
else => blk: {
|
||||
b.installArtifact(button);
|
||||
b.installArtifact(input);
|
||||
b.installArtifact(scrollable);
|
||||
b.installArtifact(vertical);
|
||||
b.installArtifact(horizontal);
|
||||
b.installArtifact(grid);
|
||||
b.installArtifact(mixed);
|
||||
b.installArtifact(text);
|
||||
b.installArtifact(palette);
|
||||
b.installArtifact(errors);
|
||||
break :blk demo;
|
||||
.imports = &.{
|
||||
.{ .name = "zterm", .module = lib },
|
||||
},
|
||||
};
|
||||
b.installArtifact(exe);
|
||||
|
||||
// zig build run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
// Allow additional arguments, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}),
|
||||
});
|
||||
// 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_source_file = b.path("src/zterm.zig"),
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.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"));
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.1.0",
|
||||
.version = "0.3.0",
|
||||
|
||||
// Tracks the earliest Zig version that the package considers to be a
|
||||
// supported use case.
|
||||
@@ -35,12 +35,7 @@
|
||||
// `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 = .{
|
||||
.zg = .{
|
||||
.url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
||||
.hash = "1220f3e29bc40856bfc06e0ee133f814b0011c76de987d8a6a458c2f34d82708899a",
|
||||
},
|
||||
},
|
||||
.dependencies = .{},
|
||||
.paths = .{
|
||||
"LICENSE",
|
||||
"build.zig",
|
||||
|
||||
247
examples/continuous.zig
Normal file
247
examples/continuous.zig
Normal file
@@ -0,0 +1,247 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Spinner element implementation that runs a simple animation that requires
|
||||
/// the continuous draw loop.
|
||||
const Spinner = struct {
|
||||
counter: u8 = 0,
|
||||
index: u8 = 0,
|
||||
|
||||
const map: [6]u21 = .{ '', '', '', '', '', '' };
|
||||
|
||||
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 {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
cells[size.x + 1].cp = map[this.index];
|
||||
|
||||
this.counter += 1;
|
||||
if (this.counter >= 20) {
|
||||
this.index += 1;
|
||||
this.index %= 6;
|
||||
this.counter = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
.queue = queue,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.input.deinit(this.allocator);
|
||||
}
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, _: *App.Model, 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.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
|
||||
this.queue.push(.{ .accept = try this.input.toOwnedSlice(this.allocator) });
|
||||
|
||||
if (key.eql(.{ .cp = zterm.input.Backspace }))
|
||||
_ = this.input.pop();
|
||||
|
||||
if (key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
|
||||
_ = this.input.pop();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, _: *const App.Model, 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.input.items.len == 0) return;
|
||||
|
||||
const row = 1;
|
||||
const col = 1;
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (this.input.items, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = .black;
|
||||
cells[anchor + idx].style.cursor = false;
|
||||
cells[anchor + idx].cp = cp;
|
||||
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
|
||||
if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 input_field: InputField = .init(allocator, &app.queue);
|
||||
defer input_field.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
var spinner: Spinner = .{};
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
.layout = .{
|
||||
.direction = .vertical,
|
||||
.padding = .all(5),
|
||||
},
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_grey },
|
||||
.size = .{
|
||||
.grow = .horizontal,
|
||||
.dim = .{ .y = 10 },
|
||||
},
|
||||
}, input_field.element()));
|
||||
|
||||
const nested_container: App.Container = try .init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_grey },
|
||||
}, spinner.element());
|
||||
try container.append(nested_container);
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
var framerate: u64 = 60;
|
||||
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
|
||||
var next_frame_ms: u64 = 0;
|
||||
|
||||
// draw loop
|
||||
draw: while (true) {
|
||||
const now_ms: u64 = @intCast(time.milliTimestamp());
|
||||
if (now_ms >= next_frame_ms) {
|
||||
next_frame_ms = now_ms + tick_ms;
|
||||
} else {
|
||||
std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
|
||||
next_frame_ms += tick_ms;
|
||||
}
|
||||
|
||||
const len = blk: {
|
||||
app.queue.lock();
|
||||
defer app.queue.unlock();
|
||||
break :blk app.queue.len();
|
||||
};
|
||||
|
||||
// handle events
|
||||
for (0..len) |_| {
|
||||
const event = app.queue.drain() orelse break;
|
||||
log.debug("handling event: {s}", .{@tagName(event)});
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
log.debug("key {any}", .{key});
|
||||
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
||||
},
|
||||
.accept => |input| {
|
||||
defer allocator.free(input);
|
||||
var string = try allocator.alloc(u8, input.len);
|
||||
defer allocator.free(string);
|
||||
for (0.., input) |i, char| string[i] = @intCast(char);
|
||||
log.debug("Accepted input '{s}'", .{string});
|
||||
},
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
.focus => |b| {
|
||||
// NOTE reduce framerate in case the window is not focused and restore again when focused
|
||||
framerate = if (b) 60 else 15;
|
||||
tick_ms = @divFloor(time.ms_per_s, framerate);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break :draw,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = App.panic_handler;
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const std = @import("std");
|
||||
const time = std.time;
|
||||
const assert = std.debug.assert;
|
||||
const zterm = @import("zterm");
|
||||
const App = zterm.App(struct {}, union(enum) {
|
||||
accept: []u21,
|
||||
});
|
||||
@@ -1,11 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const input = zterm.input;
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit. Press ctrl+n to launch helix.";
|
||||
|
||||
@@ -13,9 +5,9 @@ const QuitText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const y = 2;
|
||||
const x = size.x / 2 -| (text.len / 2);
|
||||
@@ -36,12 +28,12 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -75,7 +67,7 @@ pub fn main() !void {
|
||||
}, .{}));
|
||||
defer box.deinit();
|
||||
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
var scrollable: App.Scrollable = .init(box, .disabled);
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
@@ -87,15 +79,54 @@ pub fn main() !void {
|
||||
}, quit_text.element());
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
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,
|
||||
},
|
||||
.border = .{
|
||||
.color = .light_blue,
|
||||
.sides = .all,
|
||||
},
|
||||
}, .{});
|
||||
try inner_container.append(try .init(allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .blue,
|
||||
},
|
||||
.size = .{
|
||||
.dim = .{ .x = 100 },
|
||||
.grow = .horizontal,
|
||||
.dim = .{ .y = 5 },
|
||||
},
|
||||
}, .{}));
|
||||
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);
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.size = .{
|
||||
@@ -119,6 +150,7 @@ pub fn main() !void {
|
||||
|
||||
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(.{
|
||||
@@ -136,7 +168,7 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
// 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,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -149,10 +181,18 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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 input = zterm.input;
|
||||
const App = zterm.App(struct {}, union(enum) {});
|
||||
|
||||
99
examples/elements/alignment.zig
Normal file
99
examples/elements/alignment.zig
Normal file
@@ -0,0 +1,99 @@
|
||||
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 container: App.Container = try .init(allocator, .{}, .{});
|
||||
defer container.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
const quit_container = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.layout = .{
|
||||
.direction = .vertical,
|
||||
.padding = .all(5),
|
||||
},
|
||||
.size = .{
|
||||
.dim = .{ .x = 25, .y = 5 },
|
||||
.grow = .fixed,
|
||||
},
|
||||
}, quit_text.element());
|
||||
|
||||
var alignment: App.Alignment = .init(quit_container, .center);
|
||||
try container.append(try .init(allocator, .{}, alignment.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 App = zterm.App(struct {}, union(enum) {});
|
||||
@@ -1,12 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {
|
||||
click: [:0]const u8,
|
||||
});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -14,9 +5,9 @@ const QuitText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -49,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));
|
||||
switch (event) {
|
||||
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
|
||||
@@ -64,9 +55,9 @@ 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));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = size.y / 2 -| (text.len / 2);
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -86,18 +77,20 @@ const Clickable = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var clickable: Clickable = .{ .queue = &app.queue };
|
||||
const element = clickable.element();
|
||||
|
||||
var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button"));
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
@@ -107,6 +100,7 @@ pub fn main() !void {
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, 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});
|
||||
@@ -119,14 +113,13 @@ pub fn main() !void {
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.click => |button| {
|
||||
log.info("Clicked with mouse using Button: {s}", .{button});
|
||||
},
|
||||
.click => |b| log.info("Clicked with mouse using Button: {s}", .{b}),
|
||||
.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(event) catch |err| app.postEvent(.{
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -139,10 +132,23 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {
|
||||
click: [:0]const u8,
|
||||
accept,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {
|
||||
accept: []u21,
|
||||
});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -14,9 +5,9 @@ const QuitText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -33,20 +24,8 @@ const QuitText = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const InputField = struct {
|
||||
input: std.ArrayList(u21),
|
||||
queue: *App.Queue,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
|
||||
return .{
|
||||
.input = .init(allocator),
|
||||
.queue = queue,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: @This()) void {
|
||||
this.input.deinit();
|
||||
}
|
||||
const MouseDraw = struct {
|
||||
position: ?zterm.Point = null,
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
@@ -58,38 +37,22 @@ 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));
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
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() });
|
||||
|
||||
if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
|
||||
_ = this.input.pop();
|
||||
},
|
||||
else => {},
|
||||
.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 {
|
||||
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));
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
if (this.input.items.len == 0) return;
|
||||
|
||||
const row = 1;
|
||||
const col = 1;
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (this.input.items, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = .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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -97,29 +60,59 @@ const InputField = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var input_field: InputField = .init(allocator, &app.queue);
|
||||
var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black));
|
||||
defer input_field.deinit();
|
||||
|
||||
var mouse_draw: MouseDraw = .{};
|
||||
var second_mouse_draw: MouseDraw = .{};
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
const element = input_field.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
.layout = .{ .padding = .all(5) },
|
||||
.layout = .{
|
||||
.direction = .vertical,
|
||||
.padding = .all(5),
|
||||
},
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_grey },
|
||||
.size = .{
|
||||
.grow = .horizontal,
|
||||
.dim = .{ .y = 1 },
|
||||
},
|
||||
}, input_field.element()));
|
||||
|
||||
var nested_container: App.Container = try .init(allocator, .{
|
||||
.border = .{
|
||||
.sides = .all,
|
||||
.color = .black,
|
||||
},
|
||||
.rectangle = .{ .fill = .light_grey },
|
||||
.layout = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .black,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try nested_container.append(try .init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_grey },
|
||||
}, mouse_draw.element()));
|
||||
try nested_container.append(try .init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_grey },
|
||||
}, second_mouse_draw.element()));
|
||||
try container.append(nested_container);
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
@@ -134,13 +127,13 @@ pub fn main() !void {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.accept => |input| {
|
||||
defer allocator.free(input);
|
||||
log.info("Accepted input {any}", .{input});
|
||||
log.debug("Accepted input '{s}'", .{input});
|
||||
},
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -153,10 +146,24 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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 Color = zterm.Color;
|
||||
|
||||
const App = zterm.App(
|
||||
struct {},
|
||||
union(enum) {
|
||||
accept: []u8,
|
||||
},
|
||||
);
|
||||
|
||||
174
examples/elements/progress.zig
Normal file
174
examples/elements/progress.zig
Normal file
@@ -0,0 +1,174 @@
|
||||
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 progress_percent: u8 = 0;
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{ .padding = .all(5), .direction = .vertical },
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
{
|
||||
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||
.percent = .{
|
||||
.enabled = true,
|
||||
.alignment = .left,
|
||||
},
|
||||
.fg = .blue,
|
||||
.bg = .grey,
|
||||
});
|
||||
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||
}
|
||||
{
|
||||
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||
.percent = .{
|
||||
.enabled = true,
|
||||
.alignment = .middle, // default
|
||||
},
|
||||
.fg = .red,
|
||||
.bg = .grey,
|
||||
});
|
||||
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||
}
|
||||
{
|
||||
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||
.percent = .{
|
||||
.enabled = true,
|
||||
.alignment = .right,
|
||||
},
|
||||
.fg = .green,
|
||||
.bg = .grey,
|
||||
});
|
||||
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||
}
|
||||
{
|
||||
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||
.percent = .{ .enabled = false },
|
||||
.fg = .default,
|
||||
.bg = .grey,
|
||||
});
|
||||
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||
}
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
var framerate: u64 = 60;
|
||||
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
|
||||
var next_frame_ms: u64 = 0;
|
||||
|
||||
var increase_progress: u64 = 10;
|
||||
|
||||
// Continuous drawing
|
||||
// draw loop
|
||||
draw: while (true) {
|
||||
const now_ms: u64 = @intCast(time.milliTimestamp());
|
||||
if (now_ms >= next_frame_ms) {
|
||||
next_frame_ms = now_ms + tick_ms;
|
||||
} else {
|
||||
std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
|
||||
next_frame_ms += tick_ms;
|
||||
}
|
||||
|
||||
// NOTE time based progress increasion
|
||||
increase_progress -= 1;
|
||||
if (increase_progress == 0) {
|
||||
increase_progress = 10;
|
||||
progress_percent += 1;
|
||||
if (progress_percent > 100) progress_percent = 0;
|
||||
app.postEvent(.{ .progress = progress_percent });
|
||||
}
|
||||
|
||||
const len = blk: {
|
||||
app.queue.lock();
|
||||
defer app.queue.unlock();
|
||||
break :blk app.queue.len();
|
||||
};
|
||||
|
||||
// handle events
|
||||
for (0..len) |_| {
|
||||
const event = app.queue.drain() orelse break;
|
||||
log.debug("handling event: {s}", .{@tagName(event)});
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
||||
},
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
.focus => |b| {
|
||||
// NOTE reduce framerate in case the window is not focused and restore again when focused
|
||||
framerate = if (b) 60 else 15;
|
||||
tick_ms = @divFloor(time.ms_per_s, framerate);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break :draw,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = App.panic_handler;
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const std = @import("std");
|
||||
const time = std.time;
|
||||
const assert = std.debug.assert;
|
||||
const zterm = @import("zterm");
|
||||
const App = zterm.App(
|
||||
struct {},
|
||||
union(enum) {
|
||||
progress: u8,
|
||||
},
|
||||
);
|
||||
98
examples/elements/radio-button.zig
Normal file
98
examples/elements/radio-button.zig
Normal 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,
|
||||
});
|
||||
@@ -1,11 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const input = zterm.input;
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -13,9 +5,9 @@ const QuitText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -42,9 +34,9 @@ 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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = size.y / 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -65,7 +57,7 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
if (deinit_status == .leak) {
|
||||
@@ -74,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();
|
||||
|
||||
@@ -156,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 = .{ .container = top_box };
|
||||
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 = .{ .container = bottom_box };
|
||||
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white));
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
|
||||
|
||||
try app.start();
|
||||
@@ -177,7 +169,7 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -190,10 +182,18 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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 input = zterm.input;
|
||||
const App = zterm.App(struct {}, union(enum) {});
|
||||
|
||||
97
examples/elements/selection.zig
Normal file
97
examples/elements/selection.zig
Normal 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) {});
|
||||
@@ -1,10 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -12,9 +5,9 @@ const QuitText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -38,9 +31,9 @@ const InfoText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -64,18 +57,18 @@ const ErrorNotification = struct {
|
||||
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));
|
||||
switch (event) {
|
||||
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
|
||||
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall,
|
||||
.err => |err| this.msg = err.msg,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
if (this.msg) |msg| {
|
||||
const row = size.y -| 2;
|
||||
@@ -99,12 +92,12 @@ const ErrorNotification = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -137,7 +130,7 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -150,10 +143,17 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {});
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -15,9 +8,9 @@ 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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -93,7 +86,7 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
// 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,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -106,10 +99,17 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {});
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -15,9 +8,9 @@ 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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -85,7 +78,7 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
// 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,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -98,10 +91,17 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {});
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -15,9 +8,9 @@ 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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -101,7 +94,7 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
// 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,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -114,10 +107,17 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {});
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -15,9 +8,9 @@ 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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -84,7 +77,7 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
// 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,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -97,10 +90,17 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {});
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -12,9 +5,9 @@ const QuitText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -34,12 +27,12 @@ const QuitText = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -60,10 +53,10 @@ pub fn main() !void {
|
||||
defer box.deinit();
|
||||
|
||||
inline for (std.meta.fields(zterm.Color)) |field| {
|
||||
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
if (field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
|
||||
}
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
var scrollable: App.Scrollable = .init(box, .disabled);
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try app.start();
|
||||
@@ -80,7 +73,7 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -93,10 +86,17 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {});
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit.";
|
||||
|
||||
@@ -12,9 +5,9 @@ const QuitText = struct {
|
||||
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;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
@@ -35,24 +28,39 @@ const TextStyles = struct {
|
||||
const text = "Example";
|
||||
|
||||
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 {
|
||||
@setEvalBranchQuota(50000);
|
||||
fn minSize(ctx: *anyopaque, size: zterm.Point) zterm.Point {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
_ = 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);
|
||||
_ = ctx;
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
var row: usize = 0;
|
||||
var col: usize = 0;
|
||||
|
||||
// Color
|
||||
inline for (std.meta.fields(zterm.Color)) |bg_field| {
|
||||
if (comptime bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
if (bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
|
||||
inline for (std.meta.fields(zterm.Color)) |fg_field| {
|
||||
if (comptime fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
if (comptime fg_field.value == bg_field.value) continue;
|
||||
if (fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
if (fg_field.value == bg_field.value) continue;
|
||||
|
||||
// witouth any emphasis
|
||||
for (text) |cp| {
|
||||
@@ -64,7 +72,7 @@ const TextStyles = struct {
|
||||
|
||||
// emphasis (no combinations)
|
||||
inline for (std.meta.fields(zterm.Style.Emphasis)) |emp_field| {
|
||||
if (comptime emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
|
||||
if (emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
|
||||
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
|
||||
|
||||
for (text) |cp| {
|
||||
@@ -85,12 +93,12 @@ const TextStyles = struct {
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var app: App = .init(.{});
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
@@ -109,16 +117,10 @@ pub fn main() !void {
|
||||
|
||||
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();
|
||||
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
var scrollable: App.Scrollable = .init(box, .disabled);
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try app.start();
|
||||
@@ -135,7 +137,7 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
@@ -148,10 +150,18 @@ pub fn main() !void {
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.resize(try renderer.resize());
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
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) {});
|
||||
|
||||
292
src/app.zig
292
src/app.zig
@@ -1,19 +1,4 @@
|
||||
//! Application type for TUI-applications
|
||||
const std = @import("std");
|
||||
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 Mouse = input.Mouse;
|
||||
const Key = input.Key;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
/// Create the App Type with the associated user events _E_ which describes
|
||||
/// an tagged union for all the user events that can be send through the
|
||||
@@ -27,74 +12,69 @@ const log = std.log.scoped(.app);
|
||||
/// ```zig
|
||||
/// const zterm = @import("zterm");
|
||||
/// 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
|
||||
/// var app: App = .init;
|
||||
/// var app: App = .init(.{}); // provide instance of the model that shall be used
|
||||
/// 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 {
|
||||
if (!isTaggedUnion(E)) {
|
||||
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
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)`.");
|
||||
return struct {
|
||||
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
|
||||
pub const Container = @import("container.zig").Container(Event);
|
||||
const element = @import("element.zig");
|
||||
pub const Element = element.Element(Event);
|
||||
pub const Scrollable = element.Scrollable(Event);
|
||||
pub const Queue = queue.Queue(Event, 256);
|
||||
|
||||
model: Model,
|
||||
queue: Queue,
|
||||
thread: ?std.Thread,
|
||||
quit_event: std.Thread.ResetEvent,
|
||||
termios: ?std.posix.termios = null,
|
||||
attached_handler: bool = false,
|
||||
thread: ?Thread = null,
|
||||
quit_event: Thread.ResetEvent,
|
||||
termios: ?posix.termios = null,
|
||||
winch_registered: bool = false,
|
||||
|
||||
pub const SignalHandler = struct {
|
||||
context: *anyopaque,
|
||||
callback: *const fn (context: *anyopaque) void,
|
||||
};
|
||||
// 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 {
|
||||
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 const init: @This() = .{
|
||||
pub fn init(model: Model) @This() {
|
||||
return .{
|
||||
.model = model,
|
||||
.queue = .{},
|
||||
.thread = null,
|
||||
.quit_event = .{},
|
||||
.termios = null,
|
||||
.attached_handler = false,
|
||||
.quit_event = .unset,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn start(this: *@This()) !void {
|
||||
if (this.thread) |_| return;
|
||||
|
||||
if (!this.attached_handler) {
|
||||
var winch_act = std.posix.Sigaction{
|
||||
.handler = .{ .handler = @This().handleWinch },
|
||||
.mask = std.posix.empty_sigset,
|
||||
.flags = 0,
|
||||
};
|
||||
std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null);
|
||||
|
||||
try registerWinch(.{
|
||||
.context = this,
|
||||
.callback = @This().winsizeCallback,
|
||||
});
|
||||
this.attached_handler = true;
|
||||
|
||||
// post init event (as the very first element to be in the queue - event loop)
|
||||
this.postEvent(.init);
|
||||
|
||||
if (!this.winch_registered) {
|
||||
handler_ctx = this;
|
||||
var act = posix.Sigaction{
|
||||
.handler = .{ .handler = handleWinch },
|
||||
.mask = posix.sigemptyset(),
|
||||
.flags = 0,
|
||||
};
|
||||
posix.sigaction(posix.SIG.WINCH, &act, null);
|
||||
this.winch_registered = true;
|
||||
}
|
||||
|
||||
this.quit_event.reset();
|
||||
this.thread = try std.Thread.spawn(.{}, @This().run, .{this});
|
||||
this.thread = try Thread.spawn(.{}, @This().run, .{this});
|
||||
|
||||
var termios: std.posix.termios = undefined;
|
||||
var termios: posix.termios = undefined;
|
||||
try terminal.enableRawMode(&termios);
|
||||
if (this.termios) |_| {} else this.termios = termios;
|
||||
|
||||
try terminal.saveScreen();
|
||||
try terminal.enterAltScreen();
|
||||
try terminal.saveScreen();
|
||||
try terminal.hideCursor();
|
||||
try terminal.enableMouseSupport();
|
||||
}
|
||||
@@ -102,8 +82,8 @@ pub fn App(comptime E: type) type {
|
||||
pub fn interrupt(this: *@This()) !void {
|
||||
this.quit_event.set();
|
||||
try terminal.disableMouseSupport();
|
||||
try terminal.exitAltScreen();
|
||||
try terminal.restoreScreen();
|
||||
try terminal.exitAltScreen();
|
||||
if (this.thread) |thread| {
|
||||
thread.join();
|
||||
this.thread = null;
|
||||
@@ -112,12 +92,12 @@ pub fn App(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.exitAltScreen();
|
||||
try terminal.disableRawMode(termios);
|
||||
try terminal.restoreScreen();
|
||||
try terminal.disableRawMode(&termios);
|
||||
try terminal.exitAltScreen();
|
||||
}
|
||||
this.termios = null;
|
||||
}
|
||||
@@ -139,27 +119,6 @@ pub fn App(comptime E: type) type {
|
||||
this.queue.push(e);
|
||||
}
|
||||
|
||||
fn winsizeCallback(ptr: *anyopaque) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ptr));
|
||||
_ = this;
|
||||
// this.postEvent(.{ .size = terminal.getTerminalSize() });
|
||||
}
|
||||
|
||||
var winch_handler: ?SignalHandler = null;
|
||||
|
||||
fn registerWinch(handler: SignalHandler) !void {
|
||||
if (winch_handler) |_| {
|
||||
@panic("Cannot register another WINCH handler.");
|
||||
}
|
||||
winch_handler = handler;
|
||||
}
|
||||
|
||||
fn handleWinch(_: c_int) callconv(.C) void {
|
||||
if (winch_handler) |handler| {
|
||||
handler.callback(handler.context);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(this: *@This()) !void {
|
||||
// thread to read user inputs
|
||||
var buf: [256]u8 = undefined;
|
||||
@@ -208,6 +167,9 @@ pub fn App(comptime E: type) type {
|
||||
// Legacy keys
|
||||
// CSI {ABCDEFHPQS}
|
||||
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
|
||||
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
_ = field_iter.next(); // skip first field
|
||||
|
||||
const key: Key = .{
|
||||
.cp = switch (final) {
|
||||
'A' => input.Up,
|
||||
@@ -221,22 +183,36 @@ pub fn App(comptime E: type) type {
|
||||
'Q' => input.F2,
|
||||
'R' => input.F3,
|
||||
'S' => input.F4,
|
||||
else => unreachable, // switch case prevents in this case form ever happening
|
||||
else => unreachable,
|
||||
},
|
||||
.mod = blk: {
|
||||
// modifier_mask:event_type
|
||||
var mod: Key.Modifier = .{};
|
||||
const field_buf = field_iter.next() orelse break :blk mod;
|
||||
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
|
||||
const modifier_buf = param_iter.next() orelse unreachable;
|
||||
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
|
||||
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
|
||||
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
|
||||
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
|
||||
break :blk mod;
|
||||
},
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
},
|
||||
'Z' => this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
|
||||
'~' => {
|
||||
// Legacy keys
|
||||
// CSI number ~
|
||||
// CSI number ; modifier ~
|
||||
// CSI number ; modifier:event_type ; text_as_codepoint ~
|
||||
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
const number_buf = field_iter.next() orelse unreachable; // always will have one field
|
||||
const number = std.fmt.parseUnsigned(u16, number_buf, 10) catch break;
|
||||
var field_iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
|
||||
const key: Key = .{
|
||||
.cp = switch (number) {
|
||||
.cp = blk: {
|
||||
const number_buf = field_iter.next() orelse unreachable; // always will have one field
|
||||
const number = fmt.parseUnsigned(u16, number_buf, 10) catch break;
|
||||
break :blk switch (number) {
|
||||
2 => input.Insert,
|
||||
3 => input.Delete,
|
||||
5 => input.PageUp,
|
||||
@@ -255,10 +231,31 @@ pub fn App(comptime E: type) type {
|
||||
21 => input.F10,
|
||||
23 => input.F11,
|
||||
24 => input.F12,
|
||||
25 => input.F13,
|
||||
26 => input.F14,
|
||||
28 => input.F15,
|
||||
29 => input.F16,
|
||||
31 => input.F17,
|
||||
32 => input.F18,
|
||||
33 => input.F19,
|
||||
34 => input.F20,
|
||||
// 200 => return .{ .event = .paste_start, .n = sequence.len },
|
||||
// 201 => return .{ .event = .paste_end, .n = sequence.len },
|
||||
57427 => input.KpBegin,
|
||||
57399...57454 => |code| code,
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
.mod = blk: {
|
||||
// modifier_mask:event_type
|
||||
var mod: Key.Modifier = .{};
|
||||
const field_buf = field_iter.next() orelse break :blk mod;
|
||||
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
|
||||
const modifier_buf = param_iter.next() orelse unreachable;
|
||||
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
|
||||
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
|
||||
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
|
||||
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
|
||||
break :blk mod;
|
||||
},
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
@@ -267,14 +264,14 @@ pub fn App(comptime E: type) type {
|
||||
'I' => this.postEvent(.{ .focus = true }),
|
||||
'O' => this.postEvent(.{ .focus = false }),
|
||||
'M', 'm' => {
|
||||
std.debug.assert(sequence.len >= 4);
|
||||
assert(sequence.len >= 4);
|
||||
if (sequence[2] != '<') break;
|
||||
|
||||
const delim1 = std.mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
|
||||
const button_mask = std.fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
|
||||
const delim2 = std.mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
|
||||
const px = std.fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
|
||||
const py = std.fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
|
||||
const delim1 = mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
|
||||
const button_mask = fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
|
||||
const delim2 = mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
|
||||
const px = fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
|
||||
const py = fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
|
||||
|
||||
const mouse_bits = packed struct {
|
||||
const motion: u8 = 0b00100000;
|
||||
@@ -295,12 +292,8 @@ pub fn App(comptime E: type) type {
|
||||
.x = px -| 1,
|
||||
.y = py -| 1,
|
||||
.kind = blk: {
|
||||
if (motion and button != Mouse.Button.none) {
|
||||
break :blk .drag;
|
||||
}
|
||||
if (motion and button == Mouse.Button.none) {
|
||||
break :blk .motion;
|
||||
}
|
||||
if (motion and button != Mouse.Button.none) break :blk .drag;
|
||||
if (motion and button == Mouse.Button.none) break :blk .motion;
|
||||
if (sequence[sequence.len - 1] == 'm') break :blk .release;
|
||||
break :blk .press;
|
||||
},
|
||||
@@ -314,14 +307,14 @@ pub fn App(comptime E: type) type {
|
||||
// Device Status Report
|
||||
// CSI Ps n
|
||||
// CSI ? Ps n
|
||||
std.debug.assert(sequence.len >= 3);
|
||||
assert(sequence.len >= 3);
|
||||
},
|
||||
't' => {
|
||||
// XTWINOPS
|
||||
// Split first into fields delimited by ';'
|
||||
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
var iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
const ps = iter.first();
|
||||
if (std.mem.eql(u8, "48", ps)) {
|
||||
if (mem.eql(u8, "48", ps)) {
|
||||
// in band window resize
|
||||
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
||||
const width_char = iter.next() orelse break;
|
||||
@@ -329,9 +322,10 @@ pub fn App(comptime E: type) type {
|
||||
|
||||
_ = width_char;
|
||||
_ = height_char;
|
||||
this.postEvent(.resize);
|
||||
// this.postEvent(.{ .size = .{
|
||||
// .x = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||
// .y = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
|
||||
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||
// .y = fmt.parseUnsigned(u16, height_char, 10) catch break,
|
||||
// } });
|
||||
}
|
||||
},
|
||||
@@ -347,9 +341,29 @@ pub fn App(comptime E: type) type {
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
0x50 => {
|
||||
// DCS
|
||||
},
|
||||
0x58 => {
|
||||
// SOS
|
||||
},
|
||||
0x5D => {
|
||||
// OSC
|
||||
},
|
||||
// TODO parse corresponding codes
|
||||
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
||||
else => {},
|
||||
0x5F => {
|
||||
// APC
|
||||
// parse for kitty graphics capabilities
|
||||
},
|
||||
else => {
|
||||
// alt + <char> keypress
|
||||
this.postEvent(.{
|
||||
.key = .{
|
||||
.cp = buf[1],
|
||||
.mod = .{ .alt = true },
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
} else {
|
||||
const b = buf[0];
|
||||
@@ -357,16 +371,17 @@ pub fn App(comptime E: type) type {
|
||||
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
|
||||
0x08 => .{ .cp = input.Backspace },
|
||||
0x09 => .{ .cp = input.Tab },
|
||||
0x0a, 0x0d => .{ .cp = input.Enter },
|
||||
0x0a => .{ .cp = 'j', .mod = .{ .ctrl = true } },
|
||||
0x0d => .{ .cp = input.Enter },
|
||||
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
|
||||
0x1b => escape: {
|
||||
std.debug.assert(read_bytes == 1);
|
||||
assert(read_bytes == 1);
|
||||
break :escape .{ .cp = input.Escape };
|
||||
},
|
||||
0x7f => .{ .cp = input.Backspace },
|
||||
else => {
|
||||
var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] };
|
||||
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } });
|
||||
var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..read_bytes], .i = 0 };
|
||||
while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } });
|
||||
continue;
|
||||
},
|
||||
};
|
||||
@@ -377,5 +392,56 @@ pub fn App(comptime E: type) type {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
|
||||
terminal.disableMouseSupport() catch {};
|
||||
terminal.showCursor() catch {};
|
||||
terminal.restoreScreen() catch {};
|
||||
terminal.disableRawMode(&.{
|
||||
.iflag = .{},
|
||||
.lflag = .{},
|
||||
.cflag = .{},
|
||||
.oflag = .{},
|
||||
.cc = undefined,
|
||||
.line = 0,
|
||||
.ispeed = undefined,
|
||||
.ospeed = undefined,
|
||||
}) catch {};
|
||||
terminal.exitAltScreen() 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 Queue = queue.Queue(Event, 256);
|
||||
};
|
||||
}
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const fmt = std.fmt;
|
||||
const posix = std.posix;
|
||||
const Thread = std.Thread;
|
||||
const assert = std.debug.assert;
|
||||
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;
|
||||
|
||||
33
src/cell.zig
33
src/cell.zig
@@ -1,11 +1,8 @@
|
||||
const std = @import("std");
|
||||
const Style = @import("style.zig");
|
||||
//! Cell type containing content and formatting for each character in the terminal screen.
|
||||
|
||||
pub const Cell = @This();
|
||||
|
||||
style: Style = .{ .emphasis = &.{} },
|
||||
// TODO embrace `zg` dependency more due to utf-8 encoding
|
||||
cp: u21 = ' ',
|
||||
style: Style = .{ .emphasis = &.{} },
|
||||
|
||||
pub fn eql(this: Cell, other: Cell) bool {
|
||||
return this.cp == other.cp and this.style.eql(other.style);
|
||||
@@ -16,10 +13,14 @@ pub fn reset(this: *Cell) void {
|
||||
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);
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const Style = @import("style.zig");
|
||||
const Cell = @This();
|
||||
|
||||
test "ascii styled text" {
|
||||
const cells: [4]Cell = .{
|
||||
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
||||
@@ -28,17 +29,15 @@ test "ascii styled text" {
|
||||
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
||||
};
|
||||
|
||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
||||
defer string.deinit();
|
||||
|
||||
const writer = string.writer();
|
||||
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
|
||||
defer writer.deinit();
|
||||
for (cells) |cell| {
|
||||
try cell.value(writer);
|
||||
try cell.value(&writer.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",
|
||||
string.items,
|
||||
writer.writer.buffer[0..writer.writer.end],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,16 +49,14 @@ test "utf-8 styled text" {
|
||||
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
||||
};
|
||||
|
||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
||||
defer string.deinit();
|
||||
|
||||
const writer = string.writer();
|
||||
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
|
||||
defer writer.deinit();
|
||||
for (cells) |cell| {
|
||||
try cell.value(writer);
|
||||
try cell.value(&writer.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",
|
||||
string.items,
|
||||
writer.writer.buffer[0..writer.writer.end],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Color = enum(u8) {
|
||||
default = 0,
|
||||
black = 16,
|
||||
@@ -20,19 +18,23 @@ pub const Color = enum(u8) {
|
||||
white,
|
||||
// TODO add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
|
||||
|
||||
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
|
||||
// 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 {
|
||||
if (this == .default) {
|
||||
switch (coloring) {
|
||||
.fg => try std.fmt.format(writer, "39", .{}),
|
||||
.bg => try std.fmt.format(writer, "49", .{}),
|
||||
.ul => try std.fmt.format(writer, "59", .{}),
|
||||
.fg => try writer.printAscii("39", .{}),
|
||||
.bg => try writer.printAscii("49", .{}),
|
||||
.ul => try writer.printAscii("59", .{}),
|
||||
}
|
||||
} else {
|
||||
switch (coloring) {
|
||||
.fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(this)}),
|
||||
.bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(this)}),
|
||||
.ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(this)}),
|
||||
.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)}),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
const std = @import("std");
|
||||
|
||||
const isTaggedUnion = @import("event.zig").isTaggedUnion;
|
||||
|
||||
const Cell = @import("cell.zig");
|
||||
const Color = @import("color.zig").Color;
|
||||
const Point = @import("point.zig").Point;
|
||||
const Style = @import("style.zig");
|
||||
const Error = @import("error.zig").Error;
|
||||
|
||||
const log = std.log.scoped(.container);
|
||||
// FIX known issues:
|
||||
// - hold fewer instances of the `Allocator`
|
||||
|
||||
/// Border configuration struct
|
||||
pub const Border = packed struct {
|
||||
// corners:
|
||||
const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
|
||||
const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
|
||||
|
||||
/// Color to use for the border
|
||||
color: Color = .default,
|
||||
/// Configure the corner type to be used for the border
|
||||
@@ -39,13 +26,12 @@ pub const Border = packed struct {
|
||||
} = .{},
|
||||
|
||||
pub fn content(this: @This(), cells: []Cell, size: Point) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const frame = switch (this.corners) {
|
||||
.rounded => Border.rounded_border,
|
||||
.squared => Border.squared_border,
|
||||
const frame: [6]u21 = switch (this.corners) {
|
||||
.rounded => .{ '╭', '─', '╮', '│', '╰', '╯' },
|
||||
.squared => .{ '┌', '─', '┐', '│', '└', '┘' },
|
||||
};
|
||||
std.debug.assert(frame.len == 6);
|
||||
|
||||
// render top and bottom border
|
||||
if (this.sides.top or this.sides.bottom) {
|
||||
@@ -94,8 +80,9 @@ pub const Border = packed struct {
|
||||
test "all sides" {
|
||||
const event = @import("event.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 = .{
|
||||
.color = .green,
|
||||
.sides = .all,
|
||||
@@ -106,14 +93,15 @@ pub const Border = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.all.zon"));
|
||||
}, @TypeOf(container), &container, Model, @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(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .vertical,
|
||||
@@ -124,14 +112,15 @@ pub const Border = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.vertical.zon"));
|
||||
}, @TypeOf(container), &container, Model, @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(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .horizontal,
|
||||
@@ -142,7 +131,7 @@ pub const Border = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.horizontal.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/border.horizontal.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,20 +144,28 @@ pub const Rectangle = packed struct {
|
||||
|
||||
// NOTE caller owns `Cells` slice and ensures that `cells.len == size.x * size.y`
|
||||
pub fn content(this: @This(), cells: []Cell, size: Point) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
cells[(row * size.x) + col].style.bg = this.fill;
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG render corresponding beginning of the rectangle for this `Container` *red*
|
||||
if (comptime build_options.debug) {
|
||||
cells[0].style.fg = .red;
|
||||
cells[0].style.bg = .black;
|
||||
cells[0].cp = 'r'; // 'r' for *rectangle*
|
||||
}
|
||||
}
|
||||
|
||||
test "fill color overwrite parent fill" {
|
||||
const event = @import("event.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 },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
@@ -180,14 +177,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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 = .{
|
||||
.padding = .all(2),
|
||||
},
|
||||
@@ -202,14 +200,43 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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)" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.padding = .{
|
||||
.top = -18,
|
||||
.bottom = -18,
|
||||
.left = -28,
|
||||
.right = -28,
|
||||
},
|
||||
},
|
||||
.rectangle = .{ .fill = .green },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, @TypeOf(container), &container, Model, @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(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .black,
|
||||
},
|
||||
@@ -230,14 +257,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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 = .black,
|
||||
},
|
||||
@@ -261,14 +289,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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 = .black,
|
||||
},
|
||||
@@ -293,38 +322,33 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_separator.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_separator.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
/// Layout configuration struct
|
||||
pub const Layout = packed struct {
|
||||
// separator.line:
|
||||
const line: [2]u21 = .{ '│', '─' };
|
||||
const dotted: [2]u21 = .{ '┆', '┄' };
|
||||
const double: [2]u21 = .{ '║', '═' };
|
||||
|
||||
/// control the direction in which child elements are laid out
|
||||
direction: enum(u1) { horizontal, vertical } = .horizontal,
|
||||
/// Padding outside of the child elements
|
||||
padding: packed struct {
|
||||
top: u16 = 0,
|
||||
bottom: u16 = 0,
|
||||
left: u16 = 0,
|
||||
right: u16 = 0,
|
||||
top: i16 = 0,
|
||||
bottom: i16 = 0,
|
||||
left: i16 = 0,
|
||||
right: i16 = 0,
|
||||
|
||||
/// Create a padding with equivalent padding in all four directions.
|
||||
pub fn all(padding: u16) @This() {
|
||||
pub fn all(padding: i16) @This() {
|
||||
return .{ .top = padding, .bottom = padding, .left = padding, .right = padding };
|
||||
}
|
||||
|
||||
/// Create a padding with equivalent padding in the left and right directions; others directions remain the default value.
|
||||
pub fn horizontal(padding: u16) @This() {
|
||||
pub fn horizontal(padding: i16) @This() {
|
||||
return .{ .left = padding, .right = padding };
|
||||
}
|
||||
|
||||
/// Create a padding with equivalent padding in the top and bottom directions; others directions remain the default value.
|
||||
pub fn vertical(padding: u16) @This() {
|
||||
pub fn vertical(padding: i16) @This() {
|
||||
return .{ .top = padding, .bottom = padding };
|
||||
}
|
||||
} = .{},
|
||||
@@ -341,14 +365,19 @@ pub const Layout = packed struct {
|
||||
} = .line,
|
||||
} = .{},
|
||||
|
||||
/// Calculate the absolute offset for the provided `padding` if it is negative to get the absolute padding for the given `size`.
|
||||
pub fn getAbsolutePadding(padding: i16, size: u16) u16 {
|
||||
return if (padding >= 0) @intCast(padding) else size -| @as(u16, @intCast(-padding));
|
||||
}
|
||||
|
||||
pub fn content(this: @This(), comptime C: type, cells: []Cell, origin: Point, size: Point, children: []const C) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
if (this.separator.enabled and children.len > 1) {
|
||||
const line_cps: [2]u21 = switch (this.separator.line) {
|
||||
.line => line,
|
||||
.dotted => dotted,
|
||||
.double => double,
|
||||
.line => .{ '│', '─' },
|
||||
.dotted => .{ '┆', '┄' },
|
||||
.double => .{ '║', '═' },
|
||||
};
|
||||
const gap: u16 = (this.gap + 1) / 2;
|
||||
|
||||
@@ -371,8 +400,11 @@ pub const Layout = packed struct {
|
||||
}
|
||||
|
||||
// DEBUG render corresponding beginning of the separator for this `Container` *red*
|
||||
// cells[anchor].style.fg = .red;
|
||||
// cells[anchor].style.bg = .red;
|
||||
if (comptime build_options.debug) {
|
||||
cells[anchor].style.fg = .red;
|
||||
cells[anchor].style.bg = .black;
|
||||
cells[anchor].cp = 's'; // 's' for *separator*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,8 +412,9 @@ 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(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
@@ -395,14 +428,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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 = .{
|
||||
.padding = .all(1),
|
||||
.separator = .{
|
||||
@@ -417,14 +451,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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,
|
||||
.separator = .{
|
||||
@@ -441,14 +476,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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)" {
|
||||
const event = @import("event.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 = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -468,14 +504,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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))" {
|
||||
const event = @import("event.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 = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -496,14 +533,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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 = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -524,14 +562,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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 = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -553,7 +592,7 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -568,16 +607,17 @@ pub const Size = packed struct {
|
||||
} = .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)`");
|
||||
|
||||
const Element = @import("element.zig").Element(Event);
|
||||
const Element = @import("element.zig").Element(Model, Event);
|
||||
return struct {
|
||||
allocator: std.mem.Allocator,
|
||||
allocator: Allocator,
|
||||
origin: Point,
|
||||
size: Point,
|
||||
properties: Properties,
|
||||
element: Element,
|
||||
// TODO this should be renamed to `children`
|
||||
elements: std.ArrayList(@This()),
|
||||
|
||||
/// Properties for each `Container` to configure their layout,
|
||||
@@ -591,7 +631,7 @@ pub fn Container(comptime Event: type) type {
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
allocator: Allocator,
|
||||
properties: Properties,
|
||||
element: Element,
|
||||
) !@This() {
|
||||
@@ -601,19 +641,17 @@ pub fn Container(comptime Event: type) type {
|
||||
.size = .{},
|
||||
.properties = properties,
|
||||
.element = element,
|
||||
.elements = std.ArrayList(@This()).init(allocator),
|
||||
.elements = try std.ArrayList(@This()).initCapacity(allocator, 2),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
for (this.elements.items) |*element| {
|
||||
element.deinit();
|
||||
}
|
||||
this.elements.deinit();
|
||||
for (this.elements.items) |*element| element.deinit();
|
||||
this.elements.deinit(this.allocator);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -622,8 +660,8 @@ pub fn Container(comptime Event: type) type {
|
||||
this.element.reposition(origin);
|
||||
|
||||
var offset = origin.add(.{
|
||||
.x = layout.padding.left,
|
||||
.y = layout.padding.top,
|
||||
.x = Layout.getAbsolutePadding(layout.padding.left, this.size.x),
|
||||
.y = Layout.getAbsolutePadding(layout.padding.top, this.size.y),
|
||||
});
|
||||
|
||||
const sides = this.properties.border.sides;
|
||||
@@ -655,8 +693,8 @@ pub fn Container(comptime Event: type) type {
|
||||
};
|
||||
|
||||
if (this.elements.items.len > 0) switch (layout.direction) {
|
||||
.horizontal => size.x += layout.padding.left + layout.padding.right,
|
||||
.vertical => size.y += layout.padding.top + layout.padding.bottom,
|
||||
.horizontal => size.x += Layout.getAbsolutePadding(layout.padding.left, this.size.x) + Layout.getAbsolutePadding(layout.padding.right, this.size.x),
|
||||
.vertical => size.y += Layout.getAbsolutePadding(layout.padding.top, this.size.y) + Layout.getAbsolutePadding(layout.padding.bottom, this.size.y),
|
||||
};
|
||||
|
||||
const sides = this.properties.border.sides;
|
||||
@@ -697,25 +735,23 @@ pub fn Container(comptime Event: type) type {
|
||||
.y = @max(size.y, this.properties.size.dim.y),
|
||||
},
|
||||
};
|
||||
log.debug("fit_size returning: {any}", .{this.size});
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/// growable implicitly requires the root `Container` to have a set a size property to the size of the available terminal screen
|
||||
fn grow_resize(this: *@This(), max_size: Point) void {
|
||||
log.debug("grow_size: {any}", .{this.size});
|
||||
const layout = this.properties.layout;
|
||||
var remainder = switch (layout.direction) {
|
||||
.horizontal => max_size.x -| (layout.padding.left + layout.padding.right),
|
||||
.vertical => max_size.y -| (layout.padding.top + layout.padding.bottom),
|
||||
.horizontal => max_size.x -| (Layout.getAbsolutePadding(layout.padding.left, this.size.x) + Layout.getAbsolutePadding(layout.padding.right, this.size.x)),
|
||||
.vertical => max_size.y -| (Layout.getAbsolutePadding(layout.padding.top, this.size.y) + Layout.getAbsolutePadding(layout.padding.bottom, this.size.y)),
|
||||
};
|
||||
remainder -|= layout.gap * @as(u16, @truncate(this.elements.items.len -| 1));
|
||||
|
||||
if (layout.separator.enabled) remainder -|= @as(u16, @truncate(this.elements.items.len -| 1));
|
||||
|
||||
var available = switch (layout.direction) {
|
||||
.horizontal => max_size.y -| (layout.padding.top + layout.padding.bottom),
|
||||
.vertical => max_size.x -| (layout.padding.left + layout.padding.right),
|
||||
.horizontal => max_size.y -| (Layout.getAbsolutePadding(layout.padding.top, this.size.y) + Layout.getAbsolutePadding(layout.padding.bottom, this.size.y)),
|
||||
.vertical => max_size.x -| (Layout.getAbsolutePadding(layout.padding.left, this.size.x) + Layout.getAbsolutePadding(layout.padding.right, this.size.x)),
|
||||
};
|
||||
|
||||
const sides = this.properties.border.sides;
|
||||
@@ -788,6 +824,11 @@ pub fn Container(comptime Event: type) type {
|
||||
for (this.elements.items) |child| {
|
||||
if (child.properties.size.grow == .fixed) continue;
|
||||
|
||||
switch (layout.direction) {
|
||||
.horizontal => if (child.properties.size.grow == .vertical) continue,
|
||||
.vertical => if (child.properties.size.grow == .horizontal) continue,
|
||||
}
|
||||
|
||||
const size = switch (layout.direction) {
|
||||
.horizontal => child.size.x,
|
||||
.vertical => child.size.y,
|
||||
@@ -804,10 +845,8 @@ pub fn Container(comptime Event: type) type {
|
||||
|
||||
size_to_correct = @min(size_to_correct, remainder / growable_children);
|
||||
var overflow: u16 = 0;
|
||||
if (size_to_correct == 0 and remainder > 0) {
|
||||
// there is some overflow
|
||||
overflow = remainder;
|
||||
}
|
||||
if (size_to_correct == 0 and remainder > 0) overflow = remainder;
|
||||
|
||||
for (this.elements.items) |*child| {
|
||||
const child_size = switch (layout.direction) {
|
||||
.horizontal => child.size.x,
|
||||
@@ -817,9 +856,11 @@ pub fn Container(comptime Event: type) type {
|
||||
switch (layout.direction) {
|
||||
.horizontal => if (child.properties.size.grow != .vertical) {
|
||||
child.size.x += size_to_correct;
|
||||
remainder -|= size_to_correct;
|
||||
},
|
||||
.vertical => if (child.properties.size.grow != .horizontal) {
|
||||
child.size.y += size_to_correct;
|
||||
remainder -|= size_to_correct;
|
||||
},
|
||||
}
|
||||
if (overflow > 0) {
|
||||
@@ -836,7 +877,6 @@ pub fn Container(comptime Event: type) type {
|
||||
},
|
||||
}
|
||||
}
|
||||
remainder -|= size_to_correct;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,6 +887,7 @@ pub fn Container(comptime Event: type) type {
|
||||
|
||||
pub fn resize(this: *@This(), size: Point) void {
|
||||
// NOTE assume that this function is only called for the root `Container`
|
||||
this.size = size;
|
||||
const fit_size = this.fit_resize();
|
||||
// if (fit_size.y > size.y or fit_size.x > size.x) @panic("error: cannot render in available space");
|
||||
switch (this.properties.size.grow) {
|
||||
@@ -864,51 +905,79 @@ pub fn Container(comptime Event: type) type {
|
||||
this.grow_resize(this.size);
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !void {
|
||||
pub fn handle(this: *const @This(), model: *Model, event: Event) !void {
|
||||
switch (event) {
|
||||
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
|
||||
try this.element.handle(event);
|
||||
for (this.elements.items) |*element| try element.handle(event);
|
||||
},
|
||||
else => {
|
||||
try this.element.handle(event);
|
||||
for (this.elements.items) |*element| try element.handle(event);
|
||||
// the element receives the mouse event with relative position
|
||||
assert(mouse.x >= this.origin.x and mouse.y >= this.origin.y);
|
||||
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 });
|
||||
},
|
||||
else => try this.element.handle(model, event),
|
||||
}
|
||||
for (this.elements.items) |*element| try element.handle(model, event);
|
||||
}
|
||||
|
||||
pub fn content(this: *const @This()) ![]const Cell {
|
||||
pub fn content(this: *const @This(), model: *const Model) ![]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));
|
||||
@memset(cells, .{});
|
||||
errdefer this.allocator.free(cells);
|
||||
@memset(cells, .{});
|
||||
|
||||
this.properties.layout.content(@This(), cells, this.origin, this.size, this.elements.items);
|
||||
this.properties.border.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 top left corner of this `Container` *red*
|
||||
// cells[0].style.fg = .red;
|
||||
// cells[0].style.bg = .red;
|
||||
// DEBUG render corresponding corners (except top left) of this `Container` *red*
|
||||
if (comptime build_options.debug) {
|
||||
// top right
|
||||
cells[this.size.x -| 1].style.fg = .red;
|
||||
cells[this.size.x -| 1].style.bg = .black;
|
||||
cells[this.size.x -| 1].cp = 'c'; // 'c' for *container*
|
||||
// bottom left
|
||||
cells[this.size.x * (this.size.y -| 1)].style.fg = .red;
|
||||
cells[this.size.x * (this.size.y -| 1)].style.bg = .black;
|
||||
cells[this.size.x * (this.size.y -| 1)].cp = 'c'; // 'c' for *container*
|
||||
// bottom right
|
||||
cells[this.size.x * this.size.y -| 1].style.fg = .red;
|
||||
cells[this.size.x * this.size.y -| 1].style.bg = .black;
|
||||
cells[this.size.x * this.size.y -| 1].cp = 'c'; // 'c' for *container*
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const log = std.log.scoped(.container);
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_options = @import("build_options");
|
||||
const input = @import("input.zig");
|
||||
const isTaggedUnion = @import("event.zig").isTaggedUnion;
|
||||
const Cell = @import("cell.zig");
|
||||
const Color = @import("color.zig").Color;
|
||||
const Point = @import("point.zig").Point;
|
||||
const Style = @import("style.zig");
|
||||
const Error = @import("error.zig").Error;
|
||||
|
||||
test {
|
||||
_ = Border;
|
||||
_ = Layout;
|
||||
_ = Rectangle;
|
||||
@import("std").testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
||||
test "Container Fixed and Grow Size Vertical" {
|
||||
const event = @import("event.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 },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
@@ -926,14 +995,15 @@ test "Container Fixed and Grow Size Vertical" {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.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" {
|
||||
const event = @import("event.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, .{
|
||||
.size = .{
|
||||
.dim = .{ .x = 5 },
|
||||
@@ -949,5 +1019,5 @@ test "Container Fixed and Grow Size Horizontal" {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/fixed_grow_horizontal.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_horizontal.zon"));
|
||||
}
|
||||
|
||||
1495
src/element.zig
1495
src/element.zig
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,5 @@
|
||||
//! Events which are defined by the library. They might be extended by user
|
||||
//! events. See `App` for more details about user defined events.
|
||||
const std = @import("std");
|
||||
const input = @import("input.zig");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Key = input.Key;
|
||||
const Mouse = input.Mouse;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
/// System events available to every `zterm.App`
|
||||
pub const SystemEvent = union(enum) {
|
||||
@@ -15,8 +8,11 @@ pub const SystemEvent = union(enum) {
|
||||
init,
|
||||
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
||||
quit,
|
||||
/// Resize event to signify that the application should re-draw to resize
|
||||
resize,
|
||||
/// Error event to notify other containers about a recoverable error
|
||||
err: struct {
|
||||
/// actual error
|
||||
err: anyerror,
|
||||
/// associated error message
|
||||
msg: []const u8,
|
||||
@@ -25,15 +21,19 @@ pub const SystemEvent = union(enum) {
|
||||
key: Key,
|
||||
/// Mouse input event
|
||||
mouse: Mouse,
|
||||
/// 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 event indicating that the application has gained the focus of the user
|
||||
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 {
|
||||
if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
|
||||
@compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
|
||||
}
|
||||
// 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)`.");
|
||||
|
||||
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,11 +58,27 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
||||
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 = .{
|
||||
.signedness = .unsigned,
|
||||
.bits = log2_i,
|
||||
.bits = @bitSizeOf(@TypeOf(i)) - @clz(i),
|
||||
} });
|
||||
|
||||
const Event = @Type(.{ .@"enum" = .{
|
||||
@@ -80,15 +96,28 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
||||
} });
|
||||
}
|
||||
|
||||
// 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 {
|
||||
/// 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;
|
||||
}
|
||||
},
|
||||
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");
|
||||
const Key = input.Key;
|
||||
const Mouse = input.Mouse;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
|
||||
const std = @import("std");
|
||||
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub const Mouse = packed struct {
|
||||
x: u16,
|
||||
@@ -32,7 +29,7 @@ pub const Mouse = packed struct {
|
||||
};
|
||||
|
||||
pub fn eql(this: @This(), other: @This()) bool {
|
||||
return std.meta.eql(this, other);
|
||||
return meta.eql(this, other);
|
||||
}
|
||||
|
||||
pub fn in(this: @This(), origin: Point, size: Point) bool {
|
||||
@@ -65,9 +62,11 @@ pub const Key = packed struct {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn eql(this: @This(), other: @This()) bool {
|
||||
return std.meta.eql(this, other);
|
||||
return meta.eql(this, other);
|
||||
}
|
||||
|
||||
// TODO might be useful to use the std.ascii stuff!
|
||||
|
||||
/// Determine if the `Key` is an ascii character that can be printed to
|
||||
/// the screen. This means that the code point of the `Key` is an ascii
|
||||
/// character between 32 - 255 (with the exception of 127 = Delete) and no
|
||||
@@ -90,24 +89,25 @@ pub const Key = packed struct {
|
||||
}
|
||||
|
||||
test "isAscii with ascii character" {
|
||||
try std.testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
|
||||
try testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
|
||||
}
|
||||
|
||||
test "isAscii with non-ascii character" {
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Escape }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = Escape }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = Enter }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
|
||||
}
|
||||
|
||||
test "isAscii with excluded input.Delete" {
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = Delete }));
|
||||
try testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: std.ascii has the escape codes too!
|
||||
// codepoints for keys
|
||||
pub const Tab: u21 = 0x09;
|
||||
pub const Enter: u21 = 0x0D;
|
||||
@@ -152,21 +152,6 @@ pub const F17: u21 = 57380;
|
||||
pub const F18: u21 = 57381;
|
||||
pub const F19: u21 = 57382;
|
||||
pub const F20: u21 = 57383;
|
||||
pub const F21: u21 = 57384;
|
||||
pub const F22: u21 = 57385;
|
||||
pub const F23: u21 = 57386;
|
||||
pub const F24: u21 = 57387;
|
||||
pub const F25: u21 = 57388;
|
||||
pub const F26: u21 = 57389;
|
||||
pub const F27: u21 = 57390;
|
||||
pub const F28: u21 = 57391;
|
||||
pub const F29: u21 = 57392;
|
||||
pub const F30: u21 = 57393;
|
||||
pub const F31: u21 = 57394;
|
||||
pub const F32: u21 = 57395;
|
||||
pub const F33: u21 = 57396;
|
||||
pub const F34: u21 = 57397;
|
||||
pub const F35: u21 = 57398;
|
||||
pub const Kp0: u21 = 57399;
|
||||
pub const Kp1: u21 = 57400;
|
||||
pub const Kp2: u21 = 57401;
|
||||
@@ -223,3 +208,8 @@ pub const RightHyper: u21 = 57451;
|
||||
pub const RightMeta: u21 = 57452;
|
||||
pub const IsoLevel3Shift: u21 = 57453;
|
||||
pub const IsoLevel5Shift: u21 = 57454;
|
||||
|
||||
const std = @import("std");
|
||||
const meta = std.meta;
|
||||
const Point = @import("point.zig").Point;
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License)
|
||||
// with slight modifications
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
/// Thread safe. Fixed size. Blocking push and pop.
|
||||
/// Queue implementation. Thread safe. Fixed size. Blocking push and pop. Polling through tryPop and tryPush.
|
||||
pub fn Queue(comptime T: type, comptime size: usize) type {
|
||||
return struct {
|
||||
buf: [size]T = undefined,
|
||||
@@ -92,6 +90,23 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
||||
assert(!this.isEmptyLH());
|
||||
}
|
||||
|
||||
pub fn lock(this: *QueueType) void {
|
||||
this.mutex.lock();
|
||||
}
|
||||
|
||||
pub fn unlock(this: *QueueType) void {
|
||||
this.mutex.unlock();
|
||||
}
|
||||
|
||||
/// Used to efficiently drain the queue
|
||||
pub fn drain(this: *QueueType) ?T {
|
||||
if (this.isEmptyLH()) return null;
|
||||
|
||||
const result = this.buf[this.mask(this.read_index)];
|
||||
this.read_index = this.mask2(this.read_index + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn isEmptyLH(this: QueueType) bool {
|
||||
return this.write_index == this.read_index;
|
||||
}
|
||||
@@ -116,7 +131,7 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
||||
}
|
||||
|
||||
/// Returns the length
|
||||
fn len(this: QueueType) usize {
|
||||
pub fn len(this: QueueType) usize {
|
||||
const wrap_offset = 2 * this.buf.len *
|
||||
@intFromBool(this.write_index < this.read_index);
|
||||
const adjusted_write_index = this.write_index + wrap_offset;
|
||||
@@ -135,8 +150,12 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
||||
};
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const assert = std.debug.assert;
|
||||
const Thread = std.Thread;
|
||||
const cfg = Thread.SpawnConfig{ .allocator = testing.allocator };
|
||||
|
||||
test "Queue: simple push / pop" {
|
||||
var queue: Queue(u8, 16) = .{};
|
||||
queue.push(1);
|
||||
@@ -146,7 +165,6 @@ test "Queue: simple push / pop" {
|
||||
try testing.expectEqual(2, queue.pop());
|
||||
}
|
||||
|
||||
const Thread = std.Thread;
|
||||
fn testPushPop(q: *Queue(u8, 2)) !void {
|
||||
q.push(3);
|
||||
try testing.expectEqual(2, q.pop());
|
||||
@@ -197,9 +215,9 @@ 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.time.sleep(std.time.ns_per_s);
|
||||
std.Thread.sleep(std.time.ns_per_s);
|
||||
// Finally, let that other thread go.
|
||||
try std.testing.expectEqual(1, q.pop());
|
||||
try testing.expectEqual(1, q.pop());
|
||||
|
||||
// This won't continue until the other thread has had a chance to
|
||||
// put at least one item in the queue.
|
||||
@@ -207,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.time.sleep(std.time.ns_per_s / 2);
|
||||
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||
|
||||
// Another spurious wake...
|
||||
q.not_full.signal();
|
||||
@@ -215,10 +233,10 @@ 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.time.sleep(std.time.ns_per_s / 2);
|
||||
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||
|
||||
// Pop that thing and we're done.
|
||||
try std.testing.expectEqual(2, q.pop());
|
||||
try testing.expectEqual(2, q.pop());
|
||||
}
|
||||
|
||||
test "Fill, block, fill, block" {
|
||||
@@ -238,28 +256,28 @@ test "Fill, block, fill, block" {
|
||||
|
||||
// Just to make sure the sleeps are yielding to this thread, make
|
||||
// sure it took at least 900ms to do the push.
|
||||
try std.testing.expect(then - now > 900);
|
||||
try testing.expect(then - now > 900);
|
||||
|
||||
// This should block again, waiting for the other thread.
|
||||
queue.push(4);
|
||||
|
||||
// And once that push has gone through, the other thread's done.
|
||||
thread.join();
|
||||
try std.testing.expectEqual(3, queue.pop());
|
||||
try std.testing.expectEqual(4, queue.pop());
|
||||
try testing.expectEqual(3, queue.pop());
|
||||
try testing.expectEqual(4, queue.pop());
|
||||
}
|
||||
|
||||
fn sleepyPush(q: *Queue(u8, 1)) !void {
|
||||
// Try to ensure the other thread has already started trying to pop.
|
||||
try Thread.yield();
|
||||
std.time.sleep(std.time.ns_per_s / 2);
|
||||
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||
|
||||
// Spurious wake
|
||||
q.not_full.signal();
|
||||
q.not_empty.signal();
|
||||
|
||||
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.
|
||||
q.push(1);
|
||||
@@ -268,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.time.sleep(std.time.ns_per_s / 2);
|
||||
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||
|
||||
// Spurious wake
|
||||
q.not_full.signal();
|
||||
@@ -284,8 +302,8 @@ test "Drain, block, drain, block" {
|
||||
|
||||
var queue: Queue(u8, 1) = .{};
|
||||
const thread = try Thread.spawn(cfg, sleepyPush, .{&queue});
|
||||
try std.testing.expectEqual(1, queue.pop());
|
||||
try std.testing.expectEqual(2, queue.pop());
|
||||
try testing.expectEqual(1, queue.pop());
|
||||
try testing.expectEqual(2, queue.pop());
|
||||
thread.join();
|
||||
}
|
||||
|
||||
@@ -299,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.time.sleep(std.time.ns_per_s / 2);
|
||||
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||
queue.push(1);
|
||||
queue.push(1);
|
||||
t1.join();
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Cell = @import("cell.zig");
|
||||
const Point = @import("point.zig").Point;
|
||||
//! Renderer for `zterm`.
|
||||
|
||||
/// Double-buffered intermediate rendering pipeline
|
||||
pub const Buffered = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
allocator: Allocator,
|
||||
created: bool,
|
||||
size: Point,
|
||||
screen: []Cell,
|
||||
virtual_screen: []Cell,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) @This() {
|
||||
pub fn init(allocator: Allocator) @This() {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.created = false,
|
||||
@@ -29,9 +25,9 @@ pub const Buffered = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(this: *@This()) !void {
|
||||
pub fn resize(this: *@This()) !Point {
|
||||
const size = terminal.getTerminalSize();
|
||||
if (std.meta.eql(this.size, size)) return;
|
||||
if (meta.eql(this.size, size)) return this.size;
|
||||
|
||||
this.size = size;
|
||||
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
|
||||
@@ -52,6 +48,7 @@ pub const Buffered = struct {
|
||||
@memset(this.virtual_screen, .{});
|
||||
}
|
||||
try this.clear();
|
||||
return size;
|
||||
}
|
||||
|
||||
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
|
||||
@@ -61,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 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 origin: Point = container.origin;
|
||||
const cells: []const Cell = try container.content();
|
||||
const cells: []const Cell = try container.content(model);
|
||||
|
||||
if (cells.len == 0) return;
|
||||
|
||||
@@ -83,13 +80,15 @@ pub const Buffered = struct {
|
||||
// free immediately
|
||||
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).
|
||||
pub fn flush(this: *@This()) !void {
|
||||
try terminal.hideCursor();
|
||||
// TODO measure timings of rendered frames?
|
||||
const writer = terminal.writer();
|
||||
var cursor_position: ?Point = null;
|
||||
var writer = terminal.writer();
|
||||
const s = this.screen;
|
||||
const vs = this.virtual_screen;
|
||||
for (0..this.size.y) |row| {
|
||||
@@ -97,14 +96,36 @@ pub const Buffered = struct {
|
||||
const idx = (row * this.size.x) + col;
|
||||
const cs = s[idx];
|
||||
const cvs = vs[idx];
|
||||
|
||||
// update the latest found cursor position
|
||||
if (cvs.style.cursor) {
|
||||
assert(cursor_position == null);
|
||||
cursor_position = .{
|
||||
.x = @truncate(col),
|
||||
.y = @truncate(row),
|
||||
};
|
||||
}
|
||||
|
||||
if (cs.eql(cvs)) continue;
|
||||
|
||||
// render differences found in virtual screen
|
||||
try terminal.setCursorPosition(.{ .y = @truncate(row + 1), .x = @truncate(col + 1) });
|
||||
try cvs.value(writer);
|
||||
try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) });
|
||||
try cvs.value(&writer);
|
||||
// update screen to be the virtual screen for the next frame
|
||||
s[idx] = vs[idx];
|
||||
}
|
||||
}
|
||||
if (cursor_position) |point| {
|
||||
try terminal.showCursor();
|
||||
try terminal.setCursorPosition(point);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
const meta = std.meta;
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const terminal = @import("terminal.zig");
|
||||
const Cell = @import("cell.zig");
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
@@ -17,7 +17,6 @@ pub const Renderer = @import("render.zig");
|
||||
// Container Configurations
|
||||
pub const Border = container.Border;
|
||||
pub const Rectangle = container.Rectangle;
|
||||
pub const Scroll = container.Scroll;
|
||||
pub const Layout = container.Layout;
|
||||
|
||||
pub const Cell = @import("cell.zig");
|
||||
@@ -7,11 +7,13 @@
|
||||
|
||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
|
||||
// with slight modifications
|
||||
const std = @import("std");
|
||||
|
||||
const Color = @import("color.zig").Color;
|
||||
|
||||
pub const Style = @This();
|
||||
fg: Color = .default,
|
||||
bg: Color = .default,
|
||||
ul: Color = .default,
|
||||
cursor: bool = false,
|
||||
ul_style: Underline = .off,
|
||||
emphasis: []const Emphasis,
|
||||
|
||||
pub const Underline = enum {
|
||||
off,
|
||||
@@ -34,39 +36,42 @@ pub const Emphasis = enum(u8) {
|
||||
strikethrough,
|
||||
};
|
||||
|
||||
fg: Color = .default,
|
||||
bg: Color = .default,
|
||||
ul: Color = .default,
|
||||
ul_style: Underline = .off,
|
||||
emphasis: []const Emphasis,
|
||||
|
||||
pub fn eql(this: Style, other: Style) bool {
|
||||
return std.meta.eql(this, other);
|
||||
return meta.eql(this, other);
|
||||
}
|
||||
|
||||
pub fn value(this: Style, writer: anytype, cp: u21) !void {
|
||||
// TODO might be useful to use the std.ascii stuff!
|
||||
|
||||
pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void {
|
||||
var buffer: [4]u8 = undefined;
|
||||
const bytes = try std.unicode.utf8Encode(cp, &buffer);
|
||||
std.debug.assert(bytes > 0);
|
||||
const bytes = try unicode.utf8Encode(cp, &buffer);
|
||||
assert(bytes > 0);
|
||||
// build ansi sequence for 256 colors ...
|
||||
// foreground
|
||||
try std.fmt.format(writer, "\x1b[", .{});
|
||||
try writer.printAscii("\x1b[", .{});
|
||||
try this.fg.write(writer, .fg);
|
||||
// background
|
||||
try std.fmt.format(writer, ";", .{});
|
||||
try writer.printAsciiChar(';', .{});
|
||||
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 std.fmt.format(writer, ";", .{});
|
||||
try writer.printAsciiChar(';', .{});
|
||||
try this.ul.write(writer, .ul);
|
||||
// append styles (aka attributes like bold, italic, strikethrough, etc.)
|
||||
for (this.emphasis) |attribute| try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)});
|
||||
try std.fmt.format(writer, "m", .{});
|
||||
for (this.emphasis) |attribute| try writer.print(";{d}", .{@intFromEnum(attribute)});
|
||||
try writer.printAsciiChar('m', .{});
|
||||
// content
|
||||
try std.fmt.format(writer, "{s}", .{buffer[0..bytes]});
|
||||
try std.fmt.format(writer, "\x1b[0m", .{});
|
||||
try writer.printAscii(buffer[0..bytes], .{});
|
||||
try writer.printAscii("\x1b[0m", .{});
|
||||
}
|
||||
|
||||
// TODO implement helper functions for terminal capabilities:
|
||||
// - links / url display (osc 8)
|
||||
// - show / hide cursor?
|
||||
|
||||
const std = @import("std");
|
||||
const unicode = std.unicode;
|
||||
const meta = std.meta;
|
||||
const assert = std.debug.assert;
|
||||
const Color = @import("color.zig").Color;
|
||||
const Style = @This();
|
||||
|
||||
113
src/terminal.zig
113
src/terminal.zig
@@ -1,15 +1,3 @@
|
||||
const std = @import("std");
|
||||
const code_point = @import("code_point");
|
||||
const ctlseqs = @import("ctlseqs.zig");
|
||||
const input = @import("input.zig");
|
||||
|
||||
const Key = input.Key;
|
||||
const Point = @import("point.zig").Point;
|
||||
const Size = @import("point.zig").Point;
|
||||
const Cell = @import("cell.zig");
|
||||
|
||||
const log = std.log.scoped(.terminal);
|
||||
|
||||
// Ref: https://vt100.net/docs/vt510-rm/DECRPM.html
|
||||
pub const ReportMode = enum {
|
||||
not_recognized,
|
||||
@@ -21,89 +9,95 @@ pub const ReportMode = enum {
|
||||
|
||||
/// Gets number of rows and columns in the terminal
|
||||
pub fn getTerminalSize() Size {
|
||||
var ws: std.posix.winsize = undefined;
|
||||
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
|
||||
var ws: posix.winsize = undefined;
|
||||
_ = posix.system.ioctl(posix.STDIN_FILENO, posix.T.IOCGWINSZ, @intFromPtr(&ws));
|
||||
return .{ .x = ws.col, .y = ws.row };
|
||||
}
|
||||
|
||||
pub fn saveScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.save_screen);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.save_screen);
|
||||
}
|
||||
|
||||
pub fn restoreScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.restore_screen);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.restore_screen);
|
||||
}
|
||||
|
||||
pub fn enterAltScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.smcup);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.smcup);
|
||||
}
|
||||
|
||||
pub fn exitAltScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.rmcup);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.rmcup);
|
||||
}
|
||||
|
||||
pub fn clearScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.clear_screen);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.clear_screen);
|
||||
}
|
||||
|
||||
pub fn hideCursor() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.hide_cursor);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.hide_cursor);
|
||||
}
|
||||
|
||||
pub fn showCursor() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.show_cursor);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor);
|
||||
}
|
||||
|
||||
pub fn setCursorPositionHome() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.home);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home);
|
||||
}
|
||||
|
||||
pub fn enableMouseSupport() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_set);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_set);
|
||||
}
|
||||
|
||||
pub fn disableMouseSupport() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_reset);
|
||||
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_reset);
|
||||
}
|
||||
|
||||
pub fn read(buf: []u8) !usize {
|
||||
return try std.posix.read(std.posix.STDIN_FILENO, buf);
|
||||
return try posix.read(posix.STDIN_FILENO, buf);
|
||||
}
|
||||
|
||||
pub fn write(buf: []const u8) !usize {
|
||||
return try std.posix.write(std.posix.STDIN_FILENO, buf);
|
||||
return try posix.write(posix.STDIN_FILENO, buf);
|
||||
}
|
||||
|
||||
fn contextWrite(context: @This(), data: []const u8) anyerror!usize {
|
||||
_ = context;
|
||||
return try std.posix.write(std.posix.STDOUT_FILENO, data);
|
||||
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;
|
||||
}
|
||||
|
||||
const Writer = std.io.Writer(
|
||||
@This(),
|
||||
anyerror,
|
||||
contextWrite,
|
||||
);
|
||||
|
||||
pub fn writer() Writer {
|
||||
return .{ .context = .{} };
|
||||
// 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 = &.{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setCursorPosition(pos: Point) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y, pos.x });
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
|
||||
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y + 1, pos.x + 1 });
|
||||
_ = try posix.write(posix.STDIN_FILENO, value);
|
||||
}
|
||||
|
||||
pub fn getCursorPosition() !Size.Position {
|
||||
// Needs Raw mode (no wait for \n) to work properly cause
|
||||
// control sequence will not be written without it.
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n");
|
||||
_ = try posix.write(posix.STDIN_FILENO, "\x1b[6n");
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
|
||||
// format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R"
|
||||
const len = try std.posix.read(std.posix.STDIN_FILENO, &buf);
|
||||
const len = try posix.read(posix.STDIN_FILENO, &buf);
|
||||
|
||||
if (!isCursorPosition(buf[0..len])) {
|
||||
return error.InvalidValueReturned;
|
||||
@@ -166,8 +160,8 @@ pub fn isCursorPosition(buf: []u8) bool {
|
||||
///
|
||||
/// `bak`: pointer to store termios struct backup before
|
||||
/// altering, this is used to disable raw mode.
|
||||
pub fn enableRawMode(bak: *std.posix.termios) !void {
|
||||
var termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO);
|
||||
pub fn enableRawMode(bak: *posix.termios) !void {
|
||||
var termios = try posix.tcgetattr(posix.STDIN_FILENO);
|
||||
bak.* = termios;
|
||||
|
||||
// termios flags used by termios(3)
|
||||
@@ -192,20 +186,20 @@ pub fn enableRawMode(bak: *std.posix.termios) !void {
|
||||
termios.cflag.CSIZE = .CS8;
|
||||
termios.cflag.PARENB = false;
|
||||
|
||||
termios.cc[@intFromEnum(std.posix.V.MIN)] = 1;
|
||||
termios.cc[@intFromEnum(std.posix.V.TIME)] = 0;
|
||||
termios.cc[@intFromEnum(posix.V.MIN)] = 1;
|
||||
termios.cc[@intFromEnum(posix.V.TIME)] = 0;
|
||||
|
||||
try std.posix.tcsetattr(
|
||||
std.posix.STDIN_FILENO,
|
||||
try posix.tcsetattr(
|
||||
posix.STDIN_FILENO,
|
||||
.FLUSH,
|
||||
termios,
|
||||
);
|
||||
}
|
||||
|
||||
/// Reverts `enableRawMode` to restore initial functionality.
|
||||
pub fn disableRawMode(bak: *std.posix.termios) !void {
|
||||
try std.posix.tcsetattr(
|
||||
std.posix.STDIN_FILENO,
|
||||
pub fn disableRawMode(bak: *const posix.termios) !void {
|
||||
try posix.tcsetattr(
|
||||
posix.STDIN_FILENO,
|
||||
.FLUSH,
|
||||
bak.*,
|
||||
);
|
||||
@@ -215,13 +209,13 @@ pub fn disableRawMode(bak: *std.posix.termios) !void {
|
||||
pub fn canSynchornizeOutput() !bool {
|
||||
// Needs Raw mode (no wait for \n) to work properly cause
|
||||
// control sequence will not be written without it.
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?2026$p");
|
||||
_ = try posix.write(posix.STDIN_FILENO, "\x1b[?2026$p");
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
|
||||
// format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y"
|
||||
const len = try std.posix.read(std.posix.STDIN_FILENO, &buf);
|
||||
if (!std.mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) {
|
||||
const len = try posix.read(posix.STDIN_FILENO, &buf);
|
||||
if (!mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -238,3 +232,16 @@ fn getReportMode(ps: u8) ReportMode {
|
||||
else => ReportMode.not_recognized,
|
||||
};
|
||||
}
|
||||
|
||||
const log = std.log.scoped(.terminal);
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const posix = std.posix;
|
||||
const assert = std.debug.assert;
|
||||
const ctlseqs = @import("ctlseqs.zig");
|
||||
const input = @import("input.zig");
|
||||
const Key = input.Key;
|
||||
const Point = @import("point.zig").Point;
|
||||
const Size = @import("point.zig").Point;
|
||||
const Cell = @import("cell.zig");
|
||||
|
||||
1
src/test/element/alignment.bottom.zon
Normal file
1
src/test/element/alignment.bottom.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.center.zon
Normal file
1
src/test/element/alignment.center.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.left.zon
Normal file
1
src/test/element/alignment.left.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.right.zon
Normal file
1
src/test/element/alignment.right.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.top.zon
Normal file
1
src/test/element/alignment.top.zon
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.vertical.scrollbar.top.zon
Normal file
1
src/test/element/scrollable.vertical.scrollbar.top.zon
Normal file
File diff suppressed because one or more lines are too long
127
src/testing.zig
127
src/testing.zig
@@ -1,23 +1,12 @@
|
||||
//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
|
||||
const std = @import("std");
|
||||
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;
|
||||
|
||||
// TODO how would I describe the expected screens?
|
||||
// - including styling?
|
||||
// - compare generated strings instead? -> how would this be generated for the user?
|
||||
|
||||
/// Single-buffer test rendering pipeline for testing purposes.
|
||||
pub const Renderer = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
allocator: Allocator,
|
||||
size: Point,
|
||||
screen: []Cell,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, size: Point) @This() {
|
||||
pub fn init(allocator: Allocator, size: Point) @This() {
|
||||
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
|
||||
@memset(screen, .{});
|
||||
|
||||
@@ -45,10 +34,10 @@ pub const Renderer = struct {
|
||||
@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 origin: Point = container.origin;
|
||||
const cells: []const Cell = try container.content();
|
||||
const cells: []const Cell = try container.content(model);
|
||||
|
||||
if (cells.len == 0) return;
|
||||
|
||||
@@ -69,7 +58,7 @@ pub const Renderer = struct {
|
||||
// free immediately
|
||||
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 {
|
||||
@@ -85,6 +74,8 @@ 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();
|
||||
///
|
||||
@@ -92,8 +83,8 @@ pub const Renderer = struct {
|
||||
/// var renderer: testing.Renderer = .init(allocator, size);
|
||||
/// defer renderer.deinit();
|
||||
///
|
||||
/// try container.handle(.{ .size = size });
|
||||
/// try renderer.render(Container(event.SystemEvent), &container);
|
||||
/// try container.handle(&model, .{ .size = size });
|
||||
/// try renderer.render(@TypeOf(container), &container, Model, &.{});
|
||||
/// try renderer.save(file.writer());
|
||||
/// ```
|
||||
///
|
||||
@@ -102,7 +93,8 @@ pub const Renderer = struct {
|
||||
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
|
||||
///
|
||||
/// ```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 = .{
|
||||
/// .color = .green,
|
||||
/// .sides = .all,
|
||||
@@ -113,54 +105,81 @@ pub const Renderer = struct {
|
||||
/// try testing.expectContainerScreen(.{
|
||||
/// .rows = 20,
|
||||
/// .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 {
|
||||
const allocator = std.testing.allocator;
|
||||
pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, comptime Model: type, expected: []const Cell) !void {
|
||||
const allocator = testing.allocator;
|
||||
var renderer: Renderer = .init(allocator, size);
|
||||
defer renderer.deinit();
|
||||
|
||||
try renderer.resize(size);
|
||||
container.resize(size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(Container(event.SystemEvent), container);
|
||||
try renderer.render(T, container, Model, &.{});
|
||||
|
||||
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
|
||||
/// `zterm.testing.expectContainerScreen` for an example usage.
|
||||
pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actual: []const Cell) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
const allocator = testing.allocator;
|
||||
|
||||
try std.testing.expectEqual(expected.len, actual.len);
|
||||
try std.testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
|
||||
try testing.expectEqual(expected.len, actual.len);
|
||||
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();
|
||||
defer expected_cps.deinit(allocator);
|
||||
|
||||
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);
|
||||
defer output.deinit();
|
||||
var allocating_writer = std.Io.Writer.Allocating.init(allocator);
|
||||
defer allocating_writer.deinit();
|
||||
|
||||
var buffer = std.io.bufferedWriter(output.writer());
|
||||
defer buffer.flush() catch {};
|
||||
|
||||
const writer = buffer.writer();
|
||||
var writer = &allocating_writer.writer;
|
||||
var differ = false;
|
||||
|
||||
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, " ");
|
||||
const expected_centered = try center(allocator, "Expected Screen", size.x, " ");
|
||||
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);
|
||||
|
||||
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
|
||||
@@ -176,8 +195,8 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
|
||||
|
||||
if (!expected_cell.eql(actual_cell)) differ = true;
|
||||
|
||||
try expected_cps.append(expected_cell);
|
||||
try actual_cps.append(actual_cell);
|
||||
try expected_cps.append(allocator, expected_cell);
|
||||
try actual_cps.append(allocator, actual_cell);
|
||||
}
|
||||
|
||||
// write screens both formatted to buffer
|
||||
@@ -190,12 +209,22 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
|
||||
if (!differ) return;
|
||||
|
||||
// test failed
|
||||
try buffer.flush();
|
||||
debug.lockStdErr();
|
||||
defer debug.unlockStdErr();
|
||||
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
|
||||
const std_writer = std.io.getStdErr().writer();
|
||||
try std_writer.writeAll(output.items);
|
||||
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();
|
||||
return error.TestExpectEqualCells;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const debug = std.debug;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const event = @import("event.zig");
|
||||
const Container = @import("container.zig").Container;
|
||||
const Cell = @import("cell.zig");
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
Reference in New Issue
Block a user