mod: bump zig version to 0.16.0

This commit is contained in:
2026-05-14 11:05:55 +02:00
parent bdf58f5102
commit 24a08e0e62
25 changed files with 721 additions and 701 deletions
+9 -6
View File
@@ -125,15 +125,18 @@ const InputField = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+36 -32
View File
@@ -24,16 +24,19 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const gpa = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(gpa, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator(); var renderer = zterm.Renderer.Buffered.init(gpa);
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
var quit_text: QuitText = .{}; var quit_text: QuitText = .{};
@@ -41,7 +44,7 @@ pub fn main() !void {
// TODO what should the demo application do? // TODO what should the demo application do?
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side? // - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
// - on the left some buttons? // - on the left some buttons?
var box = try App.Container.init(allocator, .{ var box = try App.Container.init(gpa, .{
.border = .{ .border = .{
.color = .blue, .color = .blue,
.sides = .all, .sides = .all,
@@ -55,19 +58,19 @@ pub fn main() !void {
.dim = .{ .y = 90 }, .dim = .{ .y = 90 },
}, },
}, .{}); }, .{});
try box.append(try App.Container.init(allocator, .{ try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen }, .rectangle = .{ .fill = .lightgreen },
}, .{})); }, .{}));
try box.append(try App.Container.init(allocator, .{ try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen }, .rectangle = .{ .fill = .lightgreen },
}, .{})); }, .{}));
try box.append(try App.Container.init(allocator, .{ try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen }, .rectangle = .{ .fill = .lightgreen },
}, .{})); }, .{}));
var scrollable: App.Scrollable = .init(box, .disabled); var scrollable: App.Scrollable = .init(box, .disabled);
var container = try App.Container.init(allocator, .{ var container = try App.Container.init(gpa, .{
.layout = .{ .layout = .{
.gap = 2, .gap = 2,
.separator = .{ .enabled = true }, .separator = .{ .enabled = true },
@@ -75,9 +78,9 @@ pub fn main() !void {
.direction = .horizontal, .direction = .horizontal,
}, },
}, quit_text.element()); }, quit_text.element());
try container.append(try App.Container.init(allocator, .{}, scrollable.element())); try container.append(try App.Container.init(gpa, .{}, scrollable.element()));
var nested_container: App.Container = try .init(allocator, .{ var nested_container: App.Container = try .init(gpa, .{
.layout = .{ .layout = .{
.direction = .vertical, .direction = .vertical,
.separator = .{ .separator = .{
@@ -85,7 +88,7 @@ pub fn main() !void {
}, },
}, },
}, .{}); }, .{});
var inner_container: App.Container = try .init(allocator, .{ var inner_container: App.Container = try .init(gpa, .{
.layout = .{ .layout = .{
.direction = .vertical, .direction = .vertical,
}, },
@@ -94,7 +97,7 @@ pub fn main() !void {
.sides = .all, .sides = .all,
}, },
}, .{}); }, .{});
try inner_container.append(try .init(allocator, .{ try inner_container.append(try .init(gpa, .{
.rectangle = .{ .rectangle = .{
.fill = .blue, .fill = .blue,
}, },
@@ -103,7 +106,7 @@ pub fn main() !void {
.dim = .{ .y = 5 }, .dim = .{ .y = 5 },
}, },
}, .{})); }, .{}));
try inner_container.append(try .init(allocator, .{ try inner_container.append(try .init(gpa, .{
.rectangle = .{ .rectangle = .{
.fill = .red, .fill = .red,
}, },
@@ -112,20 +115,20 @@ pub fn main() !void {
.dim = .{ .y = 5 }, .dim = .{ .y = 5 },
}, },
}, .{})); }, .{}));
try inner_container.append(try .init(allocator, .{ try inner_container.append(try .init(gpa, .{
.rectangle = .{ .rectangle = .{
.fill = .green, .fill = .green,
}, },
}, .{})); }, .{}));
try nested_container.append(inner_container); try nested_container.append(inner_container);
try nested_container.append(try .init(allocator, .{ try nested_container.append(try .init(gpa, .{
.size = .{ .size = .{
.grow = .horizontal, .grow = .horizontal,
.dim = .{ .y = 1 }, .dim = .{ .y = 1 },
}, },
}, .{})); }, .{}));
try container.append(nested_container); try container.append(nested_container);
try container.append(try App.Container.init(allocator, .{ try container.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .blue }, .rectangle = .{ .fill = .blue },
.size = .{ .size = .{
.dim = .{ .x = 30 }, .dim = .{ .x = 30 },
@@ -133,25 +136,26 @@ pub fn main() !void {
}, .{})); }, .{}));
defer container.deinit(); // also de-initializes the children defer container.deinit(); // also de-initializes the children
try app.start(.full); try app.start(.full, w);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop // event loop
while (true) { while (true) {
const event = app.nextEvent(); const event = app.nextEvent() catch break; // error indicates cancled Io
log.debug("received event: {s}", .{@tagName(event)}); log.debug("received event: {s}", .{@tagName(event)});
// pre event handling // pre event handling
switch (event) { switch (event) {
.key => |key| { .key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(); if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try app.quit();
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) { if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.stop(); try app.stop(w);
defer renderer.clear() catch @panic("could not clear the screen"); try w.flush();
defer app.start(.full) catch @panic("could not start app event loop"); defer renderer.clear(w) catch @panic("could not clear the screen");
var child = std.process.Child.init(&.{"vim"}, allocator); defer app.start(.full, w) catch @panic("could not start app event loop");
_ = child.spawnAndWait() catch |err| app.postEvent(.{ var child = try std.process.spawn(io, .{ .argv = &.{"vim"} });
_ = child.wait(io) catch |err| try app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Spawning $EDITOR failed", .msg = "Spawning $EDITOR failed",
@@ -165,7 +169,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // NOTE returned errors should be propagated back to the application
container.handle(&app.model, event) catch |err| app.postEvent(.{ container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -178,10 +182,10 @@ pub fn main() !void {
else => {}, else => {},
} }
container.resize(&app.model, try renderer.resize()); container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{}); container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush(w);
} }
} }
+17 -15
View File
@@ -69,15 +69,17 @@ const Prompt = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const gpa = init.gpa;
const io = init.io;
var allocator: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(gpa, io, .{});
defer if (allocator.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &buffer);
const w = &writer.interface;
const gpa = allocator.allocator();
var app: App = .init(gpa, .{}, .{});
var renderer = zterm.Renderer.Direct.init(gpa); var renderer = zterm.Renderer.Direct.init(gpa);
defer renderer.deinit(); defer renderer.deinit();
@@ -127,16 +129,16 @@ pub fn main() !void {
}, },
}, prompt.element())); }, 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.) try app.start(.direct, w); // needs to become configurable, as what should be enabled / disabled (i.e. show cursor, hide cursor, use alternate screen, etc.)
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop // event loop
event: while (true) { event: while (true) {
// batch events since last iteration // batch events since last iteration
const len = blk: { const len = blk: {
app.queue.poll(); try app.queue.poll(io);
app.queue.lock(); try app.queue.lock(io);
defer app.queue.unlock(); defer app.queue.unlock(io);
break :blk app.queue.len(); break :blk app.queue.len();
}; };
@@ -149,14 +151,14 @@ pub fn main() !void {
switch (event) { switch (event) {
// NOTE draw the character with the ctrl indication and a newline to make sure the rendering stays consistent // NOTE draw the character with the ctrl indication and a newline to make sure the rendering stays consistent
.cancel => { .cancel => {
app.quit(); try app.quit();
break :event; break :event;
}, },
.line => |line| { .line => |line| {
defer gpa.free(line); defer gpa.free(line);
log.debug("{s}", .{line}); log.debug("{s}", .{line});
if (std.mem.eql(u8, line, "q\n")) app.quit(); if (std.mem.eql(u8, line, "q\n")) try app.quit();
}, },
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback // 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 }), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
@@ -164,7 +166,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // NOTE returned errors should be propagated back to the application
container.handle(&app.model, event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| try app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -175,7 +177,7 @@ pub fn main() !void {
container.resize(&app.model, try renderer.resize()); container.resize(&app.model, try renderer.resize());
container.reposition(&app.model, .{}); container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush(w);
} }
} }
+9 -6
View File
@@ -24,15 +24,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+13 -16
View File
@@ -40,7 +40,7 @@ const Clickable = struct {
}; };
} }
fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { fn handle(ctx: *anyopaque, io: std.Io, _: *App.Model, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) { .mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
@@ -49,7 +49,7 @@ const Clickable = struct {
value %= 17; value %= 17;
if (value == 0) value = 1; if (value == 0) value = 1;
this.color = @enumFromInt(value); this.color = @enumFromInt(value);
this.queue.push(.{ .click = @tagName(mouse.button) }); try this.queue.push(io, .{ .click = @tagName(mouse.button) });
}, },
else => {}, else => {},
} }
@@ -74,15 +74,12 @@ const Clickable = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -102,24 +99,24 @@ 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 = .lightgrey } }, element));
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element())); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
try app.start(.full); try app.start(.full, w);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop // event loop
while (true) { while (true) {
const event = app.nextEvent(); const event = try app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)}); log.debug("received event: {s}", .{@tagName(event)});
// pre event handling // pre event handling
switch (event) { switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try app.quit(),
.click => |b| log.info("Clicked with mouse using Button: {s}", .{b}), .click => |b| log.info("Clicked with mouse using Button: {s}", .{b}),
.accept => log.info("Clicked built-in button using the mouse", .{}), .accept => log.info("Clicked built-in button using the mouse", .{}),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {}, else => {},
} }
container.handle(&app.model, event) catch |err| app.postEvent(.{ container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -132,10 +129,10 @@ pub fn main() !void {
else => {}, else => {},
} }
container.resize(&app.model, try renderer.resize()); container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{}); container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush(w);
} }
} }
+9 -6
View File
@@ -57,15 +57,18 @@ const MouseDraw = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+10 -7
View File
@@ -24,16 +24,19 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
defer renderer.deinit(); defer renderer.deinit();
var progress_percent: u8 = 0; var progress_percent: u8 = 0;
+9 -6
View File
@@ -24,15 +24,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+16 -17
View File
@@ -53,19 +53,18 @@ const HelloWorldText = packed struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer { var stdout = std.Io.File.stdout();
const deinit_status = gpa.deinit(); var stdout_buffer: [4096]u8 = undefined;
if (deinit_status == .leak) { var writer = stdout.writerStreaming(io, &stdout_buffer);
log.err("memory leak", .{}); const w = &writer.interface;
} defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
}
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -151,22 +150,22 @@ pub fn main() !void {
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true)); var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true));
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element())); try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start(.full); try app.start(.full, w);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop // event loop
while (true) { while (true) {
const event = app.nextEvent(); const event = try app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)}); log.debug("received event: {s}", .{@tagName(event)});
// pre event handling // pre event handling
switch (event) { switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {}, else => {},
} }
container.handle(&app.model, event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| try app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -179,10 +178,10 @@ pub fn main() !void {
else => {}, else => {},
} }
container.resize(&app.model, try renderer.resize()); container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{}); container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush(w);
} }
} }
+9 -6
View File
@@ -24,15 +24,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+9 -6
View File
@@ -89,15 +89,18 @@ const ErrorNotification = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+9 -6
View File
@@ -27,15 +27,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+9 -6
View File
@@ -27,15 +27,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+9 -6
View File
@@ -27,15 +27,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+9 -6
View File
@@ -27,15 +27,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+9 -6
View File
@@ -24,15 +24,18 @@ const QuitText = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+9 -6
View File
@@ -170,15 +170,18 @@ const TextStyles = struct {
} }
}; };
pub fn main() !void { pub fn main(init: std.process.Init) !void {
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init; var app: App = .init(allocator, io, .{});
defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
+66 -74
View File
@@ -28,8 +28,9 @@ pub fn App(comptime M: type, comptime E: type) type {
io: std.Io, io: std.Io,
model: Model, model: Model,
queue: Queue, queue: Queue,
thread: ?Thread = null, // thread: ?Thread = null,
quit_event: Thread.ResetEvent, // quit_event: Thread.ResetEvent,
future: ?std.Io.Future(@typeInfo(@typeInfo(@TypeOf(run)).@"fn".return_type.?).error_union.error_set!void) = null,
termios: ?posix.termios = null, termios: ?posix.termios = null,
handler_registered: bool = false, handler_registered: bool = false,
config: TerminalConfiguration, config: TerminalConfiguration,
@@ -58,24 +59,24 @@ pub fn App(comptime M: type, comptime E: type) type {
// global variable for the registered handler for WINCH // global variable for the registered handler for WINCH
var handler_ctx: *anyopaque = undefined; var handler_ctx: *anyopaque = undefined;
/// registered WINCH handler to report resize events /// registered WINCH handler to report resize events
fn handleWinch(_: i32) callconv(.c) void { fn handleWinch(_: std.os.linux.SIG) callconv(.c) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx)); const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE this does not have to be done if in-band resize events are supported // 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! // -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize); this.postEvent(.resize) catch {};
} }
/// registered CONT handler to force a complete redraw /// registered CONT handler to force a complete redraw
fn handleCont(_: i32) callconv(.c) void { fn handleCont(_: std.os.linux.SIG) callconv(.c) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx)); const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE this does not have to be done if in-band resize events are supported // 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! // -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize); this.postEvent(.resize) catch {};
} }
/// registered INT handler to catch ctrl-c to send a `.cancel` /// registered INT handler to catch ctrl-c to send a `.cancel`
fn handleInt(_: i32) callconv(.c) void { fn handleInt(_: std.os.linux.SIG) callconv(.c) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx)); const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE do not quit the application and instead issue a `.cancel` event // NOTE do not quit the application and instead issue a `.cancel` event
this.postEvent(.cancel); this.postEvent(.cancel) catch {};
} }
pub fn init(gpa: Allocator, io: std.Io, model: Model) @This() { pub fn init(gpa: Allocator, io: std.Io, model: Model) @This() {
@@ -84,17 +85,17 @@ pub fn App(comptime M: type, comptime E: type) type {
.io = io, .io = io,
.model = model, .model = model,
.queue = .{}, .queue = .{},
.quit_event = .{},
.config = undefined, .config = undefined,
}; };
} }
pub fn start(this: *@This(), config: TerminalConfiguration) !void { pub fn start(this: *@This(), config: TerminalConfiguration, w: *std.Io.Writer) !void {
if (this.future) |_| return;
this.config = config; this.config = config;
if (!this.handler_registered) { if (!this.handler_registered) {
// post init event (as the very first element to be in the queue - event loop) // post init event (as the very first element to be in the queue - event loop)
this.postEvent(.init); try this.postEvent(.init);
handler_ctx = this; handler_ctx = this;
if (config.altScreen) { if (config.altScreen) {
@@ -119,52 +120,53 @@ pub fn App(comptime M: type, comptime E: type) type {
this.handler_registered = true; this.handler_registered = true;
} }
this.quit_event.reset(); this.future = this.io.async(run, .{this});
this.thread = try Thread.spawn(.{}, @This().run, .{this});
var termios: posix.termios = undefined; var termios: posix.termios = undefined;
if (this.config.rawMode) try terminal.enableRawMode(&termios); if (this.config.rawMode) try terminal.enableRawMode(&termios);
if (this.termios) |_| {} else this.termios = termios; if (this.termios) |_| {} else this.termios = termios;
if (this.config.altScreen) try terminal.enterAltScreen(); if (this.config.altScreen) try terminal.enterAltScreen(w);
if (this.config.saveScreen) try terminal.saveScreen(); if (this.config.saveScreen) try terminal.saveScreen(w);
if (this.config.hideCursor) try terminal.hideCursor(); if (this.config.hideCursor) try terminal.hideCursor(w);
if (this.config.altScreen and this.config.rawMode) try terminal.enableMouseSupport(); if (this.config.altScreen and this.config.rawMode) try terminal.enableMouseSupport(w);
} }
pub fn stop(this: *@This()) !void { pub fn stop(this: *@This(), w: *std.Io.Writer) !void {
this.quit_event.set(); if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport(w);
if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport(); if (this.config.saveScreen) try terminal.restoreScreen(w);
if (this.config.saveScreen) try terminal.restoreScreen(); if (this.config.altScreen) try terminal.exitAltScreen(w);
if (this.config.altScreen) try terminal.exitAltScreen();
if (this.config.hideCursor) try terminal.showCursor(); if (this.config.hideCursor) try terminal.showCursor(w);
if (this.config.saveScreen) try terminal.resetCursor(); if (this.config.saveScreen) try terminal.resetCursor(w);
if (this.termios) |termios| { if (this.termios) |termios| {
if (this.config.rawMode) try terminal.disableRawMode(&termios); if (this.config.rawMode) try terminal.disableRawMode(&termios);
this.termios = null; this.termios = null;
} }
if (this.thread) |*thread| { if (this.future) |*future| {
thread.join(); future.cancel(this.io) catch {};
this.thread = null; this.future = null;
} }
} }
/// Quit the application loop. /// Quit the application loop.
/// This will cancel 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 { pub fn quit(this: *@This()) !void {
this.quit_event.set(); try this.postEvent(.quit);
this.postEvent(.quit); if (this.future) |*future| {
future.cancel(this.io) catch {};
this.future = null;
}
} }
/// Returns the next available event, blocking until one is available. /// Returns the next available event, blocking until one is available.
pub fn nextEvent(this: *@This()) Event { pub fn nextEvent(this: *@This()) !Event {
return this.queue.pop(); return this.queue.pop(this.io);
} }
/// Post an `Event` into the queue. Blocks if there is no capacity for the `Event`. /// Post an `Event` into the queue. Blocks if there is no capacity for the `Event`.
pub fn postEvent(this: *@This(), e: Event) void { pub fn postEvent(this: *@This(), e: Event) !void {
this.queue.push(e); try this.queue.push(this.io, e);
} }
fn run(this: *@This()) !void { fn run(this: *@This()) !void {
@@ -173,23 +175,9 @@ pub fn App(comptime M: type, comptime E: type) type {
// NOTE set the `NONBLOCK` option for the stdin file, such that reading is not blocking! // NOTE set the `NONBLOCK` option for the stdin file, such that reading is not blocking!
if (this.config.rawMode) { if (this.config.rawMode) {
// TODO is there a better way to do this through the `std.Io` interface? // 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) { var fl_flags = posix.system.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0);
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"); fl_flags |= 1 << @bitOffsetOf(posix.system.O, "NONBLOCK");
_ = posix.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags) catch |err| switch (err) { _ = posix.system.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags);
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 remaining_bytes: usize = 0;
@@ -197,12 +185,13 @@ pub fn App(comptime M: type, comptime E: type) type {
defer lines.deinit(this.gpa); defer lines.deinit(this.gpa);
while (true) { while (true) {
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch { this.io.checkCancel() catch break;
// non-blocking read // non-blocking read
const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) { const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) {
error.WouldBlock => { error.WouldBlock => {
// wait a bit // wait a bit
std.Thread.sleep(20); try this.io.sleep(.fromMilliseconds(20), .awake);
continue; continue;
}, },
else => return err, else => return err,
@@ -211,7 +200,7 @@ pub fn App(comptime M: type, comptime E: type) type {
if (read_bytes == 0) { if (read_bytes == 0) {
// received <EOF> // received <EOF>
this.postEvent(.cancel); try this.postEvent(.cancel);
continue; continue;
} }
@@ -235,7 +224,7 @@ pub fn App(comptime M: type, comptime E: type) type {
'S' => .{ .cp = input.F4 }, 'S' => .{ .cp = input.F4 },
else => continue, else => continue,
}; };
this.postEvent(.{ .key = key }); try this.postEvent(.{ .key = key });
}, },
0x5B => { // csi 0x5B => { // csi
if (read_bytes < 3) continue; if (read_bytes < 3) continue;
@@ -285,9 +274,9 @@ pub fn App(comptime M: type, comptime E: type) type {
break :blk mod; break :blk mod;
}, },
}; };
this.postEvent(.{ .key = key }); try this.postEvent(.{ .key = key });
}, },
'Z' => this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }), 'Z' => try this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
'~' => { '~' => {
// Legacy keys // Legacy keys
// CSI number ~ // CSI number ~
@@ -345,11 +334,11 @@ pub fn App(comptime M: type, comptime E: type) type {
break :blk mod; break :blk mod;
}, },
}; };
this.postEvent(.{ .key = key }); try this.postEvent(.{ .key = key });
}, },
// TODO focus usage? should this even be in the default event system? // TODO focus usage? should this even be in the default event system?
'I' => this.postEvent(.{ .focus = true }), 'I' => try this.postEvent(.{ .focus = true }),
'O' => this.postEvent(.{ .focus = false }), 'O' => try this.postEvent(.{ .focus = false }),
'M', 'm' => { 'M', 'm' => {
assert(sequence.len >= 4); assert(sequence.len >= 4);
if (sequence[2] != '<') break; if (sequence[2] != '<') break;
@@ -374,7 +363,7 @@ pub fn App(comptime M: type, comptime E: type) type {
// const alt = button_mask & mouse_bits.alt > 0; // const alt = button_mask & mouse_bits.alt > 0;
// const ctrl = button_mask & mouse_bits.ctrl > 0; // const ctrl = button_mask & mouse_bits.ctrl > 0;
this.postEvent(.{ try this.postEvent(.{
.mouse = .{ .mouse = .{
.button = button, .button = button,
.x = px -| 1, .x = px -| 1,
@@ -410,7 +399,7 @@ pub fn App(comptime M: type, comptime E: type) type {
_ = width_char; _ = width_char;
_ = height_char; _ = height_char;
this.postEvent(.resize); try this.postEvent(.resize);
// this.postEvent(.{ .size = .{ // this.postEvent(.{ .size = .{
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break, // .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = fmt.parseUnsigned(u16, height_char, 10) catch break, // .y = fmt.parseUnsigned(u16, height_char, 10) catch break,
@@ -445,7 +434,7 @@ pub fn App(comptime M: type, comptime E: type) type {
}, },
else => { else => {
// alt + <char> keypress // alt + <char> keypress
this.postEvent(.{ try this.postEvent(.{
.key = .{ .key = .{
.cp = buf[1], .cp = buf[1],
.mod = .{ .alt = true }, .mod = .{ .alt = true },
@@ -473,14 +462,14 @@ pub fn App(comptime M: type, comptime E: type) type {
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1; while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
remaining_bytes = read_bytes - len; remaining_bytes = read_bytes - len;
var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 }; var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 };
while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } }); while (iter.nextCodepoint()) |cp| try this.postEvent(.{ .key = .{ .cp = cp } });
if (remaining_bytes > 0) { if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]); @memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
} }
continue; // this switch block does not return a `Key` we continue with loop continue; // this switch block does not return a `Key` we continue with loop
}, },
}; };
this.postEvent(.{ .key = key }); try this.postEvent(.{ .key = key });
} else { } else {
var len = read_bytes; var len = read_bytes;
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1; while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
@@ -489,26 +478,27 @@ pub fn App(comptime M: type, comptime E: type) type {
if (remaining_bytes > 0) { if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]); @memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
} else { } else {
if (read_bytes != 512 and buf[len - 1] == '\n') this.postEvent(.{ .line = try lines.toOwnedSlice(this.gpa) }); if (read_bytes != 512 and buf[len - 1] == '\n') try this.postEvent(.{ .line = try lines.toOwnedSlice(this.gpa) });
// NOTE line did not end with `\n` but with EOF, meaning the user canceled // NOTE line did not end with `\n` but with EOF, meaning the user canceled
if (read_bytes != 512 and buf[len - 1] != '\n') { if (read_bytes != 512 and buf[len - 1] != '\n') {
lines.clearRetainingCapacity(); lines.clearRetainingCapacity();
this.postEvent(.cancel); try this.postEvent(.cancel);
} }
} }
} }
} }
continue;
};
break;
} }
} }
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
terminal.disableMouseSupport() catch {}; var stdout = std.Io.File.stdout();
terminal.showCursor() catch {}; var writer = stdout.writerStreaming(std.Io.Threaded.global_single_threaded.io(), &.{});
terminal.resetCursor() catch {}; const w = &writer.interface;
terminal.restoreScreen() catch {};
terminal.disableMouseSupport(w) catch {};
terminal.showCursor(w) catch {};
terminal.resetCursor(w) catch {};
terminal.restoreScreen(w) catch {};
terminal.disableRawMode(&.{ terminal.disableRawMode(&.{
.iflag = .{}, .iflag = .{},
.lflag = .{}, .lflag = .{},
@@ -519,7 +509,9 @@ pub fn App(comptime M: type, comptime E: type) type {
.ispeed = undefined, .ispeed = undefined,
.ospeed = undefined, .ospeed = undefined,
}) catch {}; }) catch {};
terminal.exitAltScreen() catch {}; terminal.exitAltScreen(w) catch {};
w.flush() catch {};
std.debug.defaultPanic(msg, ret_addr); std.debug.defaultPanic(msg, ret_addr);
} }
+3
View File
@@ -13,7 +13,10 @@ pub fn reset(this: *Cell) void {
} }
pub fn value(this: Cell, writer: *std.Io.Writer) !void { pub fn value(this: Cell, writer: *std.Io.Writer) !void {
// TODO the style values should be concatinated, such that less bytes are written to the terminal
try this.style.value(writer, this.cp); try this.style.value(writer, this.cp);
// std.log.debug("writer buffer size: {d}", .{writer.end});
if (writer.end >= writer.buffer.len - 32) writer.flush() catch {};
} }
const std = @import("std"); const std = @import("std");
+10 -9
View File
@@ -947,7 +947,7 @@ pub fn Container(Model: type, Event: type) type {
return .max(element_size, min_size); return .max(element_size, min_size);
} }
pub fn handle(this: *const @This(), model: *Model, event: Event) !void { pub fn handle(this: *const @This(), io: std.Io, model: *Model, event: Event) !void {
switch (event) { switch (event) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) { .mouse => |mouse| if (mouse.in(this.origin, this.size)) {
// the element receives the mouse event with relative position // the element receives the mouse event with relative position
@@ -955,16 +955,17 @@ pub fn Container(Model: type, Event: type) type {
var relative_mouse: input.Mouse = mouse; var relative_mouse: input.Mouse = mouse;
relative_mouse.x -= this.origin.x; relative_mouse.x -= this.origin.x;
relative_mouse.y -= this.origin.y; relative_mouse.y -= this.origin.y;
try this.element.handle(model, .{ .mouse = relative_mouse }); try this.element.handle(io, model, .{ .mouse = relative_mouse });
}, },
.bell => { // NOTE likely no `Container` should catch the `.bell` event and instead it should be handled by the application itself (which also can then skip the propagation of that event, as it does not cause anything to happen to the `Container` tree)
// ring the terminal bell and do not propagate the event any further // .bell => {
_ = try terminal.ringBell(); // // ring the terminal bell and do not propagate the event any further
return; // _ = try terminal.ringBell();
}, // return;
else => try this.element.handle(model, event), // },
else => try this.element.handle(io, model, event),
} }
for (this.elements.items) |*element| try element.handle(model, event); for (this.elements.items) |*element| try element.handle(io, model, event);
} }
pub fn content(this: *const @This(), model: *const Model) ![]Cell { pub fn content(this: *const @This(), model: *const Model) ![]Cell {
+43 -42
View File
@@ -20,7 +20,7 @@ pub fn Element(Model: type, Event: type) type {
minSize: ?*const fn (ctx: *anyopaque, model: *const Model, size: Point) Point = null, minSize: ?*const fn (ctx: *anyopaque, model: *const Model, size: Point) Point = null,
resize: ?*const fn (ctx: *anyopaque, model: *const Model, size: Point) void = null, resize: ?*const fn (ctx: *anyopaque, model: *const Model, size: Point) void = null,
reposition: ?*const fn (ctx: *anyopaque, model: *const Model, origin: Point) void = null, reposition: ?*const fn (ctx: *anyopaque, model: *const Model, origin: Point) void = null,
handle: ?*const fn (ctx: *anyopaque, model: *Model, event: Event) anyerror!void = null, handle: ?*const fn (ctx: *anyopaque, io: std.Io, model: *Model, event: Event) anyerror!void = null,
content: ?*const fn (ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) anyerror!void = null, content: ?*const fn (ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) anyerror!void = null,
}; };
@@ -78,9 +78,9 @@ pub fn Element(Model: type, Event: type) type {
/// error may then be used by the application to display information /// error may then be used by the application to display information
/// about the error to the user and is left intentionally up to the /// about the error to the user and is left intentionally up to the
/// implementation to decide. /// implementation to decide.
pub inline fn handle(this: @This(), model: *Model, event: Event) !void { pub inline fn handle(this: @This(), io: std.Io, model: *Model, event: Event) !void {
if (this.vtable.handle) |handle_fn| if (this.vtable.handle) |handle_fn|
try handle_fn(this.ptr, model, event); try handle_fn(this.ptr, io, model, event);
} }
/// Write content into the `cells` of the `Container`. The associated /// Write content into the `cells` of the `Container`. The associated
@@ -202,9 +202,9 @@ pub fn Alignment(Model: type, Event: type) type {
this.container.reposition(model, origin); this.container.reposition(model, origin);
} }
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void { fn handle(ctx: *anyopaque, io: std.Io, model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
try this.container.handle(model, event); try this.container.handle(io, model, event);
} }
fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void { fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void {
@@ -332,7 +332,7 @@ pub fn Scrollable(Model: type, Event: type) type {
this.container.reposition(model, .{}); this.container.reposition(model, .{});
} }
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void { fn handle(ctx: *anyopaque, io: std.Io, model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
// TODO what about multiple scrollable `Element` usages? mouse // TODO what about multiple scrollable `Element` usages? mouse
@@ -403,7 +403,7 @@ pub fn Scrollable(Model: type, Event: type) type {
this.anchor.y = @min(value, max_anchor_y); this.anchor.y = @min(value, max_anchor_y);
} }
}, },
else => try this.container.handle(model, .{ else => try this.container.handle(io, model, .{
.mouse = .{ .mouse = .{
.x = mouse.x + this.anchor.x, .x = mouse.x + this.anchor.x,
.y = mouse.y + this.anchor.y, .y = mouse.y + this.anchor.y,
@@ -412,7 +412,7 @@ pub fn Scrollable(Model: type, Event: type) type {
}, },
}), }),
}, },
else => try this.container.handle(model, event), else => try this.container.handle(io, model, event),
} }
} }
@@ -559,7 +559,8 @@ pub fn TextField(Model: type, Event: type) type {
}; };
} }
pub fn handle(this: *@This(), model: *Model, event: Event) !void { pub fn handle(this: *@This(), io: std.Io, model: *Model, event: Event) !void {
_ = io;
_ = model; _ = model;
switch (event) { switch (event) {
.key => |key| { .key => |key| {
@@ -654,9 +655,9 @@ pub fn TextField(Model: type, Event: type) type {
} }
} }
fn element_handle(ctx: *anyopaque, model: *Model, event: Event) !void { fn element_handle(ctx: *anyopaque, io: std.Io, model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
try this.handle(model, event); try this.handle(io, model, event);
} }
pub fn toOwnedSlice(this: *@This(), comptime t: enum { bytes, code_points }) !switch (t) { pub fn toOwnedSlice(this: *@This(), comptime t: enum { bytes, code_points }) !switch (t) {
@@ -777,16 +778,16 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
}; };
} }
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void { fn handle(ctx: *anyopaque, io: std.Io, model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.key => |key| { .key => |key| {
// NOTE handle order of keys such that the `input.Enter` might not be processed twice! (leading to unexpected values) // NOTE handle order of keys such that the `input.Enter` might not be processed twice! (leading to unexpected values)
try this.text_field.handle(model, event); try this.text_field.handle(io, model, event);
// TODO enter to accept? // TODO enter to accept?
// - shift+enter is not recognized by the input reader of `zterm`, so currently it is not possible to add newlines into the text box? // - shift+enter is not recognized by the input reader of `zterm`, so currently it is not possible to add newlines into the text box?
if (key.eql(.{ .cp = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) this.queue.push(@unionInit( if (key.eql(.{ .cp = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) try this.queue.push(io, @unionInit(
Event, Event,
@tagName(accept_event), @tagName(accept_event),
switch (event_type) { switch (event_type) {
@@ -856,11 +857,11 @@ pub fn Button(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event))
}; };
} }
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { fn handle(ctx: *anyopaque, io: std.Io, _: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
// TODO should this also support key presses to accept? // TODO should this also support key presses to accept?
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(accept_event), .mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) try this.queue.push(io, accept_event),
else => {}, else => {},
} }
} }
@@ -921,7 +922,7 @@ pub fn RadioButton(Model: type, Event: type) type {
}; };
} }
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { fn handle(ctx: *anyopaque, _: std.Io, _: *Model, event: Event) !void {
var this: *@This() = @ptrCast(@alignCast(ctx)); var this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
// TODO should this also support key presses to accept? // TODO should this also support key presses to accept?
@@ -989,7 +990,7 @@ pub fn Selection(Model: type, Event: type) fn (type) type {
}; };
} }
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { fn handle(ctx: *anyopaque, _: std.Io, _: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.mouse => |mouse| if (mouse.y == 0) { .mouse => |mouse| if (mouse.y == 0) {
@@ -1148,7 +1149,7 @@ pub fn Progress(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)
}; };
} }
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { fn handle(ctx: *anyopaque, _: std.Io, _: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
// TODO should this `Element` trigger a completion event? (I don't think that this is useful?) // TODO should this `Element` trigger a completion event? (I don't think that this is useful?)
switch (event) { switch (event) {
@@ -1265,7 +1266,7 @@ test "scrollable vertical" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
// scroll down 15 times (exactly to the end) (with both mouse and key inputs) // scroll down 15 times (exactly to the end) (with both mouse and key inputs)
for (0..7) |_| try container.handle(&model, .{ for (0..7) |_| try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1273,14 +1274,14 @@ test "scrollable vertical" {
.y = 5, .y = 5,
}, },
}); });
for (7..15) |_| try container.handle(&model, .{ for (7..15) |_| try container.handle(std.testing.io, model, .{
.key = .{ .cp = 'j' }, .key = .{ .cp = 'j' },
}); });
try renderer.render(@TypeOf(container), &container, Model, &.{}); try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// further scrolling down will not change anything // further scrolling down will not change anything
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1292,34 +1293,34 @@ test "scrollable vertical" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// scrolling up and down again // scrolling up and down again
for (0..5) |_| try container.handle(&model, .{ for (0..5) |_| try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'k' }, .key = .{ .cp = 'k' },
}); });
for (0..5) |_| try container.handle(&model, .{ for (0..5) |_| try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'j' }, .key = .{ .cp = 'j' },
}); });
try renderer.render(@TypeOf(container), &container, Model, &.{}); try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// scrolling half page up and down again // scrolling half page up and down again
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'u', .mod = .{ .ctrl = true } }, .key = .{ .cp = 'u', .mod = .{ .ctrl = true } },
}); });
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'd', .mod = .{ .ctrl = true } }, .key = .{ .cp = 'd', .mod = .{ .ctrl = true } },
}); });
try renderer.render(@TypeOf(container), &container, Model, &.{}); try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// scrolling page up // scrolling page up
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'b', .mod = .{ .ctrl = true } }, .key = .{ .cp = 'b', .mod = .{ .ctrl = true } },
}); });
try renderer.render(@TypeOf(container), &container, Model, &.{}); try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
// and down again // and down again
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'f', .mod = .{ .ctrl = true } }, .key = .{ .cp = 'f', .mod = .{ .ctrl = true } },
}); });
try renderer.render(@TypeOf(container), &container, Model, &.{}); try renderer.render(@TypeOf(container), &container, Model, &.{});
@@ -1381,7 +1382,7 @@ test "scrollable vertical with scrollbar" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.top.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.top.zon"), renderer.screen);
// scroll down 15 times (exactly to the end) // scroll down 15 times (exactly to the end)
for (0..15) |_| try container.handle(&model, .{ for (0..15) |_| try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1393,7 +1394,7 @@ test "scrollable vertical with scrollbar" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen);
// further scrolling down will not change anything // further scrolling down will not change anything
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1460,7 +1461,7 @@ test "scrollable horizontal" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen);
// scroll right 15 times (exactly to the end) // scroll right 15 times (exactly to the end)
for (0..15) |_| try container.handle(&model, .{ for (0..15) |_| try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1472,7 +1473,7 @@ test "scrollable horizontal" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
// further scrolling right will not change anything // further scrolling right will not change anything
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1539,7 +1540,7 @@ test "scrollable horizontal with scrollbar" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.left.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.left.zon"), renderer.screen);
// scroll right 15 times (exactly to the end) // scroll right 15 times (exactly to the end)
for (0..15) |_| try container.handle(&model, .{ for (0..15) |_| try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1551,7 +1552,7 @@ test "scrollable horizontal with scrollbar" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen);
// further scrolling right will not change anything // further scrolling right will not change anything
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1736,21 +1737,21 @@ test "input element" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.without.text.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.without.text.zon"), renderer.screen);
// press 'a' 15 times // press 'a' 15 times
for (0..15) |_| try container.handle(&model, .{ for (0..15) |_| try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'a' }, .key = .{ .cp = 'a' },
}); });
try renderer.render(@TypeOf(container), &container, Model, &.{}); try renderer.render(@TypeOf(container), &container, Model, &.{});
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.zon"), renderer.screen);
// press 'a' 15 times // press 'a' 15 times
for (0..15) |_| try container.handle(&model, .{ for (0..15) |_| try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'a' }, .key = .{ .cp = 'a' },
}); });
try renderer.render(@TypeOf(container), &container, Model, &.{}); try renderer.render(@TypeOf(container), &container, Model, &.{});
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.overflow.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.overflow.zon"), renderer.screen);
// test the accepting of the `Element` // test the accepting of the `Element`
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.key = .{ .cp = input.Enter }, .key = .{ .cp = input.Enter },
}); });
const accept_event = queue.pop(); const accept_event = queue.pop();
@@ -1802,7 +1803,7 @@ test "button" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen);
// test the accepting of the `Element` // test the accepting of the `Element`
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.mouse = .{ .mouse = .{
.x = 5, .x = 5,
.y = 3, .y = 3,
@@ -1860,7 +1861,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_zero.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_zero.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.progress = 25, .progress = 25,
}); });
container.resize(&.{}, size); container.resize(&.{}, size);
@@ -1869,7 +1870,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_quarter.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_quarter.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.progress = 50, .progress = 50,
}); });
container.resize(&.{}, size); container.resize(&.{}, size);
@@ -1878,7 +1879,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_half.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_half.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.progress = 75, .progress = 75,
}); });
container.resize(&.{}, size); container.resize(&.{}, size);
@@ -1887,7 +1888,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_three_quarter.zon"), renderer.screen); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_three_quarter.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(&model, .{ try container.handle(std.testing.io, &model, .{
.progress = 100, .progress = 100,
}); });
container.resize(&.{}, size); container.resize(&.{}, size);
+28 -22
View File
@@ -56,21 +56,29 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
const b_fields = @typeInfo(B).@"union".fields; const b_fields = @typeInfo(B).@"union".fields;
const b_fields_tag = @typeInfo(B).@"union".tag_type.?; const b_fields_tag = @typeInfo(B).@"union".tag_type.?;
const b_enum_fields = @typeInfo(b_fields_tag).@"enum".fields; const b_enum_fields = @typeInfo(b_fields_tag).@"enum".fields;
var fields: [a_fields.len + b_fields.len]std.builtin.Type.UnionField = undefined; var field_names: [a_fields.len + b_fields.len][:0]const u8 = undefined;
var enum_fields: [a_fields.len + b_fields.len]std.builtin.Type.EnumField = undefined; var field_types: [a_fields.len + b_fields.len]type = undefined;
var enum_names: [a_fields.len + b_fields.len][:0]const u8 = undefined;
// FIX can I not use `comptime_int`? `u4` would be actually incorrect!
// can I get the correct size through the length of the fields?
var enum_values: [a_fields.len + b_fields.len]u4 = undefined;
var i: usize = 0; var i: usize = 0;
for (a_fields, a_enum_fields) |field, enum_field| { for (a_fields, a_enum_fields) |field, enum_field| {
fields[i] = field; field_names[i] = field.name;
field_types[i] = field.type;
var enum_f = enum_field; var enum_f = enum_field;
enum_f.value = i; enum_f.value = i;
enum_fields[i] = enum_f; enum_names[i] = enum_f.name;
enum_values[i] = i;
i += 1; i += 1;
} }
for (b_fields, b_enum_fields) |field, enum_field| { for (b_fields, b_enum_fields) |field, enum_field| {
fields[i] = field; field_names[i] = field.name;
field_types[i] = field.type;
var enum_f = enum_field; var enum_f = enum_field;
enum_f.value = i; enum_f.value = i;
enum_fields[i] = enum_f; enum_names[i] = enum_f.name;
enum_values[i] = i;
i += 1; i += 1;
} }
@@ -92,24 +100,22 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
j += 1; j += 1;
} }
const EventType = @Type(.{ .int = .{ const EventType = @Int(.unsigned, @bitSizeOf(@TypeOf(i)) - @clz(i));
.signedness = .unsigned,
.bits = @bitSizeOf(@TypeOf(i)) - @clz(i),
} });
const Event = @Type(.{ .@"enum" = .{ const Event = @Enum(
.tag_type = EventType, EventType,
.fields = enum_fields[0..], .exhaustive,
.decls = &.{}, &enum_names,
.is_exhaustive = true, &enum_values,
} }); );
return @Type(.{ .@"union" = .{ return @Union(
.layout = .auto, .auto,
.tag_type = Event, Event,
.fields = fields[0..], &field_names,
.decls = &.{}, &field_types,
} }); &@splat(.{}),
);
} }
/// Determine whether the provided type `T` is a tagged union: `union(enum)`. /// Determine whether the provided type `T` is a tagged union: `union(enum)`.
+45 -45
View File
@@ -1,4 +1,4 @@
// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License) // taken from https://github.com/rockorager/libvaxis/blob/main/s!rc/queue.zig (MIT-Lice!n!se)
// with slight modifications // 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`.
@@ -9,26 +9,26 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
read_index: usize = 0, read_index: usize = 0,
write_index: usize = 0, write_index: usize = 0,
mutex: std.Thread.Mutex = .{}, mutex: std.Io.Mutex = .init,
// blocks when the buffer is full // blocks when the buffer is full
not_full: std.Thread.Condition = .{}, not_full: std.Io.Condition = .init,
// ...or empty // ...or empty
not_empty: std.Thread.Condition = .{}, not_empty: std.Io.Condition = .init,
const QueueType = @This(); const QueueType = @This();
/// Pop an item from the queue. Blocks until an item is available. /// Pop an item from the queue. Blocks until an item is available.
pub fn pop(this: *QueueType) T { pub fn pop(this: *QueueType, io: std.Io) !T {
this.mutex.lock(); try this.mutex.lock(io);
defer this.mutex.unlock(); defer this.mutex.unlock(io);
while (this.isEmptyLH()) { while (this.isEmptyLH()) {
this.not_empty.wait(&this.mutex); try this.not_empty.wait(io, &this.mutex);
} }
assert(!this.isEmptyLH()); assert(!this.isEmptyLH());
if (this.isFullLH()) { if (this.isFullLH()) {
// If we are full, wake up a push that might be // If we are full, wake up a push that might be
// waiting here. // waiting here.
this.not_full.signal(); this.not_full.signal(io);
} }
const result = this.buf[this.mask(this.read_index)]; const result = this.buf[this.mask(this.read_index)];
@@ -38,15 +38,15 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
/// Push an item into the queue. Blocks until an item has been /// Push an item into the queue. Blocks until an item has been
/// put in the queue. /// put in the queue.
pub fn push(this: *QueueType, item: T) void { pub fn push(this: *QueueType, io: std.Io, item: T) !void {
this.mutex.lock(); try this.mutex.lock(io);
defer this.mutex.unlock(); defer this.mutex.unlock(io);
while (this.isFullLH()) { while (this.isFullLH()) {
this.not_full.wait(&this.mutex); try this.not_full.wait(io, &this.mutex);
} }
if (this.isEmptyLH()) { if (this.isEmptyLH()) {
// If we were empty, wake up a pop if it was waiting. // If we were empty, wake up a pop if it was waiting.
this.not_empty.signal(); this.not_empty.signal(io);
} }
assert(!this.isFullLH()); assert(!this.isFullLH());
@@ -57,45 +57,45 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
/// Push an item into the queue. Returns true when the item /// Push an item into the queue. Returns true when the item
/// was successfully placed in the queue, false if the queue /// was successfully placed in the queue, false if the queue
/// was full. /// was full.
pub fn tryPush(this: *QueueType, item: T) bool { pub fn tryPush(this: *QueueType, io: std.Io, item: T) !bool {
this.mutex.lock(); try this.mutex.lock(io);
if (this.isFullLH()) { if (this.isFullLH()) {
this.mutex.unlock(); this.mutex.unlock();
return false; return false;
} }
this.mutex.unlock(); this.mutex.unlock(io);
this.push(item); this.push(item);
return true; return true;
} }
/// Pop an item from the queue. Returns null when no item is /// Pop an item from the queue. Returns null when no item is
/// available. /// available.
pub fn tryPop(this: *QueueType) ?T { pub fn tryPop(this: *QueueType, io: std.Io) !?T {
this.mutex.lock(); try this.mutex.lock(io);
if (this.isEmptyLH()) { if (this.isEmptyLH()) {
this.mutex.unlock(); this.mutex.unlock(io);
return null; return null;
} }
this.mutex.unlock(); this.mutex.unlock(io);
return this.pop(); return this.pop();
} }
/// Poll the queue. This call blocks until events are in the queue /// Poll the queue. This call blocks until events are in the queue
pub fn poll(this: *QueueType) void { pub fn poll(this: *QueueType, io: std.Io) !void {
this.mutex.lock(); try this.mutex.lock(io);
defer this.mutex.unlock(); defer this.mutex.unlock(io);
while (this.isEmptyLH()) { while (this.isEmptyLH()) {
this.not_empty.wait(&this.mutex); try this.not_empty.wait(io, &this.mutex);
} }
assert(!this.isEmptyLH()); assert(!this.isEmptyLH());
} }
pub fn lock(this: *QueueType) void { pub fn lock(this: *QueueType, io: std.Io) !void {
this.mutex.lock(); try this.mutex.lock(io);
} }
pub fn unlock(this: *QueueType) void { pub fn unlock(this: *QueueType, io: std.Io) void {
this.mutex.unlock(); this.mutex.unlock(io);
} }
/// Used to efficiently drain the queue /// Used to efficiently drain the queue
@@ -117,16 +117,16 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
} }
/// Returns `true` if the queue is empty and `false` otherwise. /// Returns `true` if the queue is empty and `false` otherwise.
pub fn isEmpty(this: *QueueType) bool { pub fn isEmpty(this: *QueueType, io: std.Io) !bool {
this.mutex.lock(); try this.mutex.lock(io);
defer this.mutex.unlock(); defer this.mutex.unlock(io);
return this.isEmptyLH(); return this.isEmptyLH();
} }
/// Returns `true` if the queue is full and `false` otherwise. /// Returns `true` if the queue is full and `false` otherwise.
pub fn isFull(this: *QueueType) bool { pub fn isFull(this: *QueueType, io: std.Io) !bool {
this.mutex.lock(); try this.mutex.lock(io);
defer this.mutex.unlock(); defer this.mutex.unlock(io);
return this.isFullLH(); return this.isFullLH();
} }
@@ -199,15 +199,15 @@ test "Try to pop, fill from another thread" {
thread.join(); thread.join();
} }
fn sleepyPop(q: *Queue(u8, 2)) !void { fn sleepyPop(q: *Queue(u8, 2), io: std.Io) !void {
// First we wait for the queue to be full. // First we wait for the queue to be full.
while (!q.isFull()) while (!q.isFull())
try Thread.yield(); try Thread.yield();
// Then we spuriously wake it up, because that's a thing that can // Then we spuriously wake it up, because that's a thing that can
// happen. // happen.
q.not_full.signal(); q.not_full.signal(io);
q.not_empty.signal(); q.not_empty.signal(io);
// Then give the other thread a good chance of waking up. It's not // Then give the other thread a good chance of waking up. It's not
// clear that yield guarantees the other thread will be scheduled, // clear that yield guarantees the other thread will be scheduled,
@@ -228,8 +228,8 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
std.Thread.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Another spurious wake... // Another spurious wake...
q.not_full.signal(); q.not_full.signal(io);
q.not_empty.signal(); q.not_empty.signal(io);
// And another chance for the other thread to see that it's // And another chance for the other thread to see that it's
// spurious and go back to sleep. // spurious and go back to sleep.
try Thread.yield(); try Thread.yield();
@@ -267,14 +267,14 @@ test "Fill, block, fill, block" {
try testing.expectEqual(4, queue.pop()); try testing.expectEqual(4, queue.pop());
} }
fn sleepyPush(q: *Queue(u8, 1)) !void { fn sleepyPush(q: *Queue(u8, 1), io: std.Io) !void {
// Try to ensure the other thread has already started trying to pop. // Try to ensure the other thread has already started trying to pop.
try Thread.yield(); try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Spurious wake // Spurious wake
q.not_full.signal(); q.not_full.signal(io);
q.not_empty.signal(); q.not_empty.signal(io);
try Thread.yield(); try Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
@@ -289,8 +289,8 @@ fn sleepyPush(q: *Queue(u8, 1)) !void {
std.Thread.sleep(std.time.ns_per_s / 2); std.Thread.sleep(std.time.ns_per_s / 2);
// Spurious wake // Spurious wake
q.not_full.signal(); q.not_full.signal(io);
q.not_empty.signal(); q.not_empty.signal(io);
q.push(2); q.push(2);
} }
+18 -21
View File
@@ -25,7 +25,7 @@ pub const Buffered = struct {
} }
} }
pub fn resize(this: *@This()) !Point { pub fn resize(this: *@This(), w: *std.Io.Writer) !Point {
const size = terminal.getTerminalSize(); const size = terminal.getTerminalSize();
if (meta.eql(this.size, size)) return this.size; if (meta.eql(this.size, size)) return this.size;
@@ -47,13 +47,13 @@ pub const Buffered = struct {
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory."); this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
@memset(this.virtual_screen, .{}); @memset(this.virtual_screen, .{});
} }
try this.clear(); try this.clear(w);
return size; return size;
} }
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call. /// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
pub fn clear(this: *@This()) !void { pub fn clear(this: *@This(), w: *std.Io.Writer) !void {
try terminal.clearScreen(); try terminal.clearScreen(w);
@memset(this.screen, .{}); @memset(this.screen, .{});
} }
@@ -84,11 +84,12 @@ pub const Buffered = struct {
} }
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop). /// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
pub fn flush(this: *@This()) !void { pub fn flush(this: *@This(), w: *std.Io.Writer) !void {
try terminal.hideCursor(); defer w.flush() catch {};
try terminal.hideCursor(w);
// TODO measure timings of rendered frames? // TODO measure timings of rendered frames?
var cursor_position: ?Point = null; var cursor_position: ?Point = null;
var writer = terminal.writer();
const s = this.screen; const s = this.screen;
const vs = this.virtual_screen; const vs = this.virtual_screen;
@@ -105,21 +106,21 @@ pub const Buffered = struct {
.x = @truncate(col), .x = @truncate(col),
.y = @truncate(row), .y = @truncate(row),
}; };
try cvs.style.set_cursor_style(&writer); try cvs.style.set_cursor_style(w);
} }
if (cs.eql(cvs)) continue; if (cs.eql(cvs)) continue;
// render differences found in virtual screen // render differences found in virtual screen
try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) }); try terminal.setCursorPosition(w, .{ .y = @truncate(row), .x = @truncate(col) });
try cvs.value(&writer); try cvs.value(w);
// update screen to be the virtual screen for the next frame // update screen to be the virtual screen for the next frame
s[idx] = vs[idx]; s[idx] = vs[idx];
} }
} }
if (cursor_position) |point| { if (cursor_position) |point| {
try terminal.showCursor(); try terminal.showCursor(w);
try terminal.setCursorPosition(point); try terminal.setCursorPosition(w, point);
} }
} }
}; };
@@ -158,14 +159,14 @@ pub const Direct = struct {
try terminal.clearScreen(); try terminal.clearScreen();
} }
pub fn writeCtrlDWithNewline(this: *@This()) !void { pub fn writeCtrlDWithNewline(this: *@This(), writer: *std.Io.Writer) !void {
_ = this; _ = this;
_ = try terminal.write("^D\n"); _ = try terminal.write(writer, "^D\n");
} }
pub fn writeNewline(this: *@This()) !void { pub fn writeNewline(this: *@This(), writer: *std.Io.Writer) !void {
_ = this; _ = this;
_ = try terminal.write("\n"); _ = try terminal.write(writer, "\n");
} }
/// Render provided cells at size (anchor and dimension) into the *screen*. /// Render provided cells at size (anchor and dimension) into the *screen*.
@@ -200,11 +201,7 @@ pub const Direct = struct {
for (container.elements.items) |*element| try this.render(Container, element, Model, model); for (container.elements.items) |*element| try this.render(Container, element, Model, model);
} }
pub fn flush(this: *@This()) !void { pub fn flush(this: *@This(), writer: *std.Io.Writer) !void {
const stdout_file = std.fs.File.stdout();
var buffer: [4096]u8 = undefined;
var stdout_writer = stdout_file.writer(&buffer);
var writer = &stdout_writer.interface;
defer writer.flush() catch {}; defer writer.flush() catch {};
row: for (0..this.size.y) |row| { row: for (0..this.size.y) |row| {
+33 -54
View File
@@ -14,94 +14,73 @@ pub fn getTerminalSize() Size {
return .{ .x = ws.col, .y = ws.row }; return .{ .x = ws.col, .y = ws.row };
} }
pub fn saveScreen() !void { pub fn saveScreen(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.save_screen); _ = try w.write(ctlseqs.save_screen);
} }
pub fn restoreScreen() !void { pub fn restoreScreen(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.restore_screen); _ = try w.write(ctlseqs.restore_screen);
} }
pub fn enterAltScreen() !void { pub fn enterAltScreen(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.smcup); _ = try w.write(ctlseqs.smcup);
} }
pub fn exitAltScreen() !void { pub fn exitAltScreen(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.rmcup); _ = try w.write(ctlseqs.rmcup);
} }
pub fn clearScreen() !void { pub fn clearScreen(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.clear_screen); _ = try w.write(ctlseqs.clear_screen);
} }
pub fn hideCursor() !void { pub fn hideCursor(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.hide_cursor); _ = try w.write(ctlseqs.hide_cursor);
} }
pub fn showCursor() !void { pub fn showCursor(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor); _ = try w.write(ctlseqs.show_cursor);
} }
pub fn resetCursor() !void { pub fn resetCursor(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.reset_cursor_shape); _ = try w.write(ctlseqs.reset_cursor_shape);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.osc12_reset); _ = try w.write(ctlseqs.osc12_reset);
} }
pub fn setCursorPositionHome() !void { pub fn setCursorPositionHome(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home); _ = try w.write(ctlseqs.home);
} }
pub fn enableMouseSupport() !void { pub fn enableMouseSupport(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_set); _ = try w.write(ctlseqs.mouse_set);
} }
pub fn disableMouseSupport() !void { pub fn disableMouseSupport(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_reset); _ = try w.write(ctlseqs.mouse_reset);
} }
pub fn ringBell() !void { pub fn ringBell(w: *std.Io.Writer) !void {
_ = try posix.write(posix.STDIN_FILENO, &.{7}); _ = try w.write(&.{7});
} }
pub fn read(buf: []u8) !usize { pub fn read(buf: []u8) !usize {
return try posix.read(posix.STDIN_FILENO, buf); return try posix.read(posix.STDIN_FILENO, buf);
} }
pub fn write(buf: []const u8) !usize { pub fn write(w: *std.Io.Writer, buf: []const u8) !usize {
return try posix.write(posix.STDIN_FILENO, buf); return try w.write(buf);
} }
fn drainFn(w: *std.Io.Writer, data: []const []const u8, splat: usize) error{WriteFailed}!usize { pub fn setCursorPosition(w: *std.Io.Writer, pos: Point) !void {
_ = w;
if (data.len == 0 or splat == 0) return 0;
var len: usize = 0;
for (data) |bytes| len += posix.write(posix.STDOUT_FILENO, bytes) catch return error.WriteFailed;
return len;
}
// TODO I now need to add that much, for just the one function above?
pub fn writer() std.Io.Writer {
return .{
.vtable = &.{
.drain = drainFn,
.flush = std.Io.Writer.noopFlush,
},
.buffer = &.{},
};
}
pub fn setCursorPosition(pos: Point) !void {
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y + 1, pos.x + 1 }); const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y + 1, pos.x + 1 });
_ = try posix.write(posix.STDIN_FILENO, value); _ = try w.write(value);
} }
pub fn getCursorPosition() !Size { pub fn getCursorPosition(w: *std.Io.Writer) !Size {
// Needs Raw mode (no wait for \n) to work properly cause // Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it. // control sequence will not be written without it.
_ = try posix.write(posix.STDIN_FILENO, "\x1b[6n"); _ = try w.write("\x1b[6n");
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
@@ -215,10 +194,10 @@ pub fn disableRawMode(bak: *const posix.termios) !void {
} }
// Ref // Ref
pub fn canSynchornizeOutput() !bool { pub fn canSynchornizeOutput(w: *std.Io.Writer) !bool {
// Needs Raw mode (no wait for \n) to work properly cause // Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it. // control sequence will not be written without it.
_ = try posix.write(posix.STDIN_FILENO, "\x1b[?2026$p"); _ = try w.write("\x1b[?2026$p");
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;