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`.
193 lines
6.4 KiB
Zig
193 lines
6.4 KiB
Zig
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) {});
|