10 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
23 changed files with 463 additions and 94 deletions

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

@@ -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();
@@ -165,7 +165,7 @@ pub fn main() !void {
}, 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;

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();
@@ -133,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
@@ -149,7 +149,7 @@ pub fn main() !void {
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
renderer.size = .{}; // reset size, such that next resize will cause a full re-draw!
defer app.start() catch @panic("could not start app event loop");
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 = .{

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

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();
@@ -102,7 +102,7 @@ pub fn main() !void {
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

View File

@@ -65,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();
@@ -112,7 +112,7 @@ pub fn main() !void {
}, 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

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;

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

View File

@@ -65,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();
@@ -151,7 +151,7 @@ pub fn main() !void {
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

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

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) {

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

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

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

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

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();
@@ -58,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) {

View File

@@ -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();
@@ -209,7 +209,7 @@ pub fn main() !void {
}, 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

View File

@@ -17,20 +17,43 @@
/// );
/// // 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();
/// 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;
@@ -41,51 +64,65 @@ pub fn App(comptime M: type, comptime E: type) type {
// -> 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(io: std.Io, model: Model) @This() {
pub fn init(gpa: Allocator, io: std.Io, model: Model) @This() {
return .{
.gpa = gpa,
.io = io,
.model = model,
.queue = .{},
.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.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;
@@ -94,13 +131,10 @@ 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.resetCursor();
try terminal.restoreScreen();
try terminal.disableRawMode(&termios);
try terminal.exitAltScreen();
if (this.config.rawMode) try terminal.disableRawMode(&termios);
this.termios = null;
}
}
@@ -126,7 +160,7 @@ pub fn App(comptime M: type, comptime E: type) type {
// 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.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0) catch |err| switch (err) {
error.FileBusy => unreachable,
@@ -146,10 +180,13 @@ pub fn App(comptime M: type, comptime E: type) type {
else => |e| return e,
};
}
var remaining_bytes: usize = 0;
var lines: std.ArrayList(u8) = .empty;
defer lines.deinit(this.gpa);
while (true) {
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
var remaining_bytes: usize = 0;
// non-blocking read
const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) {
error.WouldBlock => {
@@ -159,6 +196,13 @@ pub fn App(comptime M: type, comptime E: type) type {
},
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) {
@@ -399,29 +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 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 } });
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;
};
@@ -472,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

@@ -764,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;
}
},
@@ -815,7 +823,7 @@ pub fn Container(Model: type, Event: type) type {
.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 == .vertical_only 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;
},
}
@@ -862,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;

View File

@@ -8,6 +8,11 @@ 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
@@ -23,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

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;
@@ -123,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

@@ -98,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");