41 Commits

Author SHA1 Message Date
6781c43fcc add(renderer/direct): write newline function to align view if necessary
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m8s
2026-01-27 15:33:35 +01:00
eb36c7c410 fix: correctly read inputs from *stdin* when not using *raw mode*
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m6s
2026-01-23 12:44:48 +01:00
4441f1b933 feat(app): event line provides entire line contents as a single event; add(event): .cancel event
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m5s
Introduce `.cancel` event that is fired when the user sends EOF
in *non raw mode* renderings. The event `.line` now sends the
entire line (even if larger than the internal buffer) but requires
now an `Allocator`.
2026-01-22 08:56:12 +01:00
39fb8af174 fix(app): do not reset remaining bytes after reading
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m15s
2026-01-22 07:15:46 +01:00
bfbe75f8d3 feat(inline): rendering without alternate screen
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (pull_request) Successful in 1m10s
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 55s
Fix issue with growth resize for containers with corresponding
borders, padding and gaps.
2026-01-20 23:09:16 +01:00
89517b2546 fix: add newly introduced parameter for App.start function to all corresponding usages 2026-01-20 15:16:10 +01:00
a71d808250 add: .line core event for line contents in non *raw mode* instead of .key core events
The end of the `.line` event received contents is highlighted by a trailing newline
(which cannot occur before, as that triggers the line event itself).

Add signal handler for SIGCONT which forces a `.resize` event that should re-draw the
contents after continuing a suspended application (i.e. ctrl+z followed by `fg`).
2026-01-20 15:11:19 +01:00
97a240c54d mod: example shows how dynamic sizing is achived that can be independent form the reported terminal size
Setting the cursor with the `Direct` handler will cause the rendering
to halt at that point and leave the cursor at point.

Due to not enabling *raw mode* with the newly introduced `App.start`
configuration options corresponding inputs are only visible to `zterm`
once the input has been completed with a newline. With this it is not
necessary for the renderer to know nothing more than the width of the
terminal (which is implied through the `Container` sizes). Making it
very trivial to implement.
2026-01-20 13:57:55 +01:00
c29c60bd89 WIP make example interactive
Handle inputs as per usual (which however is a bit weak, is it goes through
key by key and not the entire line), batch all events such that all events
are handled before the next frame is rendered. For this the `App.start`
function needs to become configurable, such that it changes the termios as
configured (hence it is currently all commented out for testing).
2026-01-20 11:54:28 +01:00
4a3bec3edc add: Direct Renderer implementation rendering the fixed size of the root Container
A corresponding example has been added, which is very minimalistic as of now,
but will add further functionality to test the corresponding workflow that is
usual in `zterm` such that it in the best case only a swap in the renderer to
switch from alternate mode drawing to direct drawing.
2026-01-20 11:28:37 +01:00
836d7669e5 feat: introduce growth options .horizontal_only and .vertical_only
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m5s
Growing containers only in one specific direction while fixing the
other dimension (which is then required to be provided - just like
before).
2026-01-18 13:36:03 +01:00
1621715ad8 lint: correct typo
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m7s
2026-01-17 12:29:42 +01:00
4874252e8c mod: adapt implementation to zig version 0.15.2
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 54s
2026-01-17 12:05:20 +01:00
e1e8907848 doc: remove hint as builds now use latest zig version 2026-01-17 12:04:52 +01:00
1cb7fca701 mod(ci): change target zig version to *latest* 2026-01-17 12:04:19 +01:00
19d1602d3b ref: omit capture instead of dropping
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 49s
2026-01-12 23:28:30 +01:00
88c7eea356 feat(unicode): accept unicode characters through the app's input handler to be handled correctly; key introduce isUnicode method
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m21s
With the `isUnicode` method the read unicode characters from the user
can be checked against displayable text and rendered on the screen accordingly.
2026-01-12 22:47:20 +01:00
b1a0d60ae3 mod: bump zig master version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m9s
2026-01-06 22:58:10 +01:00
c49c2a5c6d mod(app): export TextField from App to be used in other Element implementations similar to App.Input
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m1s
2025-12-27 19:56:44 +01:00
8b5d3757fc mod(element/Input): split into TextField without associated event to automatically trigger
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m9s
2025-12-27 17:06:07 +01:00
06ab32bdde mod(container): Size property handling for .fixed, .horizontal and .vertical check with assertions required values
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m0s
2025-12-17 22:58:58 +01:00
8e3b43fa61 mod(container): .horizontal and .vertical size.grow respect minSize value if no size.dim is used
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m0s
2025-12-17 22:48:02 +01:00
6b2797cd8c mod(container): .fixed sizing behavior with minSize Container / Element function
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m1s
2025-12-17 22:04:06 +01:00
e972a2ea0f mod(container): integrate minSize call when fit_resize is calculated
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 57s
2025-11-28 15:31:45 +01:00
67bfd90a2c WIP(container): fit_size call minSize for determining the minimal required size of a given Container
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 57s
2025-11-28 15:25:10 +01:00
40fa080af6 WIP(container): use another approach
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 57s
2025-11-28 15:08:58 +01:00
d92f562a57 WIP(container): propagate individual child sizes correctly
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 57s
2025-11-28 15:03:50 +01:00
f7025a0fc2 fix(container): minSize should not add children's sizes, but take max instead
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 57s
2025-11-28 14:34:32 +01:00
41229c13d3 mod(container): provide minSize function for Element implementation to refer for nested structures
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m6s
2025-11-28 14:27:41 +01:00
855594a8c8 mod(element): introduce deinit interface for deinitializing Elements automatically with the Container
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m34s
2025-11-27 21:13:03 +01:00
9488d0b64d feat(terminal/cursor): add support for cursor shape configuration
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m5s
2025-11-20 18:46:58 +01:00
424740d350 feat(terminal/osc12): define cursor color through style of cell that describes the cursor position
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m33s
The `.default` color will reset the cursor color to the terminal's default color
2025-11-19 18:42:13 +01:00
28c733352e feat(event): introduce .bell system event
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 16m37s
2025-11-17 18:30:31 +01:00
3b5507ec1e fix(lint): correct typo
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m4s
2025-11-10 17:18:36 +01:00
38d31fae72 feat(element): parameter *const App.Model
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 55s
The renderer will provide `resize`, `reposition` and `minSize` (for
the `Scrollable` `Element`) with a read-only pointer to the model of
the application (similar to how it is already done for `handle` and
`content`). Every interface function now has the same data that it can
each use for implementing its corresponding task based on local and
shared variables through the element instance and model pointer.
2025-11-10 17:09:29 +01:00
79a0d17a66 feat(io): introduce std.Io parameter for App.init
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m22s
The provided `std.Io` is used for running the input reading and the
rendering asynch. The user can provide their desired `std.Io`
implementation as they wish to use. The examples use `std.Io.Threaded`
as a simple threaded solution.
2025-11-10 16:44:02 +01:00
accbb4c97d mod: bump action dependencies
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 56s
2025-11-05 22:45:56 +01:00
ad32e46bc9 feat(element/scrollable): mouse support for scrollbar interaction
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 55s
Fix an issue with the scrollbar contents to work proper with statically
defined `Container` sizes through its `Properties` sizing option.
2025-11-05 22:08:35 +01:00
8ebab702ac fix(element/scrollable): reduce the number of minSize calls and correctly resize scrollable Container
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m26s
2025-11-05 18:33:16 +01:00
e53bb7880b mod: cleanup TODO and outdated comments 2025-11-05 18:32:49 +01:00
a83e86f8d9 feat(element/scrollable): background configuration
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m30s
2025-11-02 21:00:17 +01:00
34 changed files with 1133 additions and 493 deletions

View File

@@ -15,9 +15,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup zig installation
uses: mlugg/setup-zig@v2
uses: https://codeberg.org/mlugg/setup-zig@v2
with:
version: master
version: latest
- name: Lint check
run: zig fmt --check .
- name: Spell checking
uses: crate-ci/typos@v1.39.0
with:
config: ./.typos-config
- name: Run tests
run: zig build --release=fast
- name: Release build artifacts

View File

@@ -14,13 +14,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup zig installation
uses: mlugg/setup-zig@v2
uses: https://codeberg.org/mlugg/setup-zig@v2
with:
version: master
version: latest
- name: Lint check
run: zig fmt --check --exclude src/test .
- name: Spell checking
uses: crate-ci/typos@v1.25.0
uses: crate-ci/typos@v1.39.0
with:
config: ./.typos-config
- name: Run tests

View File

@@ -2,9 +2,6 @@
`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:

View File

@@ -24,6 +24,8 @@ 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;
@@ -73,6 +75,8 @@ 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,

View File

@@ -87,7 +87,7 @@ const InputField = struct {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
if (key.isAscii()) try this.input.append(this.allocator, key.cp);
if (key.isUnicode()) try this.input.append(this.allocator, key.cp);
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
this.queue.push(.{ .accept = try this.input.toOwnedSlice(this.allocator) });
@@ -133,7 +133,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -153,7 +153,7 @@ pub fn main() !void {
defer container.deinit();
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.rectangle = .{ .fill = .lightgrey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 10 },
@@ -161,11 +161,11 @@ pub fn main() !void {
}, input_field.element()));
const nested_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.rectangle = .{ .fill = .lightgrey },
}, spinner.element());
try container.append(nested_container);
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
var framerate: u64 = 60;
@@ -228,8 +228,8 @@ pub fn main() !void {
}
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -1,5 +1,5 @@
const QuitText = struct {
const text = "Press ctrl+c to quit. Press ctrl+n to launch helix.";
const text = "Press ctrl+c to quit. Press ctrl+n to launch `vim`.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
@@ -27,13 +27,12 @@ const QuitText = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -57,15 +56,14 @@ pub fn main() !void {
},
}, .{});
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.rectangle = .{ .fill = .lightgreen },
}, .{}));
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.rectangle = .{ .fill = .lightgreen },
}, .{}));
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.rectangle = .{ .fill = .lightgreen },
}, .{}));
defer box.deinit();
var scrollable: App.Scrollable = .init(box, .disabled);
@@ -92,7 +90,7 @@ pub fn main() !void {
.direction = .vertical,
},
.border = .{
.color = .light_blue,
.color = .lightblue,
.sides = .all,
},
}, .{});
@@ -135,7 +133,7 @@ pub fn main() !void {
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -151,8 +149,8 @@ pub fn main() !void {
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
renderer.size = .{}; // reset size, such that next resize will cause a full re-draw!
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
defer app.start(.full) catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"vim"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
.err = .{
.err = err,
@@ -181,8 +179,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

192
examples/direct.zig Normal file
View File

@@ -0,0 +1,192 @@
const QuitText = struct {
const text = "Press ctrl+c 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() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var allocator: std.heap.DebugAllocator(.{}) = .init;
defer if (allocator.deinit() == .leak) log.err("memory leak", .{});
const gpa = allocator.allocator();
var app: App = .init(gpa, .{}, .{});
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); // needs to become configurable, as what should be enabled / disabled (i.e. show cursor, hide cursor, use alternate screen, etc.)
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
event: while (true) {
// batch events since last iteration
const len = blk: {
app.queue.poll();
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
// handle events
for (0..len) |_| {
const event = app.queue.pop();
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 renderer.writeCtrlDWithNewline(),
.line => |line| {
defer gpa.free(line);
log.debug("{s}", .{line});
},
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break :event,
else => {},
}
}
// if there are more events to process continue handling them otherwise I can render the next frame
if (app.queue.len() > 0) continue :event;
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(struct {}, union(enum) {});

View File

@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -55,7 +55,7 @@ pub fn main() !void {
var alignment: App.Alignment = .init(quit_container, .center);
try container.append(try .init(allocator, .{}, alignment.element()));
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -83,8 +83,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -82,7 +82,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -99,10 +99,10 @@ pub fn main() !void {
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .lightgrey } }, element));
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -132,8 +132,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -65,13 +65,11 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black));
defer input_field.deinit();
var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black, .blue));
var mouse_draw: MouseDraw = .{};
var second_mouse_draw: MouseDraw = .{};
var quit_text: QuitText = .{};
@@ -86,7 +84,7 @@ pub fn main() !void {
defer container.deinit();
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.rectangle = .{ .fill = .lightgrey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 1 },
@@ -98,7 +96,7 @@ pub fn main() !void {
.sides = .all,
.color = .black,
},
.rectangle = .{ .fill = .light_grey },
.rectangle = .{ .fill = .lightgrey },
.layout = .{
.separator = .{
.enabled = true,
@@ -107,14 +105,14 @@ pub fn main() !void {
},
}, .{});
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.rectangle = .{ .fill = .lightgrey },
}, mouse_draw.element()));
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.rectangle = .{ .fill = .lightgrey },
}, second_mouse_draw.element()));
try container.append(nested_container);
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -146,8 +144,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -85,7 +85,7 @@ pub fn main() !void {
});
try container.append(try App.Container.init(allocator, .{}, progress.element()));
}
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
var framerate: u64 = 60;
@@ -152,8 +152,8 @@ pub fn main() !void {
}
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -51,7 +51,7 @@ pub fn main() !void {
try container.append(try .init(allocator, .{}, radiobutton.element()));
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -80,8 +80,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -56,7 +56,6 @@ const HelloWorldText = packed struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer {
const deinit_status = gpa.deinit();
@@ -66,7 +65,7 @@ pub fn main() !void {
}
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -87,24 +86,23 @@ pub fn main() !void {
},
}, .{});
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.rectangle = .{ .fill = .lightgreen },
.size = .{
.dim = .{ .y = 30 },
},
}, .{}));
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.rectangle = .{ .fill = .lightgreen },
.size = .{
.dim = .{ .y = 5 },
},
}, element));
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.rectangle = .{ .fill = .lightgreen },
.size = .{
.dim = .{ .y = 2 },
},
}, .{}));
defer top_box.deinit();
var bottom_box = try App.Container.init(allocator, .{
.border = .{
@@ -132,7 +130,6 @@ pub fn main() !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 = .{
@@ -148,13 +145,13 @@ pub fn main() !void {
defer container.deinit();
// place empty container containing the element of the scrollable Container.
var scrollable_top: App.Scrollable = .init(top_box, .enabled(.grey));
var scrollable_top: App.Scrollable = .init(top_box, .enabled(.default, false));
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white));
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true));
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -182,8 +179,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -52,7 +52,7 @@ pub fn main() !void {
defer container.deinit();
try container.append(try .init(allocator, .{}, selection.element()));
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -80,8 +80,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -97,7 +97,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -116,7 +116,7 @@ pub fn main() !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();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) {
@@ -143,8 +143,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -69,7 +69,7 @@ pub fn main() !void {
}
defer container.deinit(); // also de-initializes the children
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -99,8 +99,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -61,7 +61,7 @@ pub fn main() !void {
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -91,8 +91,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -77,7 +77,7 @@ pub fn main() !void {
}
defer container.deinit(); // also de-initializes the children
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -107,8 +107,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -60,7 +60,7 @@ pub fn main() !void {
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -90,8 +90,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -50,7 +50,6 @@ pub fn main() !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
@@ -59,7 +58,7 @@ pub fn main() !void {
var scrollable: App.Scrollable = .init(box, .disabled);
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) {
@@ -86,8 +85,8 @@ pub fn main() !void {
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -117,7 +117,7 @@ const TextStyles = struct {
};
}
fn minSize(ctx: *anyopaque, size: zterm.Point) zterm.Point {
fn minSize(ctx: *anyopaque, _: *const App.Model, size: zterm.Point) zterm.Point {
_ = ctx;
_ = size;
return .{
@@ -178,7 +178,7 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -204,15 +204,12 @@ pub fn main() !void {
},
}, table_head.element()));
var box = try App.Container.init(allocator, .{
var scrollable: App.Scrollable = .init(try .init(allocator, .{
.layout = .{ .direction = .vertical },
}, text_styles.element());
defer box.deinit();
var scrollable: App.Scrollable = .init(box, .disabled);
}, text_styles.element()), .enabled(.white, true));
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
@@ -250,8 +247,8 @@ pub fn main() !void {
}
}
container.resize(try renderer.resize());
container.reposition(.{});
container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
}

View File

@@ -16,75 +16,114 @@
/// union(enum) {}, // no additional user event's
/// );
/// // later on create an `App` instance and start the event loop
/// var app: App = .init(.{}); // provide instance of the model that shall be used
/// try app.start();
/// 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
/// ```
pub fn App(comptime M: type, comptime E: type) type {
if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`");
if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`.");
return struct {
gpa: Allocator,
io: std.Io,
model: Model,
queue: Queue,
thread: ?Thread = null,
quit_event: Thread.ResetEvent,
termios: ?posix.termios = null,
winch_registered: bool = false,
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,
};
};
// 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(_: i32) 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);
}
/// registered CONT handler to force a complete redraw
fn handleCont(_: i32) callconv(.c) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE this does not have to be done if in-band resize events are supported
// -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize);
}
pub fn init(model: Model) @This() {
pub fn init(gpa: Allocator, io: std.Io, model: Model) @This() {
return .{
.gpa = gpa,
.io = io,
.model = model,
.queue = .{},
.quit_event = .unset,
.quit_event = .{},
.config = undefined,
};
}
pub fn start(this: *@This()) !void {
pub fn start(this: *@This(), config: TerminalConfiguration) !void {
this.config = config;
if (this.thread) |_| return;
// post init event (as the very first element to be in the queue - event loop)
this.postEvent(.init);
if (!this.winch_registered) {
if (!this.handler_registered) {
handler_ctx = this;
var act = posix.Sigaction{
posix.sigaction(posix.SIG.WINCH, &.{
.handler = .{ .handler = handleWinch },
.mask = posix.sigemptyset(),
.flags = 0,
};
posix.sigaction(posix.SIG.WINCH, &act, null);
this.winch_registered = true;
}, null);
posix.sigaction(posix.SIG.CONT, &.{
.handler = .{ .handler = handleCont },
.mask = posix.sigemptyset(),
.flags = 0,
}, null);
this.handler_registered = true;
}
this.quit_event.reset();
this.thread = try Thread.spawn(.{}, @This().run, .{this});
var termios: posix.termios = undefined;
try terminal.enableRawMode(&termios);
if (this.config.rawMode) try terminal.enableRawMode(&termios);
if (this.termios) |_| {} else this.termios = termios;
try terminal.enterAltScreen();
try terminal.saveScreen();
try terminal.hideCursor();
try terminal.enableMouseSupport();
if (this.config.altScreen) try terminal.enterAltScreen();
if (this.config.saveScreen) try terminal.saveScreen();
if (this.config.hideCursor) try terminal.hideCursor();
if (this.config.altScreen and this.config.rawMode) try terminal.enableMouseSupport();
}
pub fn interrupt(this: *@This()) !void {
this.quit_event.set();
try terminal.disableMouseSupport();
try terminal.restoreScreen();
try terminal.exitAltScreen();
if (this.thread) |thread| {
if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport();
if (this.config.saveScreen) try terminal.restoreScreen();
if (this.config.altScreen) try terminal.exitAltScreen();
if (this.thread) |*thread| {
thread.join();
this.thread = null;
}
@@ -92,18 +131,16 @@ pub fn App(comptime M: type, comptime E: type) type {
pub fn stop(this: *@This()) !void {
try this.interrupt();
if (this.config.hideCursor) try terminal.showCursor();
if (this.config.saveScreen) try terminal.resetCursor();
if (this.termios) |termios| {
try terminal.disableMouseSupport();
try terminal.showCursor();
try terminal.restoreScreen();
try terminal.disableRawMode(&termios);
try terminal.exitAltScreen();
if (this.config.rawMode) try terminal.disableRawMode(&termios);
this.termios = null;
}
this.termios = null;
}
/// Quit the application loop.
/// This will stop the internal input thread and post a **.quit** `Event`.
/// This will cancel the internal input thread and post a **.quit** `Event`.
pub fn quit(this: *@This()) void {
this.quit_event.set();
this.postEvent(.quit);
@@ -121,13 +158,52 @@ pub fn App(comptime M: type, comptime E: type) type {
fn run(this: *@This()) !void {
// thread to read user inputs
var buf: [256]u8 = undefined;
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.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
fl_flags |= 1 << @bitOffsetOf(posix.system.O, "NONBLOCK");
_ = posix.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
}
var remaining_bytes: usize = 0;
var lines: std.ArrayList(u8) = .empty;
defer lines.deinit(this.gpa);
while (true) {
// 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!
// non-blocking read
const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) {
error.WouldBlock => {
// wait a bit
std.Thread.sleep(20);
continue;
},
else => return err,
} + remaining_bytes;
remaining_bytes = 0;
if (read_bytes == 0) {
// received <EOF>
this.postEvent(.cancel);
continue;
}
// escape key presses
if (buf[0] == 0x1b and read_bytes > 1) {
switch (buf[1]) {
@@ -154,9 +230,9 @@ pub fn App(comptime M: type, comptime E: type) type {
if (read_bytes < 3) continue;
// We start iterating at index 2 to get past the '['
const sequence = for (buf[2..], 2..) |b, i| {
const sequence: []u8 = blk: for (buf[2..], 2..) |b, i| {
switch (b) {
0x40...0xFF => break buf[0 .. i + 1],
0x40...0xFF => break :blk buf[0 .. i + 1],
else => continue,
}
} else continue;
@@ -287,18 +363,19 @@ 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;
const mouse: Mouse = .{
.button = button,
.x = px -| 1,
.y = py -| 1,
.kind = blk: {
if (motion and button != Mouse.Button.none) break :blk .drag;
if (motion and button == Mouse.Button.none) break :blk .motion;
if (sequence[sequence.len - 1] == 'm') break :blk .release;
break :blk .press;
this.postEvent(.{
.mouse = .{
.button = button,
.x = px -| 1,
.y = py -| 1,
.kind = blk: {
if (motion and button != Mouse.Button.none) break :blk .drag;
if (motion and button == Mouse.Button.none) break :blk .motion;
if (sequence[sequence.len - 1] == 'm') break :blk .release;
break :blk .press;
},
},
};
this.postEvent(.{ .mouse = mouse });
});
},
'c' => {
// Primary DA (CSI ? Pm c)
@@ -366,26 +443,49 @@ pub fn App(comptime M: type, comptime E: type) type {
},
}
} else {
const b = buf[0];
const key: Key = switch (b) {
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
0x08 => .{ .cp = input.Backspace },
0x09 => .{ .cp = input.Tab },
0x0a => .{ .cp = 'j', .mod = .{ .ctrl = true } },
0x0d => .{ .cp = input.Enter },
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
0x1b => escape: {
assert(read_bytes == 1);
break :escape .{ .cp = input.Escape };
},
0x7f => .{ .cp = input.Backspace },
else => {
var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..read_bytes], .i = 0 };
while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } });
continue;
},
};
this.postEvent(.{ .key = key });
if (this.config.rawMode) {
const b = buf[0];
const key: Key = switch (b) {
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
0x08 => .{ .cp = input.Backspace },
0x09 => .{ .cp = input.Tab },
0x0a => .{ .cp = 'j', .mod = .{ .ctrl = true } },
0x0d => .{ .cp = input.Enter },
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
0x1b => escape: {
assert(read_bytes == 1);
break :escape .{ .cp = input.Escape };
},
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| 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
},
};
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') 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();
this.postEvent(.cancel);
}
}
}
}
continue;
};
@@ -396,6 +496,7 @@ pub fn App(comptime M: type, comptime E: type) type {
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
terminal.disableMouseSupport() catch {};
terminal.showCursor() catch {};
terminal.resetCursor() catch {};
terminal.restoreScreen() catch {};
terminal.disableRawMode(&.{
.iflag = .{},
@@ -418,12 +519,13 @@ pub fn App(comptime M: type, comptime E: type) type {
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, 256);
pub const Queue = queue.Queue(Event, 512);
};
}
@@ -434,6 +536,7 @@ 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 event = @import("event.zig");
const input = @import("input.zig");

View File

@@ -1,6 +1,5 @@
//! 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 = &.{} },
@@ -26,7 +25,7 @@ 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 = .light_green, .bg = .black, .emphasis = &.{.underline} } },
.{ .cp = 's', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
};
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
@@ -46,7 +45,7 @@ test "utf-8 styled text" {
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
.{ .cp = '─', .style = .{ .emphasis = &.{} } },
.{ .cp = '┄', .style = .{ .emphasis = &.{} } },
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
.{ .cp = '┘', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
};
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);

View File

@@ -1,13 +1,13 @@
pub const Color = enum(u8) {
default = 0,
black = 16,
light_red = 1,
light_green,
light_yellow,
light_blue,
light_magenta,
light_cyan,
light_grey,
lightred = 1,
lightgreen,
lightyellow,
lightblue,
lightmagenta,
lightcyan,
lightgrey,
grey,
red,
green,
@@ -18,8 +18,6 @@ pub const Color = enum(u8) {
white,
// TODO add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
// TODO might be useful to use the std.ascii stuff!
pub inline fn write(this: Color, writer: *std.Io.Writer, comptime coloring: enum { fg, bg, ul }) !void {
if (this == .default) {
switch (coloring) {

View File

@@ -599,9 +599,11 @@ pub const Layout = packed struct {
/// Sizing options which should be used by the `Container`
pub const Size = packed struct {
dim: Point = .{},
grow: enum(u2) {
grow: enum(u3) {
both,
fixed,
vertical_only,
horizontal_only,
vertical,
horizontal,
} = .both,
@@ -648,16 +650,17 @@ pub fn Container(Model: type, Event: type) type {
pub fn deinit(this: *@This()) void {
for (this.elements.items) |*element| element.deinit();
this.elements.deinit(this.allocator);
this.element.deinit();
}
pub fn append(this: *@This(), element: @This()) !void {
try this.elements.append(this.allocator, element);
}
pub fn reposition(this: *@This(), origin: Point) void {
pub fn reposition(this: *@This(), model: *const Model, origin: Point) void {
const layout = this.properties.layout;
this.origin = origin;
this.element.reposition(origin);
this.element.reposition(model, origin);
var offset = origin.add(.{
.x = Layout.getAbsolutePadding(layout.padding.left, this.size.x),
@@ -669,7 +672,7 @@ pub fn Container(Model: type, Event: type) type {
if (sides.top) offset.y += 1;
for (this.elements.items) |*child| {
child.reposition(offset);
child.reposition(model, offset);
switch (layout.direction) {
.horizontal => offset.x += child.size.x + layout.gap,
@@ -684,7 +687,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()) Point {
fn fit_resize(this: *@This(), model: *const Model) 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) {
@@ -709,7 +712,7 @@ pub fn Container(Model: type, Event: type) type {
};
for (this.elements.items) |*child| {
const child_size = child.fit_resize();
const child_size = child.fit_resize(model);
switch (layout.direction) {
.horizontal => {
size.x += child_size.x;
@@ -722,24 +725,29 @@ 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 => 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,
.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);
},
.vertical => .{
.x = this.properties.size.dim.x,
.y = @max(size.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);
},
};
return this.size;
}
/// growable implicitly requires the root `Container` to have a set a size property to the size of the available terminal screen
fn grow_resize(this: *@This(), max_size: Point) void {
fn grow_resize(this: *@This(), model: *const Model, 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)),
@@ -756,23 +764,31 @@ pub fn Container(Model: type, Event: type) type {
const sides = this.properties.border.sides;
switch (layout.direction) {
.horizontal => {
.vertical => {
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;
}
},
.horizontal => {
if (sides.top) {
available -|= 1;
}
if (sides.bottom) {
available -|= 1;
}
if (sides.left) {
remainder -|= 1;
}
if (sides.right) {
remainder -|= 1;
}
},
@@ -793,21 +809,21 @@ pub fn Container(Model: type, Event: type) type {
if (growable_children == 0) first_growable_child = child;
growable_children += 1;
},
.horizontal => if (layout.direction == .horizontal) {
.horizontal, .horizontal_only => if (layout.direction == .horizontal) {
if (growable_children == 0) first_growable_child = child;
growable_children += 1;
},
.vertical => if (layout.direction == .vertical) {
.vertical, .vertical_only => 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 == .both) {
.horizontal => if (child.properties.size.grow == .vertical or child.properties.size.grow == .vertical_only or child.properties.size.grow == .both) {
child.size.y = available;
},
.vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .both) {
.vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .horizontal_only or child.properties.size.grow == .both) {
child.size.x = available;
},
}
@@ -825,8 +841,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) continue,
.vertical => if (child.properties.size.grow == .horizontal) continue,
.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,
}
const size = switch (layout.direction) {
@@ -854,23 +870,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) {
.horizontal => if (child.properties.size.grow != .vertical and child.properties.size.grow != .vertical_only) {
child.size.x += size_to_correct;
remainder -|= size_to_correct;
},
.vertical => if (child.properties.size.grow != .horizontal) {
.vertical => if (child.properties.size.grow != .horizontal and child.properties.size.grow != .horizontal_only) {
child.size.y += size_to_correct;
remainder -|= size_to_correct;
},
}
if (overflow > 0) {
switch (layout.direction) {
.horizontal => if (child.properties.size.grow != .vertical) {
.horizontal => if (child.properties.size.grow != .vertical and child.properties.size.grow != .vertical_only) {
child.size.x += 1;
overflow -|= 1;
remainder -|= 1;
},
.vertical => if (child.properties.size.grow != .horizontal) {
.vertical => if (child.properties.size.grow != .horizontal and child.properties.size.grow != .horizontal_only) {
child.size.y += 1;
overflow -|= 1;
remainder -|= 1;
@@ -881,28 +897,54 @@ pub fn Container(Model: type, Event: type) type {
}
}
this.element.resize(this.size);
for (this.elements.items) |*child| child.grow_resize(child.size);
this.element.resize(model, this.size);
for (this.elements.items) |*child| child.grow_resize(model, child.size);
}
pub fn resize(this: *@This(), size: Point) void {
pub fn resize(this: *@This(), model: *const Model, size: Point) void {
// NOTE assume that this function is only called for the root `Container`
this.size = size;
const fit_size = this.fit_resize();
// if (fit_size.y > size.y or fit_size.x > size.x) @panic("error: cannot render in available space");
switch (this.properties.size.grow) {
.both => this.size = Point.max(size, fit_size),
.fixed => {},
.horizontal => this.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 => .{
.x = @max(size.x, fit_size.x),
.y = size.y,
},
.vertical => this.size = .{
.vertical => .{
.x = size.x,
.y = @max(size.y, fit_size.y),
},
};
this.grow_resize(model, 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,
},
};
}
this.grow_resize(this.size);
const element_size = this.element.minSize(model, size);
return .max(element_size, min_size);
}
pub fn handle(this: *const @This(), model: *Model, event: Event) !void {
@@ -915,6 +957,11 @@ pub fn Container(Model: type, Event: type) type {
relative_mouse.y -= this.origin.y;
try this.element.handle(model, .{ .mouse = relative_mouse });
},
.bell => {
// ring the terminal bell and do not propagate the event any further
_ = try terminal.ringBell();
return;
},
else => try this.element.handle(model, event),
}
for (this.elements.items) |*element| try element.handle(model, event);
@@ -961,6 +1008,7 @@ 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;

View File

@@ -50,6 +50,7 @@ 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";
@@ -139,5 +140,6 @@ 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

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,19 @@ 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
@@ -17,6 +28,11 @@ 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
@@ -110,7 +126,7 @@ pub fn isTaggedUnion(comptime T: type) bool {
/// Determine whether the provided type `T` is a `struct`.
pub fn isStruct(comptime T: type) bool {
return switch (@typeInfo(T)) {
.@"struct" => |_| true,
.@"struct" => true,
else => false,
};
}

View File

@@ -65,7 +65,30 @@ pub const Key = packed struct {
return meta.eql(this, other);
}
// TODO might be useful to use the std.ascii stuff!
/// 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.)
}
/// 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

View File

@@ -1,7 +1,7 @@
// 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,

View File

@@ -91,6 +91,7 @@ pub const Buffered = struct {
var 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;
@@ -104,6 +105,7 @@ pub const Buffered = struct {
.x = @truncate(col),
.y = @truncate(row),
};
try cvs.style.set_cursor_style(&writer);
}
if (cs.eql(cvs)) continue;
@@ -122,6 +124,95 @@ pub const Buffered = struct {
}
};
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()) !void {
_ = this;
_ = try terminal.write("^D\n");
}
pub fn writeNewline(this: *@This()) !void {
_ = this;
_ = try terminal.write("\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()) !void {
var writer = terminal.writer();
for (0..this.size.y) |row| {
for (0..this.size.x) |col| {
const idx = (row * this.size.x) + col;
const cvs = this.screen[idx];
try cvs.value(&writer);
if (cvs.style.cursor) return; // that's where the cursor should be left!
}
}
}
};
const std = @import("std");
const meta = std.meta;
const assert = std.debug.assert;

View File

@@ -12,6 +12,8 @@ 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,
@@ -36,11 +38,29 @@ 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);
}
// TODO might be useful to use the std.ascii stuff!
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)});
}
pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void {
var buffer: [4]u8 = undefined;
@@ -74,4 +94,5 @@ const unicode = std.unicode;
const meta = std.meta;
const assert = std.debug.assert;
const Color = @import("color.zig").Color;
const ctlseqs = @import("ctlseqs.zig");
const Style = @This();

View File

@@ -42,6 +42,11 @@ pub fn showCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor);
}
pub fn resetCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.reset_cursor_shape);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.osc12_reset);
}
pub fn setCursorPositionHome() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home);
}
@@ -54,6 +59,10 @@ pub fn disableMouseSupport() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_reset);
}
pub fn ringBell() !void {
_ = try posix.write(posix.STDIN_FILENO, &.{7});
}
pub fn read(buf: []u8) !usize {
return try posix.read(posix.STDIN_FILENO, buf);
}
@@ -89,7 +98,7 @@ pub fn setCursorPosition(pos: Point) !void {
_ = try posix.write(posix.STDIN_FILENO, value);
}
pub fn getCursorPosition() !Size.Position {
pub fn getCursorPosition() !Size {
// Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it.
_ = try posix.write(posix.STDIN_FILENO, "\x1b[6n");

View File

@@ -112,9 +112,11 @@ pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, compt
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(T, container, Model, &.{});
const model: Model = .{};
container.resize(&model, size);
container.reposition(&model, .{});
try renderer.render(T, container, Model, &model);
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
}
@@ -209,14 +211,16 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
if (!differ) return;
// test failed
debug.lockStdErr();
defer debug.unlockStdErr();
var buf: [1024]u8 = undefined;
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();
var stdout_buffer: [1024]u8 = undefined;
var stdout = std.fs.File.stdout().writer(&stdout_buffer);
const stdout_writer = &stdout.interface;
try stdout_writer.writeAll(writer.buffer[0..writer.end]);
try stdout_writer.flush();
return error.TestExpectEqualCells;
}