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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const gpa = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(gpa, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
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(gpa);
defer renderer.deinit();
var quit_text: QuitText = .{};
@@ -41,7 +44,7 @@ pub fn main() !void {
// TODO what should the demo application do?
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
// - on the left some buttons?
var box = try App.Container.init(allocator, .{
var box = try App.Container.init(gpa, .{
.border = .{
.color = .blue,
.sides = .all,
@@ -55,19 +58,19 @@ pub fn main() !void {
.dim = .{ .y = 90 },
},
}, .{});
try box.append(try App.Container.init(allocator, .{
try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen },
}, .{}));
try box.append(try App.Container.init(allocator, .{
try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen },
}, .{}));
try box.append(try App.Container.init(allocator, .{
try box.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .lightgreen },
}, .{}));
var scrollable: App.Scrollable = .init(box, .disabled);
var container = try App.Container.init(allocator, .{
var container = try App.Container.init(gpa, .{
.layout = .{
.gap = 2,
.separator = .{ .enabled = true },
@@ -75,9 +78,9 @@ pub fn main() !void {
.direction = .horizontal,
},
}, 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 = .{
.direction = .vertical,
.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 = .{
.direction = .vertical,
},
@@ -94,7 +97,7 @@ pub fn main() !void {
.sides = .all,
},
}, .{});
try inner_container.append(try .init(allocator, .{
try inner_container.append(try .init(gpa, .{
.rectangle = .{
.fill = .blue,
},
@@ -103,7 +106,7 @@ pub fn main() !void {
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
try inner_container.append(try .init(gpa, .{
.rectangle = .{
.fill = .red,
},
@@ -112,20 +115,20 @@ pub fn main() !void {
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
try inner_container.append(try .init(gpa, .{
.rectangle = .{
.fill = .green,
},
}, .{}));
try nested_container.append(inner_container);
try nested_container.append(try .init(allocator, .{
try nested_container.append(try .init(gpa, .{
.size = .{
.grow = .horizontal,
.dim = .{ .y = 1 },
},
}, .{}));
try container.append(nested_container);
try container.append(try App.Container.init(allocator, .{
try container.append(try App.Container.init(gpa, .{
.rectangle = .{ .fill = .blue },
.size = .{
.dim = .{ .x = 30 },
@@ -133,25 +136,26 @@ pub fn main() !void {
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
const event = app.nextEvent() catch break; // error indicates cancled Io
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) try app.quit();
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.stop();
defer renderer.clear() catch @panic("could not clear the screen");
defer app.start(.full) catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"vim"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
try app.stop(w);
try w.flush();
defer renderer.clear(w) catch @panic("could not clear the screen");
defer app.start(.full, w) catch @panic("could not start app event loop");
var child = try std.process.spawn(io, .{ .argv = &.{"vim"} });
_ = child.wait(io) catch |err| try app.postEvent(.{
.err = .{
.err = err,
.msg = "Spawning $EDITOR failed",
@@ -165,7 +169,7 @@ pub fn main() !void {
}
// 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,
.msg = "Container Event handling failed",
@@ -178,10 +182,10 @@ pub fn main() !void {
else => {},
}
container.resize(&app.model, try renderer.resize());
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
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});
const gpa = init.gpa;
const io = init.io;
var allocator: std.heap.DebugAllocator(.{}) = .init;
defer if (allocator.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(gpa, io, .{});
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);
defer renderer.deinit();
@@ -127,16 +129,16 @@ pub fn main() !void {
},
}, 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});
try app.start(.direct, w); // needs to become configurable, as what should be enabled / disabled (i.e. show cursor, hide cursor, use alternate screen, etc.)
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
event: while (true) {
// batch events since last iteration
const len = blk: {
app.queue.poll();
app.queue.lock();
defer app.queue.unlock();
try app.queue.poll(io);
try app.queue.lock(io);
defer app.queue.unlock(io);
break :blk app.queue.len();
};
@@ -149,14 +151,14 @@ pub fn main() !void {
switch (event) {
// NOTE draw the character with the ctrl indication and a newline to make sure the rendering stays consistent
.cancel => {
app.quit();
try app.quit();
break :event;
},
.line => |line| {
defer gpa.free(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
.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
container.handle(&app.model, event) catch |err| app.postEvent(.{
container.handle(&app.model, event) catch |err| try app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -175,7 +177,7 @@ pub fn main() !void {
container.resize(&app.model, try renderer.resize());
container.reposition(&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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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));
switch (event) {
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
@@ -49,7 +49,7 @@ const Clickable = struct {
value %= 17;
if (value == 0) value = 1;
this.color = @enumFromInt(value);
this.queue.push(.{ .click = @tagName(mouse.button) });
try this.queue.push(io, .{ .click = @tagName(mouse.button) });
},
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var app: App = .init(allocator, io, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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 = .black } }, button.element()));
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
const event = try app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) 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}),
.accept => log.info("Clicked built-in button using the mouse", .{}),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(&app.model, event) catch |err| app.postEvent(.{
container.handle(io, &app.model, event) catch |err| try app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -132,10 +129,10 @@ pub fn main() !void {
else => {},
}
container.resize(&app.model, try renderer.resize());
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var app: App = .init(allocator, io, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
defer renderer.deinit();
var progress_percent: u8 = 0;
+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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -151,22 +150,22 @@ pub fn main() !void {
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true));
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start(.full);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
try app.start(.full, w);
defer app.stop(w) catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
const event = try app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) 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 }),
else => {},
}
container.handle(&app.model, event) catch |err| app.postEvent(.{
container.handle(&app.model, event) catch |err| try app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
@@ -179,10 +178,10 @@ pub fn main() !void {
else => {},
}
container.resize(&app.model, try renderer.resize());
container.resize(&app.model, try renderer.resize(w));
container.reposition(&app.model, .{});
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush();
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
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});
const allocator = init.gpa;
const io = init.io;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
var app: App = .init(allocator, io, .{});
var stdout = std.Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined;
var writer = stdout.writerStreaming(io, &stdout_buffer);
const w = &writer.interface;
defer w.flush() catch |err| log.err("Could not flush: {any}", .{err});
const allocator = gpa.allocator();
var app: App = .init(allocator, .{}, .{});
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
+341 -349
View File
@@ -28,8 +28,9 @@ pub fn App(comptime M: type, comptime E: type) type {
io: std.Io,
model: Model,
queue: Queue,
thread: ?Thread = null,
quit_event: Thread.ResetEvent,
// thread: ?Thread = null,
// 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,
handler_registered: bool = false,
config: TerminalConfiguration,
@@ -58,24 +59,24 @@ pub fn App(comptime M: type, comptime E: type) type {
// global variable for the registered handler for WINCH
var handler_ctx: *anyopaque = undefined;
/// 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));
// NOTE this does not have to be done if in-band resize events are supported
// -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize);
this.postEvent(.resize) catch {};
}
/// 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));
// NOTE this does not have to be done if in-band resize events are supported
// -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize);
this.postEvent(.resize) catch {};
}
/// 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));
// 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() {
@@ -84,17 +85,17 @@ pub fn App(comptime M: type, comptime E: type) type {
.io = io,
.model = model,
.queue = .{},
.quit_event = .{},
.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;
if (!this.handler_registered) {
// 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;
if (config.altScreen) {
@@ -119,52 +120,53 @@ pub fn App(comptime M: type, comptime E: type) type {
this.handler_registered = true;
}
this.quit_event.reset();
this.thread = try Thread.spawn(.{}, @This().run, .{this});
this.future = this.io.async(run, .{this});
var termios: posix.termios = undefined;
if (this.config.rawMode) try terminal.enableRawMode(&termios);
if (this.termios) |_| {} else this.termios = termios;
if (this.config.altScreen) try terminal.enterAltScreen();
if (this.config.saveScreen) try terminal.saveScreen();
if (this.config.hideCursor) try terminal.hideCursor();
if (this.config.altScreen and this.config.rawMode) try terminal.enableMouseSupport();
if (this.config.altScreen) try terminal.enterAltScreen(w);
if (this.config.saveScreen) try terminal.saveScreen(w);
if (this.config.hideCursor) try terminal.hideCursor(w);
if (this.config.altScreen and this.config.rawMode) try terminal.enableMouseSupport(w);
}
pub fn stop(this: *@This()) !void {
this.quit_event.set();
if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport();
if (this.config.saveScreen) try terminal.restoreScreen();
if (this.config.altScreen) try terminal.exitAltScreen();
pub fn stop(this: *@This(), w: *std.Io.Writer) !void {
if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport(w);
if (this.config.saveScreen) try terminal.restoreScreen(w);
if (this.config.altScreen) try terminal.exitAltScreen(w);
if (this.config.hideCursor) try terminal.showCursor();
if (this.config.saveScreen) try terminal.resetCursor();
if (this.config.hideCursor) try terminal.showCursor(w);
if (this.config.saveScreen) try terminal.resetCursor(w);
if (this.termios) |termios| {
if (this.config.rawMode) try terminal.disableRawMode(&termios);
this.termios = null;
}
if (this.thread) |*thread| {
thread.join();
this.thread = null;
if (this.future) |*future| {
future.cancel(this.io) catch {};
this.future = null;
}
}
/// Quit the application loop.
/// This will cancel the internal input thread and post a **.quit** `Event`.
pub fn quit(this: *@This()) void {
this.quit_event.set();
this.postEvent(.quit);
pub fn quit(this: *@This()) !void {
try 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.
pub fn nextEvent(this: *@This()) Event {
return this.queue.pop();
pub fn nextEvent(this: *@This()) !Event {
return this.queue.pop(this.io);
}
/// Post an `Event` into the queue. Blocks if there is no capacity for the `Event`.
pub fn postEvent(this: *@This(), e: Event) void {
this.queue.push(e);
pub fn postEvent(this: *@This(), e: Event) !void {
try this.queue.push(this.io, e);
}
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!
if (this.config.rawMode) {
// TODO is there a better way to do this through the `std.Io` interface?
var fl_flags = posix.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
var fl_flags = posix.system.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0);
fl_flags |= 1 << @bitOffsetOf(posix.system.O, "NONBLOCK");
_ = posix.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
_ = posix.system.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags);
}
var remaining_bytes: usize = 0;
@@ -197,318 +185,320 @@ pub fn App(comptime M: type, comptime E: type) type {
defer lines.deinit(this.gpa);
while (true) {
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
// non-blocking read
const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) {
error.WouldBlock => {
// wait a bit
std.Thread.sleep(20);
continue;
},
else => return err,
} + remaining_bytes;
remaining_bytes = 0;
this.io.checkCancel() catch break;
if (read_bytes == 0) {
// received <EOF>
this.postEvent(.cancel);
// non-blocking read
const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) {
error.WouldBlock => {
// wait a bit
try this.io.sleep(.fromMilliseconds(20), .awake);
continue;
}
},
else => return err,
} + remaining_bytes;
remaining_bytes = 0;
// escape key presses
if (buf[0] == 0x1b and read_bytes > 1) {
switch (buf[1]) {
0x4F => { // ss3
if (read_bytes < 3) continue;
if (read_bytes == 0) {
// received <EOF>
try this.postEvent(.cancel);
continue;
}
const key: Key = switch (buf[2]) {
'A' => .{ .cp = input.Up },
'B' => .{ .cp = input.Down },
'C' => .{ .cp = input.Right },
'D' => .{ .cp = input.Left },
'E' => .{ .cp = input.KpBegin },
'F' => .{ .cp = input.End },
'H' => .{ .cp = input.Home },
'P' => .{ .cp = input.F1 },
'Q' => .{ .cp = input.F2 },
'R' => .{ .cp = input.F3 },
'S' => .{ .cp = input.F4 },
else => continue,
};
this.postEvent(.{ .key = key });
},
0x5B => { // csi
if (read_bytes < 3) continue;
// escape key presses
if (buf[0] == 0x1b and read_bytes > 1) {
switch (buf[1]) {
0x4F => { // ss3
if (read_bytes < 3) continue;
// We start iterating at index 2 to get past the '['
const sequence: []u8 = blk: for (buf[2..], 2..) |b, i| {
switch (b) {
0x40...0xFF => break :blk buf[0 .. i + 1],
else => continue,
}
} else continue;
const final = sequence[sequence.len - 1];
switch (final) {
'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
// Legacy keys
// CSI {ABCDEFHPQS}
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
_ = field_iter.next(); // skip first field
const key: Key = .{
.cp = switch (final) {
'A' => input.Up,
'B' => input.Down,
'C' => input.Right,
'D' => input.Left,
'E' => input.KpBegin,
'F' => input.End,
'H' => input.Home,
'P' => input.F1,
'Q' => input.F2,
'R' => input.F3,
'S' => input.F4,
else => unreachable,
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
this.postEvent(.{ .key = key });
},
'Z' => this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
'~' => {
// Legacy keys
// CSI number ~
// CSI number ; modifier ~
// CSI number ; modifier:event_type ; text_as_codepoint ~
var field_iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const key: Key = .{
.cp = blk: {
const number_buf = field_iter.next() orelse unreachable; // always will have one field
const number = fmt.parseUnsigned(u16, number_buf, 10) catch break;
break :blk switch (number) {
2 => input.Insert,
3 => input.Delete,
5 => input.PageUp,
6 => input.PageDown,
7 => input.Home,
8 => input.End,
11 => input.F1,
12 => input.F2,
13 => input.F3,
14 => input.F4,
15 => input.F5,
17 => input.F6,
18 => input.F7,
19 => input.F8,
20 => input.F9,
21 => input.F10,
23 => input.F11,
24 => input.F12,
25 => input.F13,
26 => input.F14,
28 => input.F15,
29 => input.F16,
31 => input.F17,
32 => input.F18,
33 => input.F19,
34 => input.F20,
// 200 => return .{ .event = .paste_start, .n = sequence.len },
// 201 => return .{ .event = .paste_end, .n = sequence.len },
57399...57454 => |code| code,
else => unreachable,
};
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
this.postEvent(.{ .key = key });
},
// TODO focus usage? should this even be in the default event system?
'I' => this.postEvent(.{ .focus = true }),
'O' => this.postEvent(.{ .focus = false }),
'M', 'm' => {
assert(sequence.len >= 4);
if (sequence[2] != '<') break;
const delim1 = mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
const button_mask = fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
const delim2 = mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
const px = fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
const py = fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
const mouse_bits = packed struct {
const motion: u8 = 0b00100000;
const buttons: u8 = 0b11000011;
const shift: u8 = 0b00000100;
const alt: u8 = 0b00001000;
const ctrl: u8 = 0b00010000;
};
const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
const motion = button_mask & mouse_bits.motion > 0;
// const shift = button_mask & mouse_bits.shift > 0;
// const alt = button_mask & mouse_bits.alt > 0;
// const ctrl = button_mask & mouse_bits.ctrl > 0;
this.postEvent(.{
.mouse = .{
.button = button,
.x = px -| 1,
.y = py -| 1,
.kind = blk: {
if (motion and button != Mouse.Button.none) break :blk .drag;
if (motion and button == Mouse.Button.none) break :blk .motion;
if (sequence[sequence.len - 1] == 'm') break :blk .release;
break :blk .press;
},
},
});
},
'c' => {
// Primary DA (CSI ? Pm c)
},
'n' => {
// Device Status Report
// CSI Ps n
// CSI ? Ps n
assert(sequence.len >= 3);
},
't' => {
// XTWINOPS
// Split first into fields delimited by ';'
var iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const ps = iter.first();
if (mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const width_char = iter.next() orelse break;
const height_char = iter.next() orelse break;
_ = width_char;
_ = height_char;
this.postEvent(.resize);
// this.postEvent(.{ .size = .{
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = fmt.parseUnsigned(u16, height_char, 10) catch break,
// } });
}
},
'u' => {
// Kitty keyboard
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
// Not all fields will be present. Only unicode-key-code is
// mandatory
},
'y' => {
// DECRPM (CSI ? Ps ; Pm $ y)
},
else => {},
}
},
0x50 => {
// DCS
},
0x58 => {
// SOS
},
0x5D => {
// OSC
},
// TODO parse corresponding codes
0x5F => {
// APC
// parse for kitty graphics capabilities
},
else => {
// alt + <char> keypress
this.postEvent(.{
.key = .{
.cp = buf[1],
.mod = .{ .alt = true },
},
});
},
}
} else {
if (this.config.rawMode) {
const b = buf[0];
const key: Key = switch (b) {
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
0x08 => .{ .cp = input.Backspace },
0x09 => .{ .cp = input.Tab },
0x0a => .{ .cp = 'j', .mod = .{ .ctrl = true } },
0x0d => .{ .cp = input.Enter },
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
0x1b => escape: {
assert(read_bytes == 1);
break :escape .{ .cp = input.Escape };
},
0x7f => .{ .cp = input.Backspace },
else => {
var len = read_bytes;
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
remaining_bytes = read_bytes - len;
var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 };
while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } });
if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
}
continue; // this switch block does not return a `Key` we continue with loop
},
const key: Key = switch (buf[2]) {
'A' => .{ .cp = input.Up },
'B' => .{ .cp = input.Down },
'C' => .{ .cp = input.Right },
'D' => .{ .cp = input.Left },
'E' => .{ .cp = input.KpBegin },
'F' => .{ .cp = input.End },
'H' => .{ .cp = input.Home },
'P' => .{ .cp = input.F1 },
'Q' => .{ .cp = input.F2 },
'R' => .{ .cp = input.F3 },
'S' => .{ .cp = input.F4 },
else => continue,
};
this.postEvent(.{ .key = key });
} else {
var len = read_bytes;
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
remaining_bytes = read_bytes - len;
try lines.appendSlice(this.gpa, buf[0..len]);
if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
} else {
if (read_bytes != 512 and buf[len - 1] == '\n') this.postEvent(.{ .line = try lines.toOwnedSlice(this.gpa) });
// NOTE line did not end with `\n` but with EOF, meaning the user canceled
if (read_bytes != 512 and buf[len - 1] != '\n') {
lines.clearRetainingCapacity();
this.postEvent(.cancel);
try this.postEvent(.{ .key = key });
},
0x5B => { // csi
if (read_bytes < 3) continue;
// We start iterating at index 2 to get past the '['
const sequence: []u8 = blk: for (buf[2..], 2..) |b, i| {
switch (b) {
0x40...0xFF => break :blk buf[0 .. i + 1],
else => continue,
}
} else continue;
const final = sequence[sequence.len - 1];
switch (final) {
'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
// Legacy keys
// CSI {ABCDEFHPQS}
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
_ = field_iter.next(); // skip first field
const key: Key = .{
.cp = switch (final) {
'A' => input.Up,
'B' => input.Down,
'C' => input.Right,
'D' => input.Left,
'E' => input.KpBegin,
'F' => input.End,
'H' => input.Home,
'P' => input.F1,
'Q' => input.F2,
'R' => input.F3,
'S' => input.F4,
else => unreachable,
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
try this.postEvent(.{ .key = key });
},
'Z' => try this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
'~' => {
// Legacy keys
// CSI number ~
// CSI number ; modifier ~
// CSI number ; modifier:event_type ; text_as_codepoint ~
var field_iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const key: Key = .{
.cp = blk: {
const number_buf = field_iter.next() orelse unreachable; // always will have one field
const number = fmt.parseUnsigned(u16, number_buf, 10) catch break;
break :blk switch (number) {
2 => input.Insert,
3 => input.Delete,
5 => input.PageUp,
6 => input.PageDown,
7 => input.Home,
8 => input.End,
11 => input.F1,
12 => input.F2,
13 => input.F3,
14 => input.F4,
15 => input.F5,
17 => input.F6,
18 => input.F7,
19 => input.F8,
20 => input.F9,
21 => input.F10,
23 => input.F11,
24 => input.F12,
25 => input.F13,
26 => input.F14,
28 => input.F15,
29 => input.F16,
31 => input.F17,
32 => input.F18,
33 => input.F19,
34 => input.F20,
// 200 => return .{ .event = .paste_start, .n = sequence.len },
// 201 => return .{ .event = .paste_end, .n = sequence.len },
57399...57454 => |code| code,
else => unreachable,
};
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
try this.postEvent(.{ .key = key });
},
// TODO focus usage? should this even be in the default event system?
'I' => try this.postEvent(.{ .focus = true }),
'O' => try this.postEvent(.{ .focus = false }),
'M', 'm' => {
assert(sequence.len >= 4);
if (sequence[2] != '<') break;
const delim1 = mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
const button_mask = fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
const delim2 = mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
const px = fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
const py = fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
const mouse_bits = packed struct {
const motion: u8 = 0b00100000;
const buttons: u8 = 0b11000011;
const shift: u8 = 0b00000100;
const alt: u8 = 0b00001000;
const ctrl: u8 = 0b00010000;
};
const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
const motion = button_mask & mouse_bits.motion > 0;
// const shift = button_mask & mouse_bits.shift > 0;
// const alt = button_mask & mouse_bits.alt > 0;
// const ctrl = button_mask & mouse_bits.ctrl > 0;
try this.postEvent(.{
.mouse = .{
.button = button,
.x = px -| 1,
.y = py -| 1,
.kind = blk: {
if (motion and button != Mouse.Button.none) break :blk .drag;
if (motion and button == Mouse.Button.none) break :blk .motion;
if (sequence[sequence.len - 1] == 'm') break :blk .release;
break :blk .press;
},
},
});
},
'c' => {
// Primary DA (CSI ? Pm c)
},
'n' => {
// Device Status Report
// CSI Ps n
// CSI ? Ps n
assert(sequence.len >= 3);
},
't' => {
// XTWINOPS
// Split first into fields delimited by ';'
var iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const ps = iter.first();
if (mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const width_char = iter.next() orelse break;
const height_char = iter.next() orelse break;
_ = width_char;
_ = height_char;
try this.postEvent(.resize);
// this.postEvent(.{ .size = .{
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = fmt.parseUnsigned(u16, height_char, 10) catch break,
// } });
}
},
'u' => {
// Kitty keyboard
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
// Not all fields will be present. Only unicode-key-code is
// mandatory
},
'y' => {
// DECRPM (CSI ? Ps ; Pm $ y)
},
else => {},
}
},
0x50 => {
// DCS
},
0x58 => {
// SOS
},
0x5D => {
// OSC
},
// TODO parse corresponding codes
0x5F => {
// APC
// parse for kitty graphics capabilities
},
else => {
// alt + <char> keypress
try this.postEvent(.{
.key = .{
.cp = buf[1],
.mod = .{ .alt = true },
},
});
},
}
} else {
if (this.config.rawMode) {
const b = buf[0];
const key: Key = switch (b) {
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
0x08 => .{ .cp = input.Backspace },
0x09 => .{ .cp = input.Tab },
0x0a => .{ .cp = 'j', .mod = .{ .ctrl = true } },
0x0d => .{ .cp = input.Enter },
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
0x1b => escape: {
assert(read_bytes == 1);
break :escape .{ .cp = input.Escape };
},
0x7f => .{ .cp = input.Backspace },
else => {
var len = read_bytes;
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
remaining_bytes = read_bytes - len;
var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 };
while (iter.nextCodepoint()) |cp| try this.postEvent(.{ .key = .{ .cp = cp } });
if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
}
continue; // this switch block does not return a `Key` we continue with loop
},
};
try this.postEvent(.{ .key = key });
} else {
var len = read_bytes;
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
remaining_bytes = read_bytes - len;
try lines.appendSlice(this.gpa, buf[0..len]);
if (remaining_bytes > 0) {
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
} else {
if (read_bytes != 512 and buf[len - 1] == '\n') try this.postEvent(.{ .line = try lines.toOwnedSlice(this.gpa) });
// NOTE line did not end with `\n` but with EOF, meaning the user canceled
if (read_bytes != 512 and buf[len - 1] != '\n') {
lines.clearRetainingCapacity();
try this.postEvent(.cancel);
}
}
}
continue;
};
break;
}
}
}
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
terminal.disableMouseSupport() catch {};
terminal.showCursor() catch {};
terminal.resetCursor() catch {};
terminal.restoreScreen() catch {};
var stdout = std.Io.File.stdout();
var writer = stdout.writerStreaming(std.Io.Threaded.global_single_threaded.io(), &.{});
const w = &writer.interface;
terminal.disableMouseSupport(w) catch {};
terminal.showCursor(w) catch {};
terminal.resetCursor(w) catch {};
terminal.restoreScreen(w) catch {};
terminal.disableRawMode(&.{
.iflag = .{},
.lflag = .{},
@@ -519,7 +509,9 @@ pub fn App(comptime M: type, comptime E: type) type {
.ispeed = undefined,
.ospeed = undefined,
}) catch {};
terminal.exitAltScreen() catch {};
terminal.exitAltScreen(w) catch {};
w.flush() catch {};
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 {
// TODO the style values should be concatinated, such that less bytes are written to the terminal
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");
+10 -9
View File
@@ -947,7 +947,7 @@ pub fn Container(Model: type, Event: type) type {
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) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
// 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;
relative_mouse.x -= this.origin.x;
relative_mouse.y -= this.origin.y;
try this.element.handle(model, .{ .mouse = relative_mouse });
try this.element.handle(io, model, .{ .mouse = relative_mouse });
},
.bell => {
// ring the terminal bell and do not propagate the event any further
_ = try terminal.ringBell();
return;
},
else => try this.element.handle(model, event),
// NOTE likely no `Container` should catch the `.bell` event and instead it should be handled by the application itself (which also can then skip the propagation of that event, as it does not cause anything to happen to the `Container` tree)
// .bell => {
// // ring the terminal bell and do not propagate the event any further
// _ = try terminal.ringBell();
// return;
// },
else => try this.element.handle(io, model, event),
}
for (this.elements.items) |*element| try element.handle(model, event);
for (this.elements.items) |*element| try element.handle(io, model, event);
}
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,
resize: ?*const fn (ctx: *anyopaque, model: *const Model, size: 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,
};
@@ -78,9 +78,9 @@ pub fn Element(Model: type, Event: type) type {
/// error may then be used by the application to display information
/// about the error to the user and is left intentionally up to the
/// 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|
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
@@ -202,9 +202,9 @@ pub fn Alignment(Model: type, Event: type) type {
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));
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 {
@@ -332,7 +332,7 @@ pub fn Scrollable(Model: type, Event: type) type {
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));
switch (event) {
// 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);
}
},
else => try this.container.handle(model, .{
else => try this.container.handle(io, model, .{
.mouse = .{
.x = mouse.x + this.anchor.x,
.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;
switch (event) {
.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));
try this.handle(model, event);
try this.handle(io, model, event);
}
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));
switch (event) {
.key => |key| {
// 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?
// - 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,
@tagName(accept_event),
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));
switch (event) {
// 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 => {},
}
}
@@ -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));
switch (event) {
// 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));
switch (event) {
.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));
// TODO should this `Element` trigger a completion event? (I don't think that this is useful?)
switch (event) {
@@ -1265,7 +1266,7 @@ test "scrollable vertical" {
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)
for (0..7) |_| try container.handle(&model, .{
for (0..7) |_| try container.handle(std.testing.io, &model, .{
.mouse = .{
.button = .wheel_down,
.kind = .press,
@@ -1273,14 +1274,14 @@ test "scrollable vertical" {
.y = 5,
},
});
for (7..15) |_| try container.handle(&model, .{
for (7..15) |_| try container.handle(std.testing.io, model, .{
.key = .{ .cp = 'j' },
});
try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// further scrolling down will not change anything
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.mouse = .{
.button = .wheel_down,
.kind = .press,
@@ -1292,34 +1293,34 @@ test "scrollable vertical" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// 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' },
});
for (0..5) |_| try container.handle(&model, .{
for (0..5) |_| try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'j' },
});
try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// scrolling half page up and down again
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'u', .mod = .{ .ctrl = true } },
});
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'd', .mod = .{ .ctrl = true } },
});
try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// scrolling page up
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'b', .mod = .{ .ctrl = true } },
});
try renderer.render(@TypeOf(container), &container, Model, &.{});
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
// and down again
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'f', .mod = .{ .ctrl = true } },
});
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);
// 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 = .{
.button = .wheel_down,
.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);
// further scrolling down will not change anything
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.mouse = .{
.button = .wheel_down,
.kind = .press,
@@ -1460,7 +1461,7 @@ test "scrollable horizontal" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen);
// 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 = .{
.button = .wheel_right,
.kind = .press,
@@ -1472,7 +1473,7 @@ test "scrollable horizontal" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
// further scrolling right will not change anything
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.mouse = .{
.button = .wheel_right,
.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);
// 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 = .{
.button = .wheel_right,
.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);
// further scrolling right will not change anything
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.mouse = .{
.button = .wheel_right,
.kind = .press,
@@ -1736,21 +1737,21 @@ test "input element" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.without.text.zon"), renderer.screen);
// press 'a' 15 times
for (0..15) |_| try container.handle(&model, .{
for (0..15) |_| try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'a' },
});
try renderer.render(@TypeOf(container), &container, Model, &.{});
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.zon"), renderer.screen);
// press 'a' 15 times
for (0..15) |_| try container.handle(&model, .{
for (0..15) |_| try container.handle(std.testing.io, &model, .{
.key = .{ .cp = 'a' },
});
try renderer.render(@TypeOf(container), &container, Model, &.{});
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.overflow.zon"), renderer.screen);
// test the accepting of the `Element`
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.key = .{ .cp = input.Enter },
});
const accept_event = queue.pop();
@@ -1802,7 +1803,7 @@ test "button" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen);
// test the accepting of the `Element`
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.mouse = .{
.x = 5,
.y = 3,
@@ -1860,7 +1861,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_zero.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.progress = 25,
});
container.resize(&.{}, size);
@@ -1869,7 +1870,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_quarter.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.progress = 50,
});
container.resize(&.{}, size);
@@ -1878,7 +1879,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_half.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.progress = 75,
});
container.resize(&.{}, size);
@@ -1887,7 +1888,7 @@ test "progress" {
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_three_quarter.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(&model, .{
try container.handle(std.testing.io, &model, .{
.progress = 100,
});
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_tag = @typeInfo(B).@"union".tag_type.?;
const b_enum_fields = @typeInfo(b_fields_tag).@"enum".fields;
var fields: [a_fields.len + b_fields.len]std.builtin.Type.UnionField = undefined;
var enum_fields: [a_fields.len + b_fields.len]std.builtin.Type.EnumField = undefined;
var field_names: [a_fields.len + b_fields.len][:0]const u8 = undefined;
var field_types: [a_fields.len + b_fields.len]type = undefined;
var enum_names: [a_fields.len + b_fields.len][:0]const u8 = undefined;
// FIX can I not use `comptime_int`? `u4` would be actually incorrect!
// can I get the correct size through the length of the fields?
var enum_values: [a_fields.len + b_fields.len]u4 = undefined;
var i: usize = 0;
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;
enum_f.value = i;
enum_fields[i] = enum_f;
enum_names[i] = enum_f.name;
enum_values[i] = i;
i += 1;
}
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;
enum_f.value = i;
enum_fields[i] = enum_f;
enum_names[i] = enum_f.name;
enum_values[i] = i;
i += 1;
}
@@ -92,24 +100,22 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
j += 1;
}
const EventType = @Type(.{ .int = .{
.signedness = .unsigned,
.bits = @bitSizeOf(@TypeOf(i)) - @clz(i),
} });
const EventType = @Int(.unsigned, @bitSizeOf(@TypeOf(i)) - @clz(i));
const Event = @Type(.{ .@"enum" = .{
.tag_type = EventType,
.fields = enum_fields[0..],
.decls = &.{},
.is_exhaustive = true,
} });
const Event = @Enum(
EventType,
.exhaustive,
&enum_names,
&enum_values,
);
return @Type(.{ .@"union" = .{
.layout = .auto,
.tag_type = Event,
.fields = fields[0..],
.decls = &.{},
} });
return @Union(
.auto,
Event,
&field_names,
&field_types,
&@splat(.{}),
);
}
/// 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
/// 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,
write_index: usize = 0,
mutex: std.Thread.Mutex = .{},
mutex: std.Io.Mutex = .init,
// blocks when the buffer is full
not_full: std.Thread.Condition = .{},
not_full: std.Io.Condition = .init,
// ...or empty
not_empty: std.Thread.Condition = .{},
not_empty: std.Io.Condition = .init,
const QueueType = @This();
/// Pop an item from the queue. Blocks until an item is available.
pub fn pop(this: *QueueType) T {
this.mutex.lock();
defer this.mutex.unlock();
pub fn pop(this: *QueueType, io: std.Io) !T {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
while (this.isEmptyLH()) {
this.not_empty.wait(&this.mutex);
try this.not_empty.wait(io, &this.mutex);
}
assert(!this.isEmptyLH());
if (this.isFullLH()) {
// If we are full, wake up a push that might be
// waiting here.
this.not_full.signal();
this.not_full.signal(io);
}
const result = this.buf[this.mask(this.read_index)];
@@ -38,15 +38,15 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
/// Push an item into the queue. Blocks until an item has been
/// put in the queue.
pub fn push(this: *QueueType, item: T) void {
this.mutex.lock();
defer this.mutex.unlock();
pub fn push(this: *QueueType, io: std.Io, item: T) !void {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
while (this.isFullLH()) {
this.not_full.wait(&this.mutex);
try this.not_full.wait(io, &this.mutex);
}
if (this.isEmptyLH()) {
// If we were empty, wake up a pop if it was waiting.
this.not_empty.signal();
this.not_empty.signal(io);
}
assert(!this.isFullLH());
@@ -57,45 +57,45 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
/// Push an item into the queue. Returns true when the item
/// was successfully placed in the queue, false if the queue
/// was full.
pub fn tryPush(this: *QueueType, item: T) bool {
this.mutex.lock();
pub fn tryPush(this: *QueueType, io: std.Io, item: T) !bool {
try this.mutex.lock(io);
if (this.isFullLH()) {
this.mutex.unlock();
return false;
}
this.mutex.unlock();
this.mutex.unlock(io);
this.push(item);
return true;
}
/// Pop an item from the queue. Returns null when no item is
/// available.
pub fn tryPop(this: *QueueType) ?T {
this.mutex.lock();
pub fn tryPop(this: *QueueType, io: std.Io) !?T {
try this.mutex.lock(io);
if (this.isEmptyLH()) {
this.mutex.unlock();
this.mutex.unlock(io);
return null;
}
this.mutex.unlock();
this.mutex.unlock(io);
return this.pop();
}
/// Poll the queue. This call blocks until events are in the queue
pub fn poll(this: *QueueType) void {
this.mutex.lock();
defer this.mutex.unlock();
pub fn poll(this: *QueueType, io: std.Io) !void {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
while (this.isEmptyLH()) {
this.not_empty.wait(&this.mutex);
try this.not_empty.wait(io, &this.mutex);
}
assert(!this.isEmptyLH());
}
pub fn lock(this: *QueueType) void {
this.mutex.lock();
pub fn lock(this: *QueueType, io: std.Io) !void {
try this.mutex.lock(io);
}
pub fn unlock(this: *QueueType) void {
this.mutex.unlock();
pub fn unlock(this: *QueueType, io: std.Io) void {
this.mutex.unlock(io);
}
/// Used to efficiently drain the queue
@@ -117,16 +117,16 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
}
/// Returns `true` if the queue is empty and `false` otherwise.
pub fn isEmpty(this: *QueueType) bool {
this.mutex.lock();
defer this.mutex.unlock();
pub fn isEmpty(this: *QueueType, io: std.Io) !bool {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
return this.isEmptyLH();
}
/// Returns `true` if the queue is full and `false` otherwise.
pub fn isFull(this: *QueueType) bool {
this.mutex.lock();
defer this.mutex.unlock();
pub fn isFull(this: *QueueType, io: std.Io) !bool {
try this.mutex.lock(io);
defer this.mutex.unlock(io);
return this.isFullLH();
}
@@ -199,15 +199,15 @@ test "Try to pop, fill from another thread" {
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.
while (!q.isFull())
try Thread.yield();
// Then we spuriously wake it up, because that's a thing that can
// happen.
q.not_full.signal();
q.not_empty.signal();
q.not_full.signal(io);
q.not_empty.signal(io);
// Then give the other thread a good chance of waking up. It's not
// 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);
// Another spurious wake...
q.not_full.signal();
q.not_empty.signal();
q.not_full.signal(io);
q.not_empty.signal(io);
// And another chance for the other thread to see that it's
// spurious and go back to sleep.
try Thread.yield();
@@ -267,14 +267,14 @@ test "Fill, block, fill, block" {
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 Thread.yield();
std.Thread.sleep(std.time.ns_per_s / 2);
// Spurious wake
q.not_full.signal();
q.not_empty.signal();
q.not_full.signal(io);
q.not_empty.signal(io);
try Thread.yield();
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);
// Spurious wake
q.not_full.signal();
q.not_empty.signal();
q.not_full.signal(io);
q.not_empty.signal(io);
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();
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.");
@memset(this.virtual_screen, .{});
}
try this.clear();
try this.clear(w);
return size;
}
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
pub fn clear(this: *@This()) !void {
try terminal.clearScreen();
pub fn clear(this: *@This(), w: *std.Io.Writer) !void {
try terminal.clearScreen(w);
@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).
pub fn flush(this: *@This()) !void {
try terminal.hideCursor();
pub fn flush(this: *@This(), w: *std.Io.Writer) !void {
defer w.flush() catch {};
try terminal.hideCursor(w);
// TODO measure timings of rendered frames?
var cursor_position: ?Point = null;
var writer = terminal.writer();
const s = this.screen;
const vs = this.virtual_screen;
@@ -105,21 +106,21 @@ pub const Buffered = struct {
.x = @truncate(col),
.y = @truncate(row),
};
try cvs.style.set_cursor_style(&writer);
try cvs.style.set_cursor_style(w);
}
if (cs.eql(cvs)) continue;
// render differences found in virtual screen
try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) });
try cvs.value(&writer);
try terminal.setCursorPosition(w, .{ .y = @truncate(row), .x = @truncate(col) });
try cvs.value(w);
// update screen to be the virtual screen for the next frame
s[idx] = vs[idx];
}
}
if (cursor_position) |point| {
try terminal.showCursor();
try terminal.setCursorPosition(point);
try terminal.showCursor(w);
try terminal.setCursorPosition(w, point);
}
}
};
@@ -158,14 +159,14 @@ pub const Direct = struct {
try terminal.clearScreen();
}
pub fn writeCtrlDWithNewline(this: *@This()) !void {
pub fn writeCtrlDWithNewline(this: *@This(), writer: *std.Io.Writer) !void {
_ = 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;
_ = try terminal.write("\n");
_ = try terminal.write(writer, "\n");
}
/// 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);
}
pub fn flush(this: *@This()) !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;
pub fn flush(this: *@This(), writer: *std.Io.Writer) !void {
defer writer.flush() catch {};
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 };
}
pub fn saveScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.save_screen);
pub fn saveScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.save_screen);
}
pub fn restoreScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.restore_screen);
pub fn restoreScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.restore_screen);
}
pub fn enterAltScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.smcup);
pub fn enterAltScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.smcup);
}
pub fn exitAltScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.rmcup);
pub fn exitAltScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.rmcup);
}
pub fn clearScreen() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.clear_screen);
pub fn clearScreen(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.clear_screen);
}
pub fn hideCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.hide_cursor);
pub fn hideCursor(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.hide_cursor);
}
pub fn showCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor);
pub fn showCursor(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.show_cursor);
}
pub fn resetCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.reset_cursor_shape);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.osc12_reset);
pub fn resetCursor(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.reset_cursor_shape);
_ = try w.write(ctlseqs.osc12_reset);
}
pub fn setCursorPositionHome() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home);
pub fn setCursorPositionHome(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.home);
}
pub fn enableMouseSupport() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_set);
pub fn enableMouseSupport(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.mouse_set);
}
pub fn disableMouseSupport() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_reset);
pub fn disableMouseSupport(w: *std.Io.Writer) !void {
_ = try w.write(ctlseqs.mouse_reset);
}
pub fn ringBell() !void {
_ = try posix.write(posix.STDIN_FILENO, &.{7});
pub fn ringBell(w: *std.Io.Writer) !void {
_ = try w.write(&.{7});
}
pub fn read(buf: []u8) !usize {
return try posix.read(posix.STDIN_FILENO, buf);
}
pub fn write(buf: []const u8) !usize {
return try posix.write(posix.STDIN_FILENO, buf);
pub fn write(w: *std.Io.Writer, buf: []const u8) !usize {
return try w.write(buf);
}
fn drainFn(w: *std.Io.Writer, data: []const []const u8, splat: usize) error{WriteFailed}!usize {
_ = 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 {
pub fn setCursorPosition(w: *std.Io.Writer, pos: Point) !void {
var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y + 1, pos.x + 1 });
_ = try 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
// 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;
@@ -215,10 +194,10 @@ pub fn disableRawMode(bak: *const posix.termios) !void {
}
// Ref
pub fn canSynchornizeOutput() !bool {
pub fn canSynchornizeOutput(w: *std.Io.Writer) !bool {
// Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it.
_ = try posix.write(posix.STDIN_FILENO, "\x1b[?2026$p");
_ = try w.write("\x1b[?2026$p");
var buf: [64]u8 = undefined;