1 Commits

Author SHA1 Message Date
yves-biener 89bc3eac0c add(example): WIP popup Element example implementation 2025-07-12 20:08:22 +02:00
36 changed files with 1639 additions and 2897 deletions
+2 -8
View File
@@ -15,15 +15,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup zig installation
uses: https://codeberg.org/mlugg/setup-zig@v2
uses: mlugg/setup-zig@v2
with:
version: latest
- name: Lint check
run: zig fmt --check .
- name: Spell checking
uses: crate-ci/typos@v1.39.0
with:
config: ./.typos-config
version: master
- name: Run tests
run: zig build --release=fast
- name: Release build artifacts
+3 -3
View File
@@ -14,13 +14,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup zig installation
uses: https://codeberg.org/mlugg/setup-zig@v2
uses: mlugg/setup-zig@v2
with:
version: latest
version: master
- name: Lint check
run: zig fmt --check --exclude src/test .
- name: Spell checking
uses: crate-ci/typos@v1.39.0
uses: crate-ci/typos@v1.25.0
with:
config: ./.typos-config
- name: Run tests
+6 -1
View File
@@ -2,6 +2,9 @@
`zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications.
> [!CAUTION]
> Only builds using the zig master version are tested to work.
## Demo
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
@@ -10,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.
@@ -30,6 +33,8 @@ const zterm: *Dependency = b.dependency("zterm", .{
.target = target,
.optimize = optimize,
});
// ...
exe.root_module.addImport("zterm", zterm.module("zterm"));
```
### Documentation
+15 -21
View File
@@ -10,10 +10,9 @@ pub fn build(b: *std.Build) void {
alignment,
button,
input,
popup,
progress,
radio_button,
scrollable,
selection,
// layouts:
vertical,
horizontal,
@@ -24,8 +23,6 @@ pub fn build(b: *std.Build) void {
palette,
// error handling
errors,
// non alternate screen applications
direct,
};
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
@@ -37,15 +34,20 @@ pub fn build(b: *std.Build) void {
options.addOption(bool, "debug", debug_rendering);
const options_module = options.createModule();
// dependencies
const zg = b.dependency("zg", .{
.target = target,
.optimize = optimize,
});
// library
const lib = b.addModule("zterm", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "build_options", .module = options_module },
},
});
lib.addImport("code_point", zg.module("code_point"));
lib.addImport("build_options", options_module);
//--- Examples ---
const examples = std.meta.fields(Examples);
@@ -53,7 +55,6 @@ pub fn build(b: *std.Build) void {
if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry
const demo = b.addExecutable(.{
.name = e.name,
.root_module = b.createModule(.{
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
.demo => "examples/demo.zig",
.continuous => "examples/continuous.zig",
@@ -61,10 +62,9 @@ pub fn build(b: *std.Build) void {
.alignment => "examples/elements/alignment.zig",
.button => "examples/elements/button.zig",
.input => "examples/elements/input.zig",
.popup => "examples/elements/popup.zig",
.progress => "examples/elements/progress.zig",
.radio_button => "examples/elements/radio-button.zig",
.scrollable => "examples/elements/scrollable.zig",
.selection => "examples/elements/selection.zig",
// layouts:
.vertical => "examples/layouts/vertical.zig",
.horizontal => "examples/layouts/horizontal.zig",
@@ -75,32 +75,26 @@ pub fn build(b: *std.Build) void {
.palette => "examples/styles/palette.zig",
// error handling
.errors => "examples/errors.zig",
// non-alternate screen
.direct => "examples/direct.zig",
.all => unreachable, // should never happen
}),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zterm", .module = lib },
},
}),
});
// import dependencies
demo.root_module.addImport("zterm", lib);
// mapping of user selected example to compile step
if (@intFromEnum(example) == e.value or example == .all) b.installArtifact(demo);
}
// zig build test
const lib_unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "build_options", .module = options_module },
},
}),
});
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
lib_unit_tests.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
lib_unit_tests.root_module.addImport("build_options", options_module);
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
+6 -1
View File
@@ -35,7 +35,12 @@
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{},
.dependencies = .{
.zg = .{
.url = "git+https://codeberg.org/atman/zg#9427a9e53aaa29ee071f4dcb35b809a699d75aa9",
.hash = "zg-0.14.1-oGqU3IQ_tALZIiBN026_NTaPJqU-Upm8P_C7QED2Rzm8",
},
},
.paths = .{
"LICENSE",
"build.zig",
+30 -36
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -41,7 +41,7 @@ const Spinner = struct {
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -57,20 +57,18 @@ const Spinner = struct {
};
const InputField = struct {
allocator: std.mem.Allocator,
input: std.ArrayList(u21),
queue: *App.Queue,
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
return .{
.allocator = allocator,
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
.input = .init(allocator),
.queue = queue,
};
}
pub fn deinit(this: *@This()) void {
this.input.deinit(this.allocator);
pub fn deinit(this: @This()) void {
this.input.deinit();
}
pub fn element(this: *@This()) App.Element {
@@ -83,14 +81,14 @@ const InputField = struct {
};
}
fn handle(ctx: *anyopaque, io: std.Io, _: *App.Model, event: App.Event) !void {
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
if (key.isUnicode()) try this.input.append(this.allocator, key.cp);
if (key.isAscii()) try this.input.append(key.cp);
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
try this.queue.push(io, .{ .accept = try this.input.toOwnedSlice(this.allocator) });
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
if (key.eql(.{ .cp = zterm.input.Backspace }))
_ = this.input.pop();
@@ -102,7 +100,7 @@ const InputField = struct {
}
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -125,19 +123,15 @@ const InputField = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
// var stdout_buffer: [4096]u8 = undefined;
// var writer = stdout.writerStreaming(io, &stdout_buffer);
var writer = stdout.writerStreaming(io, &.{});
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -157,7 +151,7 @@ pub fn main(init: std.process.Init) !void {
defer container.deinit();
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .lightgrey },
.rectangle = .{ .fill = .light_grey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 10 },
@@ -165,12 +159,12 @@ pub fn main(init: std.process.Init) !void {
}, input_field.element()));
const nested_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .lightgrey },
.rectangle = .{ .fill = .light_grey },
}, spinner.element());
try container.append(nested_container);
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
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);
@@ -178,17 +172,17 @@ pub fn main(init: std.process.Init) !void {
// draw loop
draw: while (true) {
const now_ms: u64 = @intCast(std.Io.Timestamp.now(io, .real).toMilliseconds());
const now_ms: u64 = @intCast(time.milliTimestamp());
if (now_ms >= next_frame_ms) {
next_frame_ms = now_ms + tick_ms;
} else {
try io.sleep(.{ .nanoseconds = (next_frame_ms - now_ms) * time.ns_per_ms }, .real);
time.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
next_frame_ms += tick_ms;
}
const len = blk: {
try app.queue.lock(io);
defer app.queue.unlock(io);
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
@@ -200,7 +194,7 @@ pub fn main(init: std.process.Init) !void {
switch (event) {
.key => |key| {
log.debug("key {any}", .{key});
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try app.quit();
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
},
.accept => |input| {
defer allocator.free(input);
@@ -218,7 +212,7 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -232,10 +226,10 @@ pub fn main(init: std.process.Init) !void {
}
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -246,6 +240,6 @@ const std = @import("std");
const time = std.time;
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {
const App = zterm.App(union(enum) {
accept: []u21,
});
+44 -47
View File
@@ -1,11 +1,11 @@
const QuitText = struct {
const text = "Press ctrl+c to quit. Press ctrl+n to launch `vim`.";
const text = "Press ctrl+c to quit. Press ctrl+n to launch helix.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -24,20 +24,17 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const gpa = init.gpa;
const io = init.io;
var app: App = .init(gpa, io, .{});
var stdout = std.Io.File.stdout();
// var stdout_buffer: [4096]u8 = undefined;
// var writer = stdout.writerStreaming(io, &stdout_buffer);
var writer = stdout.writerStreaming(io, &.{});
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var renderer = zterm.Renderer.Buffered.init(gpa);
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
@@ -45,7 +42,7 @@ pub fn main(init: std.process.Init) !void {
// TODO what should the demo application do?
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
// - on the left some buttons?
var box = try App.Container.init(gpa, .{
var box = try App.Container.init(allocator, .{
.border = .{
.color = .blue,
.sides = .all,
@@ -59,19 +56,20 @@ pub fn main(init: std.process.Init) !void {
.dim = .{ .y = 90 },
},
}, .{});
try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen },
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}, .{}));
try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen },
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}, .{}));
try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen },
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}, .{}));
defer box.deinit();
var scrollable: App.Scrollable = .init(box, .disabled);
var container = try App.Container.init(gpa, .{
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.separator = .{ .enabled = true },
@@ -79,9 +77,9 @@ pub fn main(init: std.process.Init) !void {
.direction = .horizontal,
},
}, quit_text.element());
try container.append(try App.Container.init(gpa, .{}, scrollable.element()));
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
var nested_container: App.Container = try .init(gpa, .{
var nested_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
.separator = .{
@@ -89,16 +87,16 @@ pub fn main(init: std.process.Init) !void {
},
},
}, .{});
var inner_container: App.Container = try .init(gpa, .{
var inner_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
},
.border = .{
.color = .lightblue,
.color = .light_blue,
.sides = .all,
},
}, .{});
try inner_container.append(try .init(gpa, .{
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .blue,
},
@@ -107,7 +105,7 @@ pub fn main(init: std.process.Init) !void {
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(gpa, .{
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .red,
},
@@ -116,20 +114,20 @@ pub fn main(init: std.process.Init) !void {
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(gpa, .{
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .green,
},
}, .{}));
try nested_container.append(inner_container);
try nested_container.append(try .init(gpa, .{
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(gpa, .{
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.size = .{
.dim = .{ .x = 30 },
@@ -137,31 +135,31 @@ pub fn main(init: std.process.Init) !void {
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent() catch break; // error indicates cancled Io
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 } })) try app.quit();
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.stop(w);
try w.flush();
defer renderer.clear(w) catch @panic("could not clear the screen");
defer app.start(.full, w) catch @panic("could not start app event loop");
var child = try std.process.spawn(io, .{ .argv = &.{"vim"} });
_ = child.wait(io) catch |err| try app.postEvent(.{
try app.interrupt();
renderer.size = .{}; // reset size, such that next resize will cause a full re-draw!
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Spawning $EDITOR failed",
},
});
continue;
}
},
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
@@ -170,7 +168,7 @@ pub fn main(init: std.process.Init) !void {
}
// NOTE returned errors should be propagated back to the application
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -183,11 +181,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
try w.flush();
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -198,4 +195,4 @@ const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
-195
View File
@@ -1,195 +0,0 @@
const QuitText = struct {
const text = "Press ctrl+c or ctrl+d to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.minSize = minSize,
.content = content,
},
};
}
fn minSize(_: *anyopaque, _: *const App.Model, size: zterm.Point) zterm.Point {
return .{ .x = size.x, .y = 10 }; // this includes the border
}
pub 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 y = 0;
const x = 2;
const anchor = (y * size.x) + x;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.emphasis = &.{ .bold, .underline };
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const Prompt = struct {
len: u16 = 3,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
// .minSize = minSize,
.content = content,
},
};
}
// NOTE size hint is not required as the `.size = .{ .dim = .{..} }` property is set accordingly which denotes the minimal size
// fn minSize(ctx: *anyopaque, _: *const App.Model, _: zterm.Point) zterm.Point {
// const this: *@This() = @ptrCast(@alignCast(ctx));
// return .{ .x = this.len, .y = 1 };
// }
pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len > 2); // expect at least two cells
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
for (0..this.len) |idx| {
cells[idx].style.bg = .blue;
cells[idx].style.fg = .black;
cells[idx].style.emphasis = &.{.bold};
}
cells[1].cp = '>';
// leave one clear whitespace after the prompt
cells[this.len].style.bg = .default;
cells[this.len].style.cursor = true; // marks the actual end of the rendering!
}
};
pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const gpa = init.gpa;
const io = init.io;
var app: App = .init(gpa, io, .{});
var stdout = std.Io.File.stdout();
// var buffer: [4096]u8 = undefined;
// var writer = stdout.writerStreaming(io, &buffer);
var writer = stdout.writerStreaming(io, &.{});
const w = &writer.interface;
var renderer = zterm.Renderer.Direct.init(gpa);
defer renderer.deinit();
var container: App.Container = try .init(gpa, .{
.layout = .{
.direction = .vertical,
.gap = 1, // show empty line between elements to allow navigation through paragraph jumping
},
.size = .{
.grow = .horizontal_only,
},
}, .{});
defer container.deinit();
var quit_text: QuitText = .{};
var intermediate: App.Container = try .init(gpa, .{
.border = .{
.sides = .{ .left = true },
.color = .grey,
},
.layout = .{
.direction = .horizontal,
.padding = .{ .left = 1, .top = 1 },
},
}, quit_text.element());
try intermediate.append(try .init(gpa, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try intermediate.append(try .init(gpa, .{
.rectangle = .{ .fill = .green },
}, .{}));
var padding_container: App.Container = try .init(gpa, .{
.layout = .{
.padding = .horizontal(1),
},
}, .{});
try padding_container.append(intermediate);
try container.append(padding_container);
var prompt: Prompt = .{};
try container.append(try .init(gpa, .{
.rectangle = .{ .fill = .grey },
.size = .{
.dim = .{ .y = 1 },
},
}, prompt.element()));
try app.start(.direct, w); // needs to become configurable, as what should be enabled / disabled (i.e. show cursor, hide cursor, use alternate screen, etc.)
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
event: while (true) {
// batch events since last iteration
const len = blk: {
try app.queue.poll(io);
try app.queue.lock(io);
defer app.queue.unlock(io);
break :blk app.queue.len();
};
// handle events
for (0..len) |_| {
const event = app.queue.drain().?;
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
// NOTE draw the character with the ctrl indication and a newline to make sure the rendering stays consistent
.cancel => {
try app.quit();
break :event;
},
.line => |line| {
defer gpa.free(line);
log.debug("{s}", .{line});
if (std.mem.eql(u8, line, "q\n")) {
try app.quit();
break :event;
}
},
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
}
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
}
}
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) {});
+17 -20
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -24,18 +24,15 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -58,22 +55,22 @@ pub fn main(init: std.process.Init) !void {
var alignment: App.Alignment = .init(quit_container, .center);
try container.append(try .init(allocator, .{}, alignment.element()));
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.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(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -86,10 +83,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -99,4 +96,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+22 -29
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -40,7 +40,7 @@ const Clickable = struct {
};
}
fn handle(ctx: *anyopaque, io: std.Io, _: *App.Model, event: App.Event) !void {
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
@@ -49,13 +49,13 @@ const Clickable = struct {
value %= 17;
if (value == 0) value = 1;
this.color = @enumFromInt(value);
try this.queue.push(io, .{ .click = @tagName(mouse.button) });
this.queue.push(.{ .click = @tagName(mouse.button) });
},
else => {},
}
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -74,19 +74,15 @@ const Clickable = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
// var stdout_buffer: [4096]u8 = undefined;
// var writer = stdout.writerStreaming(io, &stdout_buffer);
var writer = stdout.writerStreaming(io, &.{});
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -103,27 +99,27 @@ pub fn main(init: std.process.Init) !void {
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .lightgrey } }, element));
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(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.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(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -136,10 +132,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -149,10 +145,7 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(
struct {},
union(enum) {
const App = zterm.App(union(enum) {
click: [:0]const u8,
accept,
},
);
});
+27 -31
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -37,7 +37,7 @@ const MouseDraw = struct {
};
}
fn handle(ctx: *anyopaque, _: std.Io, _: *App.Model, event: App.Event) !void {
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
@@ -45,7 +45,7 @@ const MouseDraw = struct {
}
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx));
@@ -57,22 +57,21 @@ const MouseDraw = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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: App.Input(.accept) = .init(allocator, &app.queue, .init(.black, .blue));
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 = .{};
@@ -87,7 +86,7 @@ pub fn main(init: std.process.Init) !void {
defer container.deinit();
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .lightgrey },
.rectangle = .{ .fill = .light_grey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 1 },
@@ -99,7 +98,7 @@ pub fn main(init: std.process.Init) !void {
.sides = .all,
.color = .black,
},
.rectangle = .{ .fill = .lightgrey },
.rectangle = .{ .fill = .light_grey },
.layout = .{
.separator = .{
.enabled = true,
@@ -108,24 +107,24 @@ pub fn main(init: std.process.Init) !void {
},
}, .{});
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .lightgrey },
.rectangle = .{ .fill = .light_grey },
}, mouse_draw.element()));
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .lightgrey },
.rectangle = .{ .fill = .light_grey },
}, second_mouse_draw.element()));
try container.append(nested_container);
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.accept => |input| {
defer allocator.free(input);
log.debug("Accepted input '{s}'", .{input});
@@ -134,7 +133,7 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -147,10 +146,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -162,9 +161,6 @@ const assert = std.debug.assert;
const zterm = @import("zterm");
const Color = zterm.Color;
const App = zterm.App(
struct {},
union(enum) {
const App = zterm.App(union(enum) {
accept: []u8,
},
);
});
+247
View 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, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const MouseDraw = struct {
position: ?zterm.Point = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
else => this.position = null,
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.position) |pos| {
const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x);
cells[idx].cp = 'x';
cells[idx].style.fg = .red;
}
}
};
const Popup = struct {
container: ?*App.Container = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.resize = resize,
.reposition = reposition,
.handle = handle,
.content = content,
},
};
}
fn resize(ctx: *anyopaque, size: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.resize(size);
}
fn reposition(ctx: *anyopaque, _: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.reposition(.{});
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
// TODO should the `Element` handle the pop_down element triggering (i.e. by defining a usual key for this?)
.pop_up => |optional| if (optional) |container| {
this.container = @ptrCast(@alignCast(container));
} else {
this.container = null;
},
else => if (this.container) |container| try container.handle(event),
}
}
fn render_container(container: App.Container, cells: []zterm.Cell, container_size: zterm.Point) !void {
const size = container.size;
const origin = container.origin;
const contents = try container.content();
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
var idx: usize = 0;
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
cells[anchor + (row * container_size.x) + col] = contents[idx];
idx += 1;
if (contents.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(contents);
for (container.elements.items) |child| try render_container(child, cells, size);
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.container) |container| {
assert(cells.len == @as(usize, container.size.x) * @as(usize, container.size.y));
const popup_cells = try container.content();
for (container.elements.items) |child| try render_container(child, popup_cells, size);
assert(cells.len == popup_cells.len);
@memcpy(cells, popup_cells);
container.allocator.free(popup_cells);
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var popup: Popup = .{};
var quit_text: QuitText = .{};
// TODO
// - rendering of first Container contents will be overwritten by contents of the appended Container's (see the missing blue rectangle)
// - when rendering "nothing" it causes the below Container to be overwritten to "nothing" too!
// - not sure how this should be done?
// - provide the pop-up with a area where to draw? (i.e. somewhere to draw the provided `Container`)
// - This however will not suffice as the contents will be overwritten!
var container = try App.Container.init(allocator, .{
.rectangle = .{
.fill = .blue,
},
}, .{});
defer container.deinit();
var popup_root_container = try App.Container.init(allocator, .{
.layout = .{
.padding = .{
.top = -17,
.left = -40,
.right = 5,
.bottom = 5,
},
},
}, .{});
try popup_root_container.append(try App.Container.init(allocator, .{}, popup.element()));
try container.append(try .init(allocator, .{}, quit_text.element()));
try container.append(popup_root_container); // FIXME it should not be appended (as it would become part of the layout)
var mouse: MouseDraw = .{};
var popup_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.layout = .{
.padding = .{
.top = -4,
.bottom = 1,
.left = 3,
.right = 3,
},
},
}, .{});
// showcase that inner `Container`s handle `Element`s accordingly
try popup_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, mouse.element()));
defer popup_container.deinit();
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = zterm.input.Space })) app.postEvent(.{ .pop_up = &popup_container });
if (key.eql(.{ .cp = zterm.input.Escape })) app.postEvent(.{ .pop_up = null });
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
pop_up: ?*anyopaque,
});
+32 -72
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -24,72 +24,35 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
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);
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
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,
},
.percent = .{ .enabled = true },
.fg = .green,
.bg = .grey,
});
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.layout = .{ .padding = .all(5) },
}, quit_text.element());
defer container.deinit();
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(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
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);
@@ -100,11 +63,11 @@ pub fn main(init: std.process.Init) !void {
// Continuous drawing
// draw loop
draw: while (true) {
const now_ms: u64 = @intCast(std.Io.Timestamp.now(io, .real).toMilliseconds());
const now_ms: u64 = @intCast(time.milliTimestamp());
if (now_ms >= next_frame_ms) {
next_frame_ms = now_ms + tick_ms;
} else {
try io.sleep(.{ .nanoseconds = (next_frame_ms - now_ms) * time.ns_per_ms }, .real);
time.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
next_frame_ms += tick_ms;
}
@@ -114,12 +77,12 @@ pub fn main(init: std.process.Init) !void {
increase_progress = 10;
progress_percent += 1;
if (progress_percent > 100) progress_percent = 0;
try app.postEvent(.{ .progress = progress_percent });
app.postEvent(.{ .progress = progress_percent });
}
const len = blk: {
try app.queue.lock(io);
defer app.queue.unlock(io);
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
@@ -130,7 +93,7 @@ pub fn main(init: std.process.Init) !void {
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try app.quit();
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| {
@@ -141,7 +104,7 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -155,10 +118,10 @@ pub fn main(init: std.process.Init) !void {
}
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -169,9 +132,6 @@ const std = @import("std");
const time = std.time;
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(
struct {},
union(enum) {
const App = zterm.App(union(enum) {
progress: u8,
},
);
});
-101
View File
@@ -1,101 +0,0 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
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(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try 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(io, &app.model, event) catch |err| try app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
}
}
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,
});
+30 -27
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -34,7 +34,7 @@ const HelloWorldText = packed struct {
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -53,19 +53,20 @@ const HelloWorldText = packed struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
// var stdout_buffer: [4096]u8 = undefined;
// var writer = stdout.writerStreaming(io, &stdout_buffer);
var writer = stdout.writerStreaming(io, &.{});
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -86,23 +87,24 @@ pub fn main(init: std.process.Init) !void {
},
}, .{});
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .lightgreen },
.rectangle = .{ .fill = .light_green },
.size = .{
.dim = .{ .y = 30 },
},
}, .{}));
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .lightgreen },
.rectangle = .{ .fill = .light_green },
.size = .{
.dim = .{ .y = 5 },
},
}, element));
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .lightgreen },
.rectangle = .{ .fill = .light_green },
.size = .{
.dim = .{ .y = 2 },
},
}, .{}));
defer top_box.deinit();
var bottom_box = try App.Container.init(allocator, .{
.border = .{
@@ -130,6 +132,7 @@ pub fn main(init: std.process.Init) !void {
try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer bottom_box.deinit();
var container = try App.Container.init(allocator, .{
.layout = .{
@@ -145,28 +148,28 @@ pub fn main(init: std.process.Init) !void {
defer container.deinit();
// place empty container containing the element of the scrollable Container.
var scrollable_top: App.Scrollable = .init(top_box, .enabled(.default, false));
var scrollable_top: App.Scrollable = .init(top_box, .enabled(.grey));
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true));
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white));
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.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(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -179,10 +182,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -193,4 +196,4 @@ const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
-100
View File
@@ -1,100 +0,0 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
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(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
}
}
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) {});
+20 -23
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -31,7 +31,7 @@ const InfoText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -57,7 +57,7 @@ const ErrorNotification = struct {
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
}
fn handle(ctx: *anyopaque, _: std.Io, _: *App.Model, event: App.Event) !void {
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall,
@@ -66,7 +66,7 @@ const ErrorNotification = struct {
}
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -89,18 +89,15 @@ const ErrorNotification = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -119,21 +116,21 @@ pub fn main(init: std.process.Init) !void {
try container.append(try App.Container.init(allocator, .{}, info_text.element()));
try container.append(try App.Container.init(allocator, .{}, error_notification.element()));
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.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(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -146,10 +143,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -159,4 +156,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+17 -20
View File
@@ -8,7 +8,7 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -27,18 +27,15 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -72,24 +69,24 @@ pub fn main(init: std.process.Init) !void {
}
defer container.deinit(); // also de-initializes the children
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -102,10 +99,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -115,4 +112,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+17 -20
View File
@@ -8,7 +8,7 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -27,18 +27,15 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -64,24 +61,24 @@ pub fn main(init: std.process.Init) !void {
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -94,10 +91,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -107,4 +104,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+17 -20
View File
@@ -8,7 +8,7 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -27,18 +27,15 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -80,24 +77,24 @@ pub fn main(init: std.process.Init) !void {
}
defer container.deinit(); // also de-initializes the children
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -110,10 +107,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -123,4 +120,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+17 -20
View File
@@ -8,7 +8,7 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -27,18 +27,15 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -63,24 +60,24 @@ pub fn main(init: std.process.Init) !void {
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -93,10 +90,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -106,4 +103,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+18 -21
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -24,19 +24,15 @@ const QuitText = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
// var stdout_buffer: [4096]u8 = undefined;
// var writer = stdout.writerStreaming(io, &stdout_buffer);
var writer = stdout.writerStreaming(io, &.{});
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -54,6 +50,7 @@ pub fn main(init: std.process.Init) !void {
var box = try App.Container.init(allocator, .{
.layout = .{ .direction = .horizontal },
}, .{});
defer box.deinit();
inline for (std.meta.fields(zterm.Color)) |field| {
if (field.value == 0) continue; // zterm.Color.default == 0 -> skip
@@ -62,21 +59,21 @@ pub fn main(init: std.process.Init) !void {
var scrollable: App.Scrollable = .init(box, .disabled);
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) {
const event = try app.nextEvent();
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 } })) try app.quit(),
.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(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -89,10 +86,10 @@ pub fn main(init: std.process.Init) !void {
else => {},
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -102,4 +99,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+34 -144
View File
@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -24,109 +24,14 @@ const QuitText = struct {
}
};
const TableText = struct {
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.content = content,
},
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
_ = size;
var idx: usize = 0;
{
const text = "Normal ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Bold ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Dim ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Italic ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Underl ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Blink ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Invert ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Hidden ";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
{
const text = "Strikethrough";
for (text) |cp| {
cells[idx].cp = cp;
idx += 1;
}
}
}
};
const TextStyles = struct {
const text = "Example";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.minSize = minSize,
.content = content,
},
};
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn minSize(ctx: *anyopaque, _: *const App.Model, size: zterm.Point) zterm.Point {
_ = ctx;
_ = size;
return .{
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
@setEvalBranchQuota(10000);
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -170,18 +75,15 @@ const TextStyles = struct {
}
};
pub fn main(init: std.process.Init) !void {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {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();
@@ -192,51 +94,41 @@ pub fn main(init: std.process.Init) !void {
var container = try App.Container.init(allocator, .{
.layout = .{
// .gap = 2,
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .vertical,
},
}, element);
defer container.deinit();
var table_head: TableText = .{};
try container.append(try .init(allocator, .{
.size = .{
.dim = .{ .y = 1 },
.grow = .horizontal,
},
}, table_head.element()));
var scrollable: App.Scrollable = .init(try .init(allocator, .{
var box = try App.Container.init(allocator, .{
.layout = .{ .direction = .vertical },
}, text_styles.element()), .enabled(.white, true));
.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 = .init(box, .disabled);
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
loop: while (true) {
// batch events since last iteration
const len = blk: {
try app.queue.poll(io);
try app.queue.lock(io);
defer app.queue.unlock(io);
break :blk app.queue.len();
};
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// handle events
for (0..len) |_| {
const event = app.queue.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 } })) try app.quit(),
.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(io, &app.model, event) catch |err| try app.postEvent(.{
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -245,15 +137,14 @@ pub fn main(init: std.process.Init) !void {
// post event handling
switch (event) {
.quit => break :loop,
.quit => break,
else => {},
}
}
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(w);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
@@ -263,5 +154,4 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(struct {}, union(enum) {});
const App = zterm.App(union(enum) {});
+99 -222
View File
@@ -12,209 +12,118 @@
/// ```zig
/// const zterm = @import("zterm");
/// const App = zterm.App(
/// struct {}, // empty model
/// union(enum) {}, // no additional user event's
/// union(enum) {},
/// );
/// // later on create an `App` instance and start the event loop
/// var app: App = .init(io, .{}); // provide instance of the `std.Io` and `App` model that shall be used
/// try app.start(.full);
/// defer app.stop() catch unreachable; // does not clean-up the resources used in the model
/// var app: App = .init;
/// try app.start();
/// defer app.stop() catch unreachable;
/// ```
pub fn App(comptime M: type, comptime E: type) type {
if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`");
if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`.");
pub fn App(comptime E: type) type {
if (!isTaggedUnion(E)) {
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`.");
}
return struct {
gpa: Allocator,
io: std.Io,
model: Model,
queue: Queue,
future: ?std.Io.Future(@typeInfo(@typeInfo(@TypeOf(run)).@"fn".return_type.?).error_union.error_set!void) = null,
thread: ?Thread = null,
quit_event: Thread.ResetEvent,
termios: ?posix.termios = null,
handler_registered: bool = false,
config: TerminalConfiguration,
pub const TerminalConfiguration = struct {
altScreen: bool,
saveScreen: bool,
rawMode: bool,
hideCursor: bool,
pub const full: @This() = .{
.altScreen = true,
.saveScreen = true,
.rawMode = true,
.hideCursor = true,
};
pub const direct: @This() = .{
.altScreen = false,
.saveScreen = false,
.rawMode = false,
.hideCursor = false,
};
};
winch_registered: bool = false,
// global variable for the registered handler for WINCH
var handler_ctx: *anyopaque = undefined;
/// registered WINCH handler to report resize events
fn handleWinch(_: std.os.linux.SIG) callconv(.c) void {
fn handleWinch(_: c_int) callconv(.C) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE this does not have to be done if in-band resize events are supported
// -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize) catch {};
}
/// registered CONT handler to force a complete redraw
fn handleCont(_: 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) catch {};
}
/// registered INT handler to catch ctrl-c to send a `.cancel`
fn handleInt(_: std.os.linux.SIG) callconv(.c) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE do not quit the application and instead issue a `.cancel` event
this.postEvent(.cancel) catch {};
this.postEvent(.resize);
}
pub fn init(gpa: Allocator, io: std.Io, model: Model) @This() {
return .{
.gpa = gpa,
.io = io,
.model = model,
pub const init: @This() = .{
.queue = .{},
.config = undefined,
.quit_event = .{},
};
}
pub fn start(this: *@This(), config: TerminalConfiguration, w: *std.Io.Writer) !void {
if (this.future) |_| return;
this.config = config;
pub fn start(this: *@This()) !void {
if (this.thread) |_| return;
if (!this.handler_registered) {
// post init event (as the very first element to be in the queue - event loop)
try this.postEvent(.init);
this.postEvent(.init);
if (!this.winch_registered) {
handler_ctx = this;
if (config.altScreen) {
posix.sigaction(posix.SIG.WINCH, &.{
var act = posix.Sigaction{
.handler = .{ .handler = handleWinch },
.mask = posix.sigemptyset(),
.flags = 0,
}, null);
}
// only applies to non-raw mode applications
posix.sigaction(posix.SIG.CONT, &.{
.handler = .{ .handler = handleCont },
.mask = posix.sigemptyset(),
.flags = 0,
}, null);
// only applies to non-raw mode applications
posix.sigaction(posix.SIG.INT, &.{
.handler = .{ .handler = handleInt },
.mask = posix.sigemptyset(),
.flags = 0,
}, null);
this.handler_registered = true;
};
posix.sigaction(posix.SIG.WINCH, &act, null);
this.winch_registered = true;
}
this.future = this.io.async(run, .{this});
this.quit_event.reset();
this.thread = try Thread.spawn(.{}, @This().run, .{this});
var termios: posix.termios = undefined;
if (this.config.rawMode) try terminal.enableRawMode(&termios);
try terminal.enableRawMode(&termios);
if (this.termios) |_| {} else this.termios = termios;
if (this.config.altScreen) try terminal.enterAltScreen(w);
if (this.config.saveScreen) try terminal.saveScreen(w);
if (this.config.hideCursor) try terminal.hideCursor(w);
if (this.config.altScreen and this.config.rawMode) try terminal.enableMouseSupport(w);
try terminal.saveScreen();
try terminal.enterAltScreen();
try terminal.hideCursor();
try terminal.enableMouseSupport();
}
pub fn stop(this: *@This(), w: *std.Io.Writer) !void {
if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport(w);
if (this.config.saveScreen) try terminal.restoreScreen(w);
if (this.config.altScreen) try terminal.exitAltScreen(w);
pub fn interrupt(this: *@This()) !void {
this.quit_event.set();
try terminal.disableMouseSupport();
try terminal.exitAltScreen();
try terminal.restoreScreen();
if (this.thread) |thread| {
thread.join();
this.thread = null;
}
}
if (this.config.hideCursor) try terminal.showCursor(w);
if (this.config.saveScreen) try terminal.resetCursor(w);
if (this.termios) |termios| {
if (this.config.rawMode) try terminal.disableRawMode(&termios);
pub fn stop(this: *@This()) !void {
try this.interrupt();
if (this.termios) |*termios| {
try terminal.disableMouseSupport();
try terminal.showCursor();
try terminal.exitAltScreen();
try terminal.disableRawMode(termios);
try terminal.restoreScreen();
}
this.termios = null;
}
if (this.future) |*future| {
future.cancel(this.io) catch {};
this.future = null;
}
// reset stdin nonblocking reading
{
// Step 1: Get current flags
var fl_flags = posix.system.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0);
// Step 2: Remove O_NONBLOCK
fl_flags ^= 1 << @bitOffsetOf(posix.system.O, "NONBLOCK");
std.log.debug("end fl_flags: {any}", .{fl_flags});
// Step 3: Set new flags
_ = posix.system.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags);
}
}
/// Quit the application loop.
/// This will cancel the internal input thread and post a **.quit** `Event`.
pub fn quit(this: *@This()) !void {
if (this.future) |*future| {
future.cancel(this.io) catch {};
this.future = null;
}
try this.postEvent(.quit);
/// This will stop the internal input thread and post a **.quit** `Event`.
pub fn quit(this: *@This()) void {
this.quit_event.set();
this.postEvent(.quit);
}
/// Returns the next available event, blocking until one is available.
pub fn nextEvent(this: *@This()) !Event {
return this.queue.pop(this.io);
pub fn nextEvent(this: *@This()) Event {
return this.queue.pop();
}
/// Post an `Event` into the queue. Blocks if there is no capacity for the `Event`.
pub fn postEvent(this: *@This(), e: Event) !void {
try this.queue.push(this.io, e);
pub fn postEvent(this: *@This(), e: Event) void {
this.queue.push(e);
}
fn run(this: *@This()) !void {
// thread to read user inputs
var buf: [512]u8 = undefined;
// NOTE set the `NONBLOCK` option for the stdin file, such that reading is not blocking!
// if (this.config.rawMode) {
{
// TODO is there a better way to do this through the `std.Io` interface?
var fl_flags = posix.system.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0);
std.log.debug("start fl_flags: {any}", .{fl_flags});
fl_flags |= 1 << @bitOffsetOf(posix.system.O, "NONBLOCK");
std.log.debug("setting of fl_flags: {any}", .{fl_flags});
_ = posix.system.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags);
}
var remaining_bytes: usize = 0;
var lines: std.ArrayList(u8) = .empty;
defer lines.deinit(this.gpa);
var buf: [256]u8 = undefined;
while (true) {
this.io.checkCancel() catch break;
// non-blocking read
const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) {
error.WouldBlock => {
// wait a bit
try this.io.sleep(.fromMilliseconds(20), .awake);
continue;
},
else => return err,
} + remaining_bytes;
remaining_bytes = 0;
if (read_bytes == 0) {
// received <EOF>
try this.postEvent(.cancel);
continue;
}
// FIX I still think that there is a race condition (I'm just waiting 'long' enough)
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
// FIX in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
const read_bytes = try terminal.read(buf[0..]);
// TODO `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
// escape key presses
if (buf[0] == 0x1b and read_bytes > 1) {
switch (buf[1]) {
@@ -235,15 +144,15 @@ pub fn App(comptime M: type, comptime E: type) type {
'S' => .{ .cp = input.F4 },
else => continue,
};
try this.postEvent(.{ .key = key });
this.postEvent(.{ .key = key });
},
0x5B => { // csi
if (read_bytes < 3) continue;
// We start iterating at index 2 to get past the '['
const sequence: []u8 = blk: for (buf[2..], 2..) |b, i| {
const sequence = for (buf[2..], 2..) |b, i| {
switch (b) {
0x40...0xFF => break :blk buf[0 .. i + 1],
0x40...0xFF => break buf[0 .. i + 1],
else => continue,
}
} else continue;
@@ -285,9 +194,9 @@ pub fn App(comptime M: type, comptime E: type) type {
break :blk mod;
},
};
try this.postEvent(.{ .key = key });
this.postEvent(.{ .key = key });
},
'Z' => try this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
'Z' => this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
'~' => {
// Legacy keys
// CSI number ~
@@ -345,11 +254,11 @@ pub fn App(comptime M: type, comptime E: type) type {
break :blk mod;
},
};
try this.postEvent(.{ .key = key });
this.postEvent(.{ .key = key });
},
// TODO focus usage? should this even be in the default event system?
'I' => try this.postEvent(.{ .focus = true }),
'O' => try this.postEvent(.{ .focus = false }),
'I' => this.postEvent(.{ .focus = true }),
'O' => this.postEvent(.{ .focus = false }),
'M', 'm' => {
assert(sequence.len >= 4);
if (sequence[2] != '<') break;
@@ -374,8 +283,7 @@ pub fn App(comptime M: type, comptime E: type) type {
// const alt = button_mask & mouse_bits.alt > 0;
// const ctrl = button_mask & mouse_bits.ctrl > 0;
try this.postEvent(.{
.mouse = .{
const mouse: Mouse = .{
.button = button,
.x = px -| 1,
.y = py -| 1,
@@ -385,8 +293,8 @@ pub fn App(comptime M: type, comptime E: type) type {
if (sequence[sequence.len - 1] == 'm') break :blk .release;
break :blk .press;
},
},
});
};
this.postEvent(.{ .mouse = mouse });
},
'c' => {
// Primary DA (CSI ? Pm c)
@@ -410,7 +318,7 @@ pub fn App(comptime M: type, comptime E: type) type {
_ = width_char;
_ = height_char;
try this.postEvent(.resize);
this.postEvent(.resize);
// this.postEvent(.{ .size = .{
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = fmt.parseUnsigned(u16, height_char, 10) catch break,
@@ -445,7 +353,7 @@ pub fn App(comptime M: type, comptime E: type) type {
},
else => {
// alt + <char> keypress
try this.postEvent(.{
this.postEvent(.{
.key = .{
.cp = buf[1],
.mod = .{ .alt = true },
@@ -454,7 +362,6 @@ pub fn App(comptime M: type, comptime E: type) type {
},
}
} else {
if (this.config.rawMode) {
const b = buf[0];
const key: Key = switch (b) {
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
@@ -469,48 +376,24 @@ pub fn App(comptime M: type, comptime E: type) type {
},
0x7f => .{ .cp = input.Backspace },
else => {
var len = read_bytes;
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
remaining_bytes = read_bytes - len;
var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 };
while (iter.nextCodepoint()) |cp| try this.postEvent(.{ .key = .{ .cp = cp } });
if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
}
continue; // this switch block does not return a `Key` we continue with loop
var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] };
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } });
continue;
},
};
try this.postEvent(.{ .key = key });
} else {
var len = read_bytes;
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
remaining_bytes = read_bytes - len;
try lines.appendSlice(this.gpa, buf[0..len]);
if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
} else {
if (read_bytes != 512 and buf[len - 1] == '\n') try this.postEvent(.{ .line = try lines.toOwnedSlice(this.gpa) });
// NOTE line did not end with `\n` but with EOF, meaning the user canceled
if (read_bytes != 512 and buf[len - 1] != '\n') {
lines.clearRetainingCapacity();
try this.postEvent(.cancel);
}
}
}
this.postEvent(.{ .key = key });
}
continue;
};
break;
}
}
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
var stdout = std.Io.File.stdout();
var writer = stdout.writerStreaming(std.Io.Threaded.global_single_threaded.io(), &.{});
const w = &writer.interface;
terminal.disableMouseSupport(w) catch {};
terminal.showCursor(w) catch {};
terminal.resetCursor(w) catch {};
terminal.restoreScreen(w) catch {};
terminal.disableRawMode(&.{
terminal.disableMouseSupport() catch {};
terminal.exitAltScreen() catch {};
terminal.showCursor() catch {};
var termios: posix.termios = .{
.iflag = .{},
.lflag = .{},
.cflag = .{},
@@ -519,27 +402,22 @@ pub fn App(comptime M: type, comptime E: type) type {
.line = 0,
.ispeed = undefined,
.ospeed = undefined,
}) catch {};
terminal.exitAltScreen(w) catch {};
w.flush() catch {};
};
terminal.disableRawMode(&termios) catch {};
terminal.restoreScreen() catch {};
std.debug.defaultPanic(msg, ret_addr);
}
const element = @import("element.zig");
pub const Model = M;
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Container = @import("container.zig").Container(Model, Event);
pub const Element = element.Element(Model, Event);
pub const Alignment = element.Alignment(Model, Event);
pub const Button = element.Button(Model, Event, Queue);
pub const TextField = element.TextField(Model, Event);
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, 512);
pub const Container = @import("container.zig").Container(Event);
pub const Element = element.Element(Event);
pub const Alignment = element.Alignment(Event);
pub const Button = element.Button(Event, Queue);
pub const Input = element.Input(Event, Queue);
pub const Progress = element.Progress(Event, Queue);
pub const Scrollable = element.Scrollable(Event);
pub const Queue = queue.Queue(Event, 256);
};
}
@@ -550,15 +428,14 @@ const mem = std.mem;
const fmt = std.fmt;
const posix = std.posix;
const Thread = std.Thread;
const Allocator = mem.Allocator;
const assert = std.debug.assert;
const code_point = @import("code_point");
const event = @import("event.zig");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const queue = @import("queue.zig");
const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const isStruct = event.isStruct;
const Mouse = input.Mouse;
const Key = input.Key;
const Point = @import("point.zig").Point;
+16 -17
View File
@@ -1,5 +1,6 @@
//! Cell type containing content and formatting for each character in the terminal screen.
// TODO embrace `zg` dependency more due to utf-8 encoding
cp: u21 = ' ',
style: Style = .{ .emphasis = &.{} },
@@ -12,14 +13,8 @@ pub fn reset(this: *Cell) void {
this.cp = ' ';
}
pub fn value(this: Cell, writer: *std.Io.Writer) !void {
// TODO the style values should be concatinated, such that less bytes are written to the terminal
pub fn value(this: Cell, writer: anytype) !void {
try this.style.value(writer, this.cp);
// std.log.debug("writer buffer size: {d}", .{writer.end});
// NOTE flushing should not be necessary by the library, but from the user!
// if (writer.buffer.len > 255) {
// if (writer.end >= writer.buffer.len - 256) try writer.flush();
// } else try writer.flush();
}
const std = @import("std");
@@ -31,18 +26,20 @@ test "ascii styled text" {
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
.{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } },
.{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } },
.{ .cp = 's', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
};
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer writer.deinit();
var string = std.ArrayList(u8).init(std.testing.allocator);
defer string.deinit();
const writer = string.writer();
for (cells) |cell| {
try cell.value(&writer.writer);
try cell.value(writer);
}
try std.testing.expectEqualSlices(
u8,
"\x1b[38;5;10;48;5;8;59mY\x1b[0m\x1b[39;49;59;1;4mv\x1b[0m\x1b[39;49;59;3me\x1b[0m\x1b[38;5;2;48;5;16;59;4ms\x1b[0m",
writer.writer.buffer[0..writer.writer.end],
string.items,
);
}
@@ -51,17 +48,19 @@ test "utf-8 styled text" {
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
.{ .cp = '─', .style = .{ .emphasis = &.{} } },
.{ .cp = '┄', .style = .{ .emphasis = &.{} } },
.{ .cp = '┘', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
};
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
defer writer.deinit();
var string = std.ArrayList(u8).init(std.testing.allocator);
defer string.deinit();
const writer = string.writer();
for (cells) |cell| {
try cell.value(&writer.writer);
try cell.value(writer);
}
try std.testing.expectEqualSlices(
u8,
"\x1b[38;5;10;48;5;8;59m╭\x1b[0m\x1b[39;49;59m─\x1b[0m\x1b[39;49;59m┄\x1b[0m\x1b[38;5;2;48;5;16;59;4m┘\x1b[0m",
writer.writer.buffer[0..writer.writer.end],
string.items,
);
}
+17 -14
View File
@@ -1,13 +1,13 @@
pub const Color = enum(u8) {
default = 0,
black = 16,
lightred = 1,
lightgreen,
lightyellow,
lightblue,
lightmagenta,
lightcyan,
lightgrey,
light_red = 1,
light_green,
light_yellow,
light_blue,
light_magenta,
light_cyan,
light_grey,
grey,
red,
green,
@@ -18,21 +18,24 @@ 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: *std.Io.Writer, comptime coloring: enum { fg, bg, ul }) !void {
// TODO might be useful to use the std.ascii stuff!
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
if (this == .default) {
switch (coloring) {
.fg => try writer.printAscii("39", .{}),
.bg => try writer.printAscii("49", .{}),
.ul => try writer.printAscii("59", .{}),
.fg => try format(writer, "39", .{}),
.bg => try format(writer, "49", .{}),
.ul => try format(writer, "59", .{}),
}
} else {
switch (coloring) {
.fg => try writer.print("38;5;{d}", .{@intFromEnum(this)}),
.bg => try writer.print("48;5;{d}", .{@intFromEnum(this)}),
.ul => try writer.print("58;5;{d}", .{@intFromEnum(this)}),
.fg => try format(writer, "38;5;{d}", .{@intFromEnum(this)}),
.bg => try format(writer, "48;5;{d}", .{@intFromEnum(this)}),
.ul => try format(writer, "58;5;{d}", .{@intFromEnum(this)}),
}
}
}
};
const std = @import("std");
const format = std.fmt.format;
+94 -161
View File
@@ -1,6 +1,3 @@
// FIX known issues:
// - hold fewer instances of the `Allocator`
/// Border configuration struct
pub const Border = packed struct {
/// Color to use for the border
@@ -80,9 +77,8 @@ pub const Border = packed struct {
test "all sides" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .green,
.sides = .all,
@@ -93,15 +89,14 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
}, &container, @import("test/container/border.all.zon"));
}
test "vertical sides" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .green,
.sides = .vertical,
@@ -112,15 +107,14 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/border.vertical.zon"));
}, &container, @import("test/container/border.vertical.zon"));
}
test "horizontal sides" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .green,
.sides = .horizontal,
@@ -131,7 +125,7 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/border.horizontal.zon"));
}, &container, @import("test/container/border.horizontal.zon"));
}
};
@@ -163,9 +157,8 @@ pub const Rectangle = packed struct {
test "fill color overwrite parent fill" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{ .fill = .green },
}, .{});
try container.append(try .init(std.testing.allocator, .{
@@ -177,15 +170,14 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_fill_without_padding.zon"));
}, &container, @import("test/container/rectangle_with_parent_fill_without_padding.zon"));
}
test "fill color padding to show parent fill" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.padding = .all(2),
},
@@ -200,15 +192,14 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon"));
}, &container, @import("test/container/rectangle_with_parent_padding.zon"));
}
test "fill color padding to show parent fill (negative padding)" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.padding = .{
.top = -18,
@@ -228,15 +219,14 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon"));
}, &container, @import("test/container/rectangle_with_parent_padding.zon"));
}
test "fill color spacer with padding" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{
.fill = .black,
},
@@ -257,15 +247,14 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_padding.zon"));
}, &container, @import("test/container/rectangle_with_padding.zon"));
}
test "fill color with gap" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{
.fill = .black,
},
@@ -289,15 +278,14 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_gap.zon"));
}, &container, @import("test/container/rectangle_with_gap.zon"));
}
test "fill color with separator" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.rectangle = .{
.fill = .black,
},
@@ -322,7 +310,7 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_separator.zon"));
}, &container, @import("test/container/rectangle_with_separator.zon"));
}
};
@@ -412,9 +400,8 @@ pub const Layout = packed struct {
test "separator without gaps" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.separator = .{
.enabled = true,
@@ -428,15 +415,14 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps.zon"));
}, &container, @import("test/container/separator_no_gaps.zon"));
}
test "separator without gaps with padding" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.padding = .all(1),
.separator = .{
@@ -451,15 +437,14 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps_with_padding.zon"));
}, &container, @import("test/container/separator_no_gaps_with_padding.zon"));
}
test "separator(2x) without gaps" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.direction = .vertical,
.separator = .{
@@ -476,15 +461,14 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps.zon"));
}, &container, @import("test/container/separator_2x_no_gaps.zon"));
}
test "separator(2x) with border(all)" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
@@ -504,15 +488,14 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_border.zon"));
}, &container, @import("test/container/separator_2x_no_gaps_with_border.zon"));
}
test "separator(2x) with border(all) and padding(all(1))" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
@@ -533,15 +516,14 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
}, &container, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
}
test "separator(2x) with border(all) and gap" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
@@ -562,15 +544,14 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border.zon"));
}, &container, @import("test/container/separator_2x_with_gaps_with_border.zon"));
}
test "separator(2x) with border(all) and gap and padding" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
@@ -592,27 +573,25 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
}, &container, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
}
};
/// Sizing options which should be used by the `Container`
pub const Size = packed struct {
dim: Point = .{},
grow: enum(u3) {
grow: enum(u2) {
both,
fixed,
vertical_only,
horizontal_only,
vertical,
horizontal,
} = .both,
};
pub fn Container(Model: type, Event: type) type {
pub fn Container(comptime Event: type) type {
if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`");
const Element = @import("element.zig").Element(Model, Event);
const Element = @import("element.zig").Element(Event);
return struct {
allocator: Allocator,
origin: Point,
@@ -643,24 +622,23 @@ pub fn Container(Model: type, Event: type) type {
.size = .{},
.properties = properties,
.element = element,
.elements = try std.ArrayList(@This()).initCapacity(allocator, 2),
.elements = std.ArrayList(@This()).init(allocator),
};
}
pub fn deinit(this: *@This()) void {
pub fn deinit(this: *const @This()) void {
for (this.elements.items) |*element| element.deinit();
this.elements.deinit(this.allocator);
this.element.deinit();
this.elements.deinit();
}
pub fn append(this: *@This(), element: @This()) !void {
try this.elements.append(this.allocator, element);
try this.elements.append(element);
}
pub fn reposition(this: *@This(), model: *const Model, origin: Point) void {
pub fn reposition(this: *@This(), origin: Point) void {
const layout = this.properties.layout;
this.origin = origin;
this.element.reposition(model, origin);
this.element.reposition(origin);
var offset = origin.add(.{
.x = Layout.getAbsolutePadding(layout.padding.left, this.size.x),
@@ -672,7 +650,7 @@ pub fn Container(Model: type, Event: type) type {
if (sides.top) offset.y += 1;
for (this.elements.items) |*child| {
child.reposition(model, offset);
child.reposition(offset);
switch (layout.direction) {
.horizontal => offset.x += child.size.x + layout.gap,
@@ -687,7 +665,7 @@ pub fn Container(Model: type, Event: type) type {
}
/// Resize all fit sized `Containers` to the necessary size required by its child elements.
fn fit_resize(this: *@This(), model: *const Model) Point {
fn fit_resize(this: *@This()) Point {
// NOTE this is supposed to be a simple and easy to understand algorithm, there are currently no optimizations done
const layout = this.properties.layout;
var size: Point = switch (layout.direction) {
@@ -712,7 +690,7 @@ pub fn Container(Model: type, Event: type) type {
};
for (this.elements.items) |*child| {
const child_size = child.fit_resize(model);
const child_size = child.fit_resize();
switch (layout.direction) {
.horizontal => {
size.x += child_size.x;
@@ -725,29 +703,24 @@ pub fn Container(Model: type, Event: type) type {
}
}
size = this.minSize(model, size);
// assign currently calculated size
this.size = switch (this.properties.size.grow) {
.both => .max(size, this.properties.size.dim),
.fixed => blk: {
assert(this.properties.size.dim.x > 0 or size.x > 0);
assert(this.properties.size.dim.y > 0 or size.y > 0);
break :blk .max(size, this.properties.size.dim);
.both => Point.max(size, this.properties.size.dim),
.fixed => this.properties.size.dim,
.horizontal => .{
.x = @max(size.x, this.properties.size.dim.x),
.y = this.properties.size.dim.y,
},
.horizontal, .horizontal_only => blk: {
assert(this.properties.size.dim.y > 0 or size.y > 0);
break :blk .max(size, this.properties.size.dim);
},
.vertical, .vertical_only => blk: {
assert(this.properties.size.dim.x > 0 or size.x > 0);
break :blk .max(size, this.properties.size.dim);
.vertical => .{
.x = this.properties.size.dim.x,
.y = @max(size.y, this.properties.size.dim.y),
},
};
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(), model: *const Model, max_size: Point) void {
fn grow_resize(this: *@This(), max_size: Point) void {
const layout = this.properties.layout;
var remainder = switch (layout.direction) {
.horizontal => max_size.x -| (Layout.getAbsolutePadding(layout.padding.left, this.size.x) + Layout.getAbsolutePadding(layout.padding.right, this.size.x)),
@@ -764,31 +737,23 @@ pub fn Container(Model: type, Event: type) type {
const sides = this.properties.border.sides;
switch (layout.direction) {
.vertical => {
if (sides.top) {
remainder -|= 1;
}
if (sides.bottom) {
remainder -|= 1;
}
if (sides.left) {
available -|= 1;
}
if (sides.right) {
available -|= 1;
}
},
.horizontal => {
if (sides.top) {
available -|= 1;
remainder -|= 1;
}
if (sides.bottom) {
available -|= 1;
remainder -|= 1;
}
},
.vertical => {
if (sides.left) {
available -|= 1;
remainder -|= 1;
}
if (sides.right) {
available -|= 1;
remainder -|= 1;
}
},
@@ -809,21 +774,21 @@ pub fn Container(Model: type, Event: type) type {
if (growable_children == 0) first_growable_child = child;
growable_children += 1;
},
.horizontal, .horizontal_only => if (layout.direction == .horizontal) {
.horizontal => if (layout.direction == .horizontal) {
if (growable_children == 0) first_growable_child = child;
growable_children += 1;
},
.vertical, .vertical_only => if (layout.direction == .vertical) {
.vertical => if (layout.direction == .vertical) {
if (growable_children == 0) first_growable_child = child;
growable_children += 1;
},
}
// non layout direction side growth
switch (layout.direction) {
.horizontal => if (child.properties.size.grow == .vertical or child.properties.size.grow == .vertical_only or child.properties.size.grow == .both) {
.horizontal => if (child.properties.size.grow == .vertical or child.properties.size.grow == .both) {
child.size.y = available;
},
.vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .horizontal_only or child.properties.size.grow == .both) {
.vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .both) {
child.size.x = available;
},
}
@@ -841,8 +806,8 @@ pub fn Container(Model: type, Event: type) type {
if (child.properties.size.grow == .fixed) continue;
switch (layout.direction) {
.horizontal => if (child.properties.size.grow == .vertical or child.properties.size.grow == .vertical_only) continue,
.vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .horizontal_only) continue,
.horizontal => if (child.properties.size.grow == .vertical) continue,
.vertical => if (child.properties.size.grow == .horizontal) continue,
}
const size = switch (layout.direction) {
@@ -870,23 +835,23 @@ pub fn Container(Model: type, Event: type) type {
};
if (child.properties.size.grow != .fixed and child_size == smallest_size) {
switch (layout.direction) {
.horizontal => if (child.properties.size.grow != .vertical and child.properties.size.grow != .vertical_only) {
.horizontal => if (child.properties.size.grow != .vertical) {
child.size.x += size_to_correct;
remainder -|= size_to_correct;
},
.vertical => if (child.properties.size.grow != .horizontal and child.properties.size.grow != .horizontal_only) {
.vertical => if (child.properties.size.grow != .horizontal) {
child.size.y += size_to_correct;
remainder -|= size_to_correct;
},
}
if (overflow > 0) {
switch (layout.direction) {
.horizontal => if (child.properties.size.grow != .vertical and child.properties.size.grow != .vertical_only) {
.horizontal => if (child.properties.size.grow != .vertical) {
child.size.x += 1;
overflow -|= 1;
remainder -|= 1;
},
.vertical => if (child.properties.size.grow != .horizontal and child.properties.size.grow != .horizontal_only) {
.vertical => if (child.properties.size.grow != .horizontal) {
child.size.y += 1;
overflow -|= 1;
remainder -|= 1;
@@ -897,57 +862,31 @@ pub fn Container(Model: type, Event: type) type {
}
}
this.element.resize(model, this.size);
for (this.elements.items) |*child| child.grow_resize(model, child.size);
this.element.resize(this.size);
for (this.elements.items) |*child| child.grow_resize(child.size);
}
pub fn resize(this: *@This(), model: *const Model, size: Point) void {
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(model);
this.size = switch (this.properties.size.grow) {
.both => .max(size, fit_size),
.fixed => fit_size,
.horizontal_only => .{
.x = size.x,
.y = fit_size.y,
},
.vertical_only => .{
.x = fit_size.x,
.y = size.y,
},
.horizontal => .{
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) {
.both => this.size = Point.max(size, fit_size),
.fixed => {},
.horizontal => this.size = .{
.x = @max(size.x, fit_size.x),
.y = size.y,
},
.vertical => .{
.vertical => this.size = .{
.x = size.x,
.y = @max(size.y, fit_size.y),
},
};
this.grow_resize(model, this.size);
}
this.grow_resize(this.size);
}
pub fn minSize(this: *const @This(), model: *const Model, size: Point) Point {
var min_size: Point = .{};
for (this.elements.items) |*child| {
const child_size = child.minSize(model, child.properties.size.dim);
min_size = switch (this.properties.layout.direction) {
.horizontal => .{
.x = child_size.x + min_size.x,
.y = @max(child_size.y, min_size.y),
},
.vertical => .{
.x = @max(child_size.x, min_size.x),
.y = child_size.y + min_size.y,
},
};
}
const element_size = this.element.minSize(model, size);
return .max(element_size, min_size);
}
pub fn handle(this: *const @This(), io: std.Io, model: *Model, event: Event) !void {
pub fn handle(this: *const @This(), event: Event) !void {
switch (event) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
// the element receives the mouse event with relative position
@@ -955,20 +894,17 @@ pub fn Container(Model: type, Event: type) type {
var relative_mouse: input.Mouse = mouse;
relative_mouse.x -= this.origin.x;
relative_mouse.y -= this.origin.y;
try this.element.handle(io, model, .{ .mouse = relative_mouse });
try this.element.handle(.{ .mouse = relative_mouse });
for (this.elements.items) |*element| try element.handle(event);
},
else => {
try this.element.handle(event);
for (this.elements.items) |*element| try element.handle(event);
},
// NOTE likely no `Container` should catch the `.bell` event and instead it should be handled by the application itself (which also can then skip the propagation of that event, as it does not cause anything to happen to the `Container` tree)
// .bell => {
// // ring the terminal bell and do not propagate the event any further
// _ = try terminal.ringBell();
// return;
// },
else => try this.element.handle(io, model, event),
}
for (this.elements.items) |*element| try element.handle(io, model, event);
}
pub fn content(this: *const @This(), model: *const Model) ![]Cell {
pub fn content(this: *const @This()) ![]Cell {
if (this.size.x == 0 or this.size.y == 0) return Error.TooSmall;
const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y));
@@ -979,7 +915,7 @@ pub fn Container(Model: type, Event: type) type {
this.properties.border.content(cells, this.size);
this.properties.rectangle.content(cells, this.size);
try this.element.content(model, cells, this.size);
try this.element.content(cells, this.size);
// DEBUG render corresponding corners (except top left) of this `Container` *red*
if (comptime build_options.debug) {
@@ -1009,7 +945,6 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const build_options = @import("build_options");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const Cell = @import("cell.zig");
const Color = @import("color.zig").Color;
@@ -1024,9 +959,8 @@ test {
test "Container Fixed and Grow Size Vertical" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{ .direction = .vertical },
}, .{});
try container.append(try .init(std.testing.allocator, .{
@@ -1044,15 +978,14 @@ test "Container Fixed and Grow Size Vertical" {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_vertical.zon"));
}, &container, @import("test/container/fixed_grow_vertical.zon"));
}
test "Container Fixed and Grow Size Horizontal" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const Model = struct {};
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{}, .{});
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{}, .{});
try container.append(try .init(std.testing.allocator, .{
.size = .{
.dim = .{ .x = 5 },
@@ -1068,5 +1001,5 @@ test "Container Fixed and Grow Size Horizontal" {
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_horizontal.zon"));
}, &container, @import("test/container/fixed_grow_horizontal.zon"));
}
-2
View File
@@ -50,7 +50,6 @@ pub const home = "\x1b[H";
pub const cup = "\x1b[{d};{d}H";
pub const hide_cursor = "\x1b[?25l";
pub const show_cursor = "\x1b[?25h";
pub const reset_cursor_shape = "\x1b[0 q";
pub const cursor_shape = "\x1b[{d} q";
pub const ri = "\x1bM";
pub const ind = "\n";
@@ -140,6 +139,5 @@ pub const osc10_reset = "\x1b]110\x1b\\"; // reset fg to terminal default
pub const osc11_query = "\x1b]11;?\x1b\\"; // bg
pub const osc11_set = "\x1b]11;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set default terminal bg
pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default
pub const osc12_set = "\x1b]12;{s}\x1b\\"; // set the cursor color through the name of the 8 base colors!
pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color
pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default
+266 -766
View File
File diff suppressed because it is too large Load Diff
+37 -85
View File
@@ -8,19 +8,8 @@ pub const SystemEvent = union(enum) {
init,
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
quit,
/// Cancel event to signify that the user provided an EOF
///
/// Usually this event is only triggered by the system in *non raw mode*
/// renderings otherwise the corresponding `.key` event would be fired instead.
cancel,
/// Resize event to signify that the application should re-draw to resize
///
/// Usually no `Container` nor `Element` should act on that event, as it
/// only serves for event based loops to force a re-draw with a new `Event`.
resize,
/// Ring the terminal bell to notify the user. This `Event` is handled by
/// every `Container` and will not be passed through the container tree.
bell,
/// Error event to notify other containers about a recoverable error
err: struct {
/// actual error
@@ -28,115 +17,78 @@ pub const SystemEvent = union(enum) {
/// associated error message
msg: []const u8,
},
/// Input line event received in *non raw mode* (instead of individual `key` events)
///
/// This event contains the entire line until the ending newline character
/// (which is included in the payload of this event).
line: []const u8,
/// Input key event received from the user
key: Key,
/// Mouse input event
mouse: Mouse,
/// Focus event indicating that the application has gained the focus of the user
/// Focus event for mouse interaction
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
focus: bool,
};
/// Merge the two provided `union(enum)` `A` and `B` to a tagged union containing all fields of both tagged unions in `comptime`.
/// Declarations are not supported for `comptime` created types, see https://github.com/ziglang/zig/issues/6709 for details.
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
// TODO maybe it makes sense to have a nested tagged union type (i.e. system: union(enum) and event: union(enum))
// - allows re-definition of system / built-in events
// - clearly shows which events are system / built-in ones and which are user defined events
// - the memory footprint for the nesting is not really harmful
if (!isTaggedUnion(A) or !isTaggedUnion(B)) @compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
@compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
}
const a_fields = @typeInfo(A).@"union".fields;
const a_fields_tag = @typeInfo(A).@"union".tag_type.?;
const a_enum_fields = @typeInfo(a_fields_tag).@"enum".fields;
const b_fields = @typeInfo(B).@"union".fields;
const b_fields_tag = @typeInfo(B).@"union".tag_type.?;
const b_enum_fields = @typeInfo(b_fields_tag).@"enum".fields;
var field_names: [a_fields.len + b_fields.len][:0]const u8 = undefined;
var field_types: [a_fields.len + b_fields.len]type = undefined;
var enum_names: [a_fields.len + b_fields.len][:0]const u8 = undefined;
// FIX can I not use `comptime_int`? `u4` would be actually incorrect!
// can I get the correct size through the length of the fields?
var enum_values: [a_fields.len + b_fields.len]u4 = undefined;
var fields: [a_fields.len + b_fields.len]std.builtin.Type.UnionField = undefined;
var enum_fields: [a_fields.len + b_fields.len]std.builtin.Type.EnumField = undefined;
var i: usize = 0;
for (a_fields, a_enum_fields) |field, enum_field| {
field_names[i] = field.name;
field_types[i] = field.type;
fields[i] = field;
var enum_f = enum_field;
enum_f.value = i;
enum_names[i] = enum_f.name;
enum_values[i] = i;
enum_fields[i] = enum_f;
i += 1;
}
for (b_fields, b_enum_fields) |field, enum_field| {
field_names[i] = field.name;
field_types[i] = field.type;
fields[i] = field;
var enum_f = enum_field;
enum_f.value = i;
enum_names[i] = enum_f.name;
enum_values[i] = i;
enum_fields[i] = enum_f;
i += 1;
}
// NOTE declarations are not supported for `comptime` types: https://github.com/ziglang/zig/issues/6709
// -> will lead to a compilation error when constructing the tagged union
// at the end of this function in case at least one of the provided tagged
// unions to merge contains declarations (which in this case can only be the
// user provided one)
const a_enum_decls = @typeInfo(A).@"union".decls;
const b_enum_decls = @typeInfo(B).@"union".decls;
var decls: [a_enum_decls.len + b_enum_decls.len]std.builtin.Type.Declaration = undefined;
var j: usize = 0;
for (a_enum_decls) |decl| {
decls[j] = decl;
j += 1;
}
for (b_enum_decls) |decl| {
decls[j] = decl;
j += 1;
const log2_i = @bitSizeOf(@TypeOf(i)) - @clz(i);
const EventType = @Type(.{ .int = .{
.signedness = .unsigned,
.bits = log2_i,
} });
const Event = @Type(.{ .@"enum" = .{
.tag_type = EventType,
.fields = enum_fields[0..],
.decls = &.{},
.is_exhaustive = true,
} });
return @Type(.{ .@"union" = .{
.layout = .auto,
.tag_type = Event,
.fields = fields[0..],
.decls = &.{},
} });
}
const EventType = @Int(.unsigned, @bitSizeOf(@TypeOf(i)) - @clz(i));
const Event = @Enum(
EventType,
.exhaustive,
&enum_names,
&enum_values,
);
return @Union(
.auto,
Event,
&field_names,
&field_types,
&@splat(.{}),
);
}
/// 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 {
// Determine at `comptime` whether the provided type `E` is an `union(enum)`.
pub fn isTaggedUnion(comptime E: type) bool {
switch (@typeInfo(E)) {
.@"union" => |u| {
if (u.tag_type) |_| {} else {
return false;
}
},
else => return false,
}
return true;
}
/// Determine whether the provided type `T` is a `struct`.
pub fn isStruct(comptime T: type) bool {
return switch (@typeInfo(T)) {
.@"struct" => true,
else => false,
};
}
const std = @import("std");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
+1 -24
View File
@@ -65,30 +65,7 @@ pub const Key = packed struct {
return meta.eql(this, other);
}
/// Determine if the `Key` is an unicode 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
/// modifiers (alt and/or ctrl) are used.
///
/// # Example
///
/// Get user input's from the .key event from the application event loop:
///
/// ```zig
/// switch (event) {
/// .key => |key| if (key.isUnicode()) try this.input.append(key.cp),
/// else => {},
/// }
/// ```
pub fn isUnicode(this: @This()) bool {
return this.mod.alt == false and this.mod.ctrl == false and // no modifier keys
(this.cp >= 32 and this.cp <= 126 or // ascii printable characters (except for input.Delete)
this.cp >= 128 and this.cp <= 255) or // extended ascii codes
((this.cp >= 0x0080 and this.cp <= 0x07FF) or
(this.cp >= 0x0800 and this.cp <= 0xFFFF) or
(this.cp >= 0x100000 and this.cp <= 0x10FFFF)) and // allowed unicode character ranges (2 - 4 byte characters)
(this.cp < 57348 or this.cp > 57454); // no other predefined meanings (i.e. arrow keys, etc.)
}
// 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
+53 -53
View File
@@ -1,7 +1,7 @@
// taken from https://github.com/rockorager/libvaxis/blob/main/s!rc/queue.zig (MIT-Lice!n!se)
// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License)
// with slight modifications
/// Queue implementation. Thread safe. Fixed size. _Blocking_ `push` and `pop`. _Polling_ through `tryPop` and `tryPush`.
/// 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,
@@ -9,26 +9,26 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
read_index: usize = 0,
write_index: usize = 0,
mutex: std.Io.Mutex = .init,
mutex: std.Thread.Mutex = .{},
// blocks when the buffer is full
not_full: std.Io.Condition = .init,
not_full: std.Thread.Condition = .{},
// ...or empty
not_empty: std.Io.Condition = .init,
not_empty: std.Thread.Condition = .{},
const QueueType = @This();
/// Pop an item from the queue. Blocks until an item is available.
pub fn pop(this: *QueueType, io: std.Io) !T {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
pub fn pop(this: *QueueType) T {
this.mutex.lock();
defer this.mutex.unlock();
while (this.isEmptyLH()) {
try this.not_empty.wait(io, &this.mutex);
this.not_empty.wait(&this.mutex);
}
assert(!this.isEmptyLH());
if (this.isFullLH()) {
// If we are full, wake up a push that might be
// waiting here.
this.not_full.signal(io);
this.not_full.signal();
}
const result = this.buf[this.mask(this.read_index)];
@@ -38,15 +38,15 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
/// Push an item into the queue. Blocks until an item has been
/// put in the queue.
pub fn push(this: *QueueType, io: std.Io, item: T) !void {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
pub fn push(this: *QueueType, item: T) void {
this.mutex.lock();
defer this.mutex.unlock();
while (this.isFullLH()) {
try this.not_full.wait(io, &this.mutex);
this.not_full.wait(&this.mutex);
}
if (this.isEmptyLH()) {
// If we were empty, wake up a pop if it was waiting.
this.not_empty.signal(io);
this.not_empty.signal();
}
assert(!this.isFullLH());
@@ -57,45 +57,45 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
/// Push an item into the queue. Returns true when the item
/// was successfully placed in the queue, false if the queue
/// was full.
pub fn tryPush(this: *QueueType, io: std.Io, item: T) !bool {
try this.mutex.lock(io);
pub fn tryPush(this: *QueueType, item: T) bool {
this.mutex.lock();
if (this.isFullLH()) {
this.mutex.unlock();
return false;
}
this.mutex.unlock(io);
this.mutex.unlock();
this.push(item);
return true;
}
/// Pop an item from the queue. Returns null when no item is
/// available.
pub fn tryPop(this: *QueueType, io: std.Io) !?T {
try this.mutex.lock(io);
pub fn tryPop(this: *QueueType) ?T {
this.mutex.lock();
if (this.isEmptyLH()) {
this.mutex.unlock(io);
this.mutex.unlock();
return null;
}
this.mutex.unlock(io);
this.mutex.unlock();
return this.pop();
}
/// Poll the queue. This call blocks until events are in the queue
pub fn poll(this: *QueueType, io: std.Io) !void {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
pub fn poll(this: *QueueType) void {
this.mutex.lock();
defer this.mutex.unlock();
while (this.isEmptyLH()) {
try this.not_empty.wait(io, &this.mutex);
this.not_empty.wait(&this.mutex);
}
assert(!this.isEmptyLH());
}
pub fn lock(this: *QueueType, io: std.Io) !void {
try this.mutex.lock(io);
pub fn lock(this: *QueueType) void {
this.mutex.lock();
}
pub fn unlock(this: *QueueType, io: std.Io) void {
this.mutex.unlock(io);
pub fn unlock(this: *QueueType) void {
this.mutex.unlock();
}
/// Used to efficiently drain the queue
@@ -117,16 +117,16 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
}
/// Returns `true` if the queue is empty and `false` otherwise.
pub fn isEmpty(this: *QueueType, io: std.Io) !bool {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
pub fn isEmpty(this: *QueueType) bool {
this.mutex.lock();
defer this.mutex.unlock();
return this.isEmptyLH();
}
/// Returns `true` if the queue is full and `false` otherwise.
pub fn isFull(this: *QueueType, io: std.Io) !bool {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
pub fn isFull(this: *QueueType) bool {
this.mutex.lock();
defer this.mutex.unlock();
return this.isFullLH();
}
@@ -199,15 +199,15 @@ test "Try to pop, fill from another thread" {
thread.join();
}
fn sleepyPop(q: *Queue(u8, 2), io: std.Io) !void {
fn sleepyPop(q: *Queue(u8, 2)) !void {
// First we wait for the queue to be full.
while (!q.isFull())
try Thread.yield();
// Then we spuriously wake it up, because that's a thing that can
// happen.
q.not_full.signal(io);
q.not_empty.signal(io);
q.not_full.signal();
q.not_empty.signal();
// Then give the other thread a good chance of waking up. It's not
// clear that yield guarantees the other thread will be scheduled,
@@ -215,7 +215,7 @@ fn sleepyPop(q: *Queue(u8, 2), io: std.Io) !void {
// still full and the push in the other thread is still blocked
// waiting for space.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s);
std.time.sleep(std.time.ns_per_s);
// Finally, let that other thread go.
try testing.expectEqual(1, q.pop());
@@ -225,15 +225,15 @@ fn sleepyPop(q: *Queue(u8, 2), io: std.Io) !void {
try Thread.yield();
// But we want to ensure that there's a second push waiting, so
// here's another sleep.
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Another spurious wake...
q.not_full.signal(io);
q.not_empty.signal(io);
q.not_full.signal();
q.not_empty.signal();
// And another chance for the other thread to see that it's
// spurious and go back to sleep.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Pop that thing and we're done.
try testing.expectEqual(2, q.pop());
@@ -267,17 +267,17 @@ test "Fill, block, fill, block" {
try testing.expectEqual(4, queue.pop());
}
fn sleepyPush(q: *Queue(u8, 1), io: std.Io) !void {
fn sleepyPush(q: *Queue(u8, 1)) !void {
// Try to ensure the other thread has already started trying to pop.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Spurious wake
q.not_full.signal(io);
q.not_empty.signal(io);
q.not_full.signal();
q.not_empty.signal();
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Stick something in the queue so it can be popped.
q.push(1);
@@ -286,11 +286,11 @@ fn sleepyPush(q: *Queue(u8, 1), io: std.Io) !void {
try Thread.yield();
// Give the other thread time to block again.
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
// Spurious wake
q.not_full.signal(io);
q.not_empty.signal(io);
q.not_full.signal();
q.not_empty.signal();
q.push(2);
}
@@ -317,7 +317,7 @@ test "2 readers" {
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
std.time.sleep(std.time.ns_per_s / 2);
queue.push(1);
queue.push(1);
t1.join();
+14 -123
View File
@@ -25,7 +25,7 @@ pub const Buffered = struct {
}
}
pub fn resize(this: *@This(), w: *std.Io.Writer) !Point {
pub fn resize(this: *@This()) !Point {
const size = terminal.getTerminalSize();
if (meta.eql(this.size, size)) return this.size;
@@ -47,21 +47,21 @@ pub const Buffered = struct {
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
@memset(this.virtual_screen, .{});
}
try this.clear(w);
try this.clear();
return size;
}
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
pub fn clear(this: *@This(), w: *std.Io.Writer) !void {
try terminal.clearScreen(w);
pub fn clear(this: *@This()) !void {
try terminal.clearScreen();
@memset(this.screen, .{});
}
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime Container: type, container: *Container, comptime Model: type, model: *const Model) !void {
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.content(model);
const cells: []const Cell = try container.content();
if (cells.len == 0) return;
@@ -80,19 +80,17 @@ pub const Buffered = struct {
// free immediately
container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(Container, element, Model, model);
for (container.elements.items) |*element| try this.render(T, element);
}
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
pub fn flush(this: *@This(), w: *std.Io.Writer) !void {
defer w.flush() catch {};
try terminal.hideCursor(w);
pub fn flush(this: *@This()) !void {
try terminal.hideCursor();
// TODO measure timings of rendered frames?
var cursor_position: ?Point = null;
const writer = terminal.writer();
const s = this.screen;
const vs = this.virtual_screen;
for (0..this.size.y) |row| {
for (0..this.size.x) |col| {
const idx = (row * this.size.x) + col;
@@ -106,127 +104,20 @@ pub const Buffered = struct {
.x = @truncate(col),
.y = @truncate(row),
};
try cvs.style.set_cursor_style(w);
}
if (cs.eql(cvs)) continue;
// render differences found in virtual screen
try terminal.setCursorPosition(w, .{ .y = @truncate(row), .x = @truncate(col) });
try cvs.value(w);
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(w);
try terminal.setCursorPosition(w, point);
}
}
};
pub const Direct = struct {
gpa: Allocator,
size: Point,
resized: bool,
screen: []Cell,
pub fn init(gpa: Allocator) @This() {
return .{
.gpa = gpa,
.size = .{},
.resized = true,
.screen = undefined,
};
}
pub fn deinit(this: *@This()) void {
this.gpa.free(this.screen);
}
pub fn resize(this: *@This()) !Point {
this.size = .{};
if (!this.resized) {
this.gpa.free(this.screen);
this.screen = undefined;
}
this.resized = true;
return terminal.getTerminalSize();
}
pub fn clear(this: *@This()) !void {
_ = this;
try terminal.clearScreen();
}
pub fn writeCtrlDWithNewline(this: *@This(), writer: *std.Io.Writer) !void {
_ = this;
_ = try terminal.write(writer, "^D\n");
}
pub fn writeNewline(this: *@This(), writer: *std.Io.Writer) !void {
_ = this;
_ = try terminal.write(writer, "\n");
}
/// Render provided cells at size (anchor and dimension) into the *screen*.
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;
if (this.resized) {
this.size = size;
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
this.screen = try this.gpa.alloc(Cell, n);
@memset(this.screen, .{});
this.resized = false;
}
const cells: []const Cell = try container.content(model);
var idx: usize = 0;
var vs = this.screen;
const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
vs[anchor + (row * this.size.x) + col] = cells[idx];
idx += 1;
if (cells.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(Container, element, Model, model);
}
pub fn flush(this: *@This(), writer: *std.Io.Writer) !void {
defer writer.flush() catch {};
row: for (0..this.size.y) |row| {
for (0..this.size.x) |col| {
const idx = (row * this.size.x) + col;
const cvs = this.screen[idx];
// are we done for this row already?
if (col + 1 < this.size.x and meta.eql(cvs, .{})) {
// check the remaining line
var last_cell = true;
for (col..this.size.x) |x| {
if (!meta.eql(cvs, this.screen[(row * this.size.x) + x])) {
last_cell = false;
break;
}
}
if (last_cell) {
//std.log.debug("Found the last cell in row: {d} at column: {d}", .{row, col});
_ = try writer.write("\n");
continue :row;
}
}
try cvs.value(writer);
if (cvs.style.cursor) return; // that's where the cursor should be left!
}
try terminal.showCursor();
try terminal.setCursorPosition(point);
}
}
};
+10 -30
View File
@@ -12,8 +12,6 @@ fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
cursor: bool = false,
cursor_color: Color = .default,
cursor_shape: CursorShape = .default,
ul_style: Underline = .off,
emphasis: []const Emphasis,
@@ -38,51 +36,33 @@ pub const Emphasis = enum(u8) {
strikethrough,
};
pub const CursorShape = enum(u4) {
default = 0,
block_blinking = 1,
block_steady,
underline_blinking,
underline_steady,
bar_blinking,
bar_steady,
};
pub fn eql(this: Style, other: Style) bool {
// TODO should there be a compare for every field?
return meta.eql(this, other);
}
pub fn set_cursor_style(this: Style, writer: *std.Io.Writer) !void {
if (!this.cursor) return;
switch (this.cursor_color) {
.default => try writer.print(ctlseqs.osc12_reset, .{}),
else => try writer.print(ctlseqs.osc12_set, .{@tagName(this.cursor_color)}),
}
try writer.print(ctlseqs.cursor_shape, .{@intFromEnum(this.cursor_shape)});
}
// TODO might be useful to use the std.ascii stuff!
pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void {
pub fn value(this: Style, writer: anytype, cp: u21) !void {
var buffer: [4]u8 = undefined;
const bytes = try unicode.utf8Encode(cp, &buffer);
assert(bytes > 0);
// build ansi sequence for 256 colors ...
// foreground
try writer.printAscii("\x1b[", .{});
try format(writer, "\x1b[", .{});
try this.fg.write(writer, .fg);
// background
try writer.printAsciiChar(';', .{});
try format(writer, ";", .{});
try this.bg.write(writer, .bg);
// underline
// FIX assert that if the underline property is set that the ul style and the attribute for underlining is available
try writer.printAsciiChar(';', .{});
try format(writer, ";", .{});
try this.ul.write(writer, .ul);
// append styles (aka attributes like bold, italic, strikethrough, etc.)
for (this.emphasis) |attribute| try writer.print(";{d}", .{@intFromEnum(attribute)});
try writer.printAsciiChar('m', .{});
for (this.emphasis) |attribute| try format(writer, ";{d}", .{@intFromEnum(attribute)});
try format(writer, "m", .{});
// content
try writer.printAscii(buffer[0..bytes], .{});
try writer.printAscii("\x1b[0m", .{});
try format(writer, "{s}", .{buffer[0..bytes]});
try format(writer, "\x1b[0m", .{});
}
// TODO implement helper functions for terminal capabilities:
@@ -93,6 +73,6 @@ const std = @import("std");
const unicode = std.unicode;
const meta = std.meta;
const assert = std.debug.assert;
const format = std.fmt.format;
const Color = @import("color.zig").Color;
const ctlseqs = @import("ctlseqs.zig");
const Style = @This();
+45 -39
View File
@@ -14,73 +14,79 @@ pub fn getTerminalSize() Size {
return .{ .x = ws.col, .y = ws.row };
}
pub fn saveScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.save_screen);
pub fn saveScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.save_screen);
}
pub fn restoreScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.restore_screen);
pub fn restoreScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.restore_screen);
}
pub fn enterAltScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.smcup);
pub fn enterAltScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.smcup);
}
pub fn exitAltScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.rmcup);
pub fn exitAltScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.rmcup);
}
pub fn clearScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.clear_screen);
pub fn clearScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.clear_screen);
}
pub fn hideCursor(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.hide_cursor);
pub fn hideCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.hide_cursor);
}
pub fn showCursor(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.show_cursor);
pub fn showCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor);
}
pub fn resetCursor(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.reset_cursor_shape);
_ = try w.write(ctlseqs.osc12_reset);
pub fn setCursorPositionHome() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home);
}
pub fn setCursorPositionHome(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.home);
pub fn enableMouseSupport() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_set);
}
pub fn enableMouseSupport(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.mouse_set);
}
pub fn disableMouseSupport(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.mouse_reset);
}
pub fn ringBell(w: *std.Io.Writer) !void {
_ = try w.write(&.{7});
pub fn disableMouseSupport() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_reset);
}
pub fn read(buf: []u8) !usize {
return try posix.read(posix.STDIN_FILENO, buf);
}
pub fn write(w: *std.Io.Writer, buf: []const u8) !usize {
return try w.write(buf);
pub fn write(buf: []const u8) !usize {
return try posix.write(posix.STDIN_FILENO, buf);
}
pub fn setCursorPosition(w: *std.Io.Writer, pos: Point) !void {
fn contextWrite(context: @This(), data: []const u8) anyerror!usize {
_ = context;
return try posix.write(posix.STDOUT_FILENO, data);
}
const Writer = std.io.Writer(
@This(),
anyerror,
contextWrite,
);
pub fn writer() Writer {
return .{ .context = .{} };
}
pub fn setCursorPosition(pos: Point) !void {
var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y + 1, pos.x + 1 });
_ = try w.write(value);
_ = try posix.write(posix.STDIN_FILENO, value);
}
pub fn getCursorPosition(w: *std.Io.Writer) !Size {
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 w.write("\x1b[6n");
_ = try posix.write(posix.STDIN_FILENO, "\x1b[6n");
var buf: [64]u8 = undefined;
@@ -185,7 +191,7 @@ pub fn enableRawMode(bak: *posix.termios) !void {
}
/// Reverts `enableRawMode` to restore initial functionality.
pub fn disableRawMode(bak: *const posix.termios) !void {
pub fn disableRawMode(bak: *posix.termios) !void {
try posix.tcsetattr(
posix.STDIN_FILENO,
.FLUSH,
@@ -194,10 +200,10 @@ pub fn disableRawMode(bak: *const posix.termios) !void {
}
// Ref
pub fn canSynchornizeOutput(w: *std.Io.Writer) !bool {
pub fn canSynchornizeOutput() !bool {
// Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it.
_ = try w.write("\x1b[?2026$p");
_ = try posix.write(posix.STDIN_FILENO, "\x1b[?2026$p");
var buf: [64]u8 = undefined;
@@ -226,7 +232,7 @@ const log = std.log.scoped(.terminal);
const std = @import("std");
const mem = std.mem;
const posix = std.posix;
const assert = std.debug.assert;
const code_point = @import("code_point");
const ctlseqs = @import("ctlseqs.zig");
const input = @import("input.zig");
const Key = input.Key;
+33 -68
View File
@@ -34,10 +34,10 @@ pub const Renderer = struct {
@memset(this.screen, .{});
}
pub fn render(this: *@This(), comptime T: type, container: *const T, comptime Model: type, model: *const Model) !void {
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.content(model);
const cells: []const Cell = try container.content();
if (cells.len == 0) return;
@@ -58,7 +58,7 @@ pub const Renderer = struct {
// free immediately
container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(T, element, Model, model);
for (container.elements.items) |*element| try this.render(T, element);
}
pub fn save(this: @This(), writer: anytype) !void {
@@ -74,8 +74,6 @@ pub const Renderer = struct {
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
///
/// ```zig
/// const Model = struct {};
/// var model: Model = .{};
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
/// defer file.close();
///
@@ -83,8 +81,8 @@ pub const Renderer = struct {
/// var renderer: testing.Renderer = .init(allocator, size);
/// defer renderer.deinit();
///
/// try container.handle(&model, .{ .size = size });
/// try renderer.render(@TypeOf(container), &container, Model, &.{});
/// try container.handle(.{ .size = size });
/// try renderer.render(Container(event.SystemEvent), &container);
/// try renderer.save(file.writer());
/// ```
///
@@ -93,8 +91,7 @@ pub const Renderer = struct {
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
///
/// ```zig
/// const Model = struct {};
/// var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
/// .border = .{
/// .color = .green,
/// .sides = .all,
@@ -105,57 +102,20 @@ pub const Renderer = struct {
/// try testing.expectContainerScreen(.{
/// .rows = 20,
/// .cols = 30,
/// }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
/// }, &container, @import("test/container/border.all.zon"));
/// ```
pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, comptime Model: type, expected: []const Cell) !void {
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void {
const allocator = testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
const model: Model = .{};
container.resize(&model, size);
container.reposition(&model, .{});
try renderer.render(T, container, Model, &model);
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), container);
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
}
/// Taken from: https://codeberg.org/atman/zg/src/branch/master/src/DisplayWidth.zig
/// Owned by https://codeberg.org/atman licensed under MIT all credits for this function go to him
fn center(allocator: Allocator, str: []const u8, total_width: usize, pad: []const u8) ![]u8 {
if (str.len > total_width) return error.StrTooLong;
if (str.len == total_width) return try allocator.dupe(u8, str);
if (pad.len > total_width or str.len + pad.len > total_width) return error.PadTooLong;
const margin_width = @divFloor((total_width - str.len), 2);
if (pad.len > margin_width) return error.PadTooLong;
const extra_pad: usize = if (total_width % 2 != str.len % 2) 1 else 0;
const pads = @divFloor(margin_width, pad.len) * 2 + extra_pad;
var result = try allocator.alloc(u8, pads * pad.len + str.len);
var bytes_index: usize = 0;
var pads_index: usize = 0;
while (pads_index < pads / 2) : (pads_index += 1) {
@memcpy(result[bytes_index..][0..pad.len], pad);
bytes_index += pad.len;
}
@memcpy(result[bytes_index..][0..str.len], str);
bytes_index += str.len;
pads_index = 0;
while (pads_index < pads / 2 + extra_pad) : (pads_index += 1) {
@memcpy(result[bytes_index..][0..pad.len], pad);
bytes_index += pad.len;
}
return result;
}
/// This function is intended to be used only in tests. Test if the two
/// provided cell arrays are identical. Usually the `Cell` slices are
/// the contents of a given screen from the `zterm.testing.Renderer`. See
@@ -167,21 +127,28 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer expected_cps.deinit(allocator);
defer expected_cps.deinit();
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer actual_cps.deinit(allocator);
defer actual_cps.deinit();
var allocating_writer = std.Io.Writer.Allocating.init(allocator);
defer allocating_writer.deinit();
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y);
defer output.deinit();
var writer = &allocating_writer.writer;
var buffer = std.io.bufferedWriter(output.writer());
defer buffer.flush() catch {};
const writer = buffer.writer();
var differ = false;
const expected_centered = try center(allocator, "Expected Screen", size.x, " ");
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
defer dwd.deinit();
const dw: DisplayWidth = .{ .data = &dwd };
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
defer allocator.free(expected_centered);
const actual_centered = try center(allocator, "Actual Screen", size.x, " ");
const actual_centered = try dw.center(allocator, "Actual Screen", size.x, " ");
defer allocator.free(actual_centered);
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
@@ -197,8 +164,8 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
if (!expected_cell.eql(actual_cell)) differ = true;
try expected_cps.append(allocator, expected_cell);
try actual_cps.append(allocator, actual_cell);
try expected_cps.append(expected_cell);
try actual_cps.append(actual_cell);
}
// write screens both formatted to buffer
@@ -211,16 +178,13 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
if (!differ) return;
// test failed
var buf: [1024]u8 = undefined;
try buffer.flush();
std.debug.lockStdErr();
defer std.debug.unlockStdErr();
var buffer = std.fs.File.stderr().writer(&buf);
var error_writer = &buffer.interface;
try error_writer.writeAll(writer.buffer[0..writer.end]);
try error_writer.flush();
debug.lockStdErr();
defer debug.unlockStdErr();
const std_writer = std.io.getStdErr().writer();
try std_writer.writeAll(output.items);
return error.TestExpectEqualCells;
}
@@ -231,4 +195,5 @@ const Allocator = std.mem.Allocator;
const event = @import("event.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point;