From 9ddbb193364c42a7a6d3a01084c8fe434647f5e2 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Wed, 6 Nov 2024 01:38:55 +0100 Subject: [PATCH] add: signal handling WINCH, user input thread with waiting blocking --- src/app.zig | 123 +++++++++++++++++++++++++++-------------- src/event.zig | 22 +++----- src/layout.zig | 12 ++-- src/layout/Pane.zig | 19 ++++--- src/main.zig | 8 +-- src/widget.zig | 4 +- src/widget/RawText.zig | 5 +- 7 files changed, 113 insertions(+), 80 deletions(-) diff --git a/src/app.zig b/src/app.zig index f93e2df..9bf0eb3 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,11 +1,11 @@ //! Application type for TUI-applications const std = @import("std"); const terminal = @import("terminal.zig"); -const code_point = terminal.code_point; + const mergeTaggedUnions = @import("event.zig").mergeTaggedUnions; const isTaggedUnion = @import("event.zig").isTaggedUnion; -const BuiltinEvent = @import("event.zig").BuiltinEvent; +const SystemEvent = @import("event.zig").SystemEvent; const Queue = @import("queue.zig").Queue; const log = std.log.scoped(.app); @@ -18,25 +18,40 @@ pub fn App(comptime E: type) type { @compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`."); } return struct { - pub const Event = mergeTaggedUnions(BuiltinEvent, E); + pub const Event = mergeTaggedUnions(SystemEvent, E); pub const Layout = @import("layout.zig").Layout(Event); pub const Widget = @import("widget.zig").Widget(Event); queue: Queue(Event, 256) = .{}, thread: ?std.Thread = null, - quit: bool = false, + quit: std.Thread.ResetEvent = .{}, termios: ?std.posix.termios = null, - // TODO: event loop function? - // layout handling? + pub const SignalHandler = struct { + context: *anyopaque, + callback: *const fn (context: *anyopaque) void, + }; pub fn init() @This() { + var winch_act = std.posix.Sigaction{ + .handler = .{ .handler = @This().handleWinch }, + .mask = std.posix.empty_sigset, + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null) catch @panic("could not attach signal WINCH"); + return .{}; } pub fn start(this: *@This()) !void { if (this.thread) |_| return; + try registerWinch(.{ + .context = this, + .callback = @This().winsizeCallback, + }); + + this.quit.reset(); this.thread = try std.Thread.spawn(.{}, @This().run, .{this}); var termios: std.posix.termios = undefined; try terminal.enableRawMode(&termios); @@ -49,7 +64,7 @@ pub fn App(comptime E: type) type { try terminal.disableRawMode(termios); try terminal.restoreScreen(); } - this.quit = true; + this.quit.set(); if (this.thread) |thread| { thread.join(); this.thread = null; @@ -66,46 +81,70 @@ pub fn App(comptime E: type) type { this.queue.push(event); } + fn winsizeCallback(ptr: *anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(ptr)); + this.postEvent(.{ .resize = terminal.getTerminalSize() }); + } + + var winch_handler: ?SignalHandler = null; + + fn registerWinch(handler: SignalHandler) !void { + if (winch_handler) |_| { + @panic("Cannot register another WINCH handler."); + } + winch_handler = handler; + } + + fn handleWinch(_: c_int) callconv(.C) void { + if (winch_handler) |handler| { + handler.callback(handler.context); + } + } + fn run(this: *@This()) !void { - // thread to read user inputs + // send initial terminal size + // changes are handled by the winch signal handler (see `init` and `registerWinch`) const size = terminal.getTerminalSize(); this.postEvent(.{ .resize = size }); - // read input in loop + + // thread to read user inputs var buf: [256]u8 = undefined; - while (!this.quit) { - // FIXME: here is a race-condition -> i.e. there could be events - // in the queue, but they will not be executed because the main - // loop will close! - const read_bytes = try terminal.read(buf[0..]); - // escape key presses - if (buf[0] == 0x1b and read_bytes > 1) { - switch (buf[1]) { - // TODO: parse corresponding codes - else => {}, + while (true) { + // FIX: I still think that there is a race condition (I'm just waiting 'long' enough) + this.quit.timedWait(20 * std.time.ns_per_ms) catch { + const read_bytes = try terminal.read(buf[0..]); + // escape key presses + if (buf[0] == 0x1b and read_bytes > 1) { + switch (buf[1]) { + // TODO: parse corresponding codes + else => {}, + } + } else { + const b = buf[0]; + const key: terminal.Key = switch (b) { + 0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } }, + 0x08 => .{ .cp = terminal.Key.backspace }, + 0x09 => .{ .cp = terminal.Key.tab }, + 0x0a, 0x0d => .{ .cp = terminal.Key.enter }, + 0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } }, + 0x1b => escape: { + std.debug.assert(read_bytes == 1); + break :escape .{ .cp = terminal.Key.escape }; + }, + 0x7f => .{ .cp = terminal.Key.backspace }, + else => { + var iter = terminal.code_point.Iterator{ .bytes = buf[0..read_bytes] }; + while (iter.next()) |cp| { + this.postEvent(.{ .key = .{ .cp = cp.code } }); + } + continue; + }, + }; + this.postEvent(.{ .key = key }); } - } else { - const b = buf[0]; - const key: terminal.Key = switch (b) { - 0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } }, - 0x08 => .{ .cp = terminal.Key.backspace }, - 0x09 => .{ .cp = terminal.Key.tab }, - 0x0a, 0x0d => .{ .cp = terminal.Key.enter }, - 0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } }, - 0x1b => escape: { - std.debug.assert(read_bytes == 1); - break :escape .{ .cp = terminal.Key.escape }; - }, - 0x7f => .{ .cp = terminal.Key.backspace }, - else => { - var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] }; - while (iter.next()) |cp| { - this.postEvent(.{ .key = .{ .cp = cp.code } }); - } - continue; - }, - }; - this.postEvent(.{ .key = key }); - } + continue; + }; + break; } this.postEvent(.quit); } diff --git a/src/event.zig b/src/event.zig index 29b2b03..1f43013 100644 --- a/src/event.zig +++ b/src/event.zig @@ -3,27 +3,19 @@ const std = @import("std"); const terminal = @import("terminal.zig"); -// Application events which contain information about default application -// parameter. Either `none` or `err`, where `none` represents no event with no -// message, while `err` represents an error which is propagated. `Widget`s or -// `Layout`s may react to the event but should continue throwing the message up -// to the application event loop. -const ApplicationEvent = union(enum) { - none, - quit, - err: []const u8, +pub const Error = struct { + err: anyerror, + msg: []const u8, }; -// System events which contain information about events triggered from outside -// of the application which impact the application. E.g. the terminal window -// size has changed, etc. -const SystemEvent = union(enum) { +// System events available to every application. +pub const SystemEvent = union(enum) { + quit, + err: Error, resize: terminal.Size, key: terminal.Key, }; -pub const BuiltinEvent = mergeTaggedUnions(SystemEvent, ApplicationEvent); - pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { if (!isTaggedUnion(A) or !isTaggedUnion(B)) { @compileError("Both types for merging tagged unions need to be of type `union(enum)`."); diff --git a/src/layout.zig b/src/layout.zig index 3cdc01f..eeb79f9 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -12,10 +12,10 @@ //! widgets when deallocated. This means that `deinit()` will also deallocate //! every used widget too. const std = @import("std"); -const lib_event = @import("event.zig"); +const isTaggedUnion = @import("event.zig").isTaggedUnion; pub fn Layout(comptime Event: type) type { - if (!lib_event.isTaggedUnion(Event)) { + if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } return struct { @@ -31,12 +31,12 @@ pub fn Layout(comptime Event: type) type { object: Ptr = undefined, vtable: *const VTable = undefined, - // Handle the provided `Event` for this `Widget`. + // Handle the provided `Event` for this `Layout`. pub fn handle(this: *LayoutType, event: Event) !*std.ArrayList(Event) { return try this.vtable.handle(this, event); } - // Return the entire content of this `Widget`. + // Return the entire content of this `Layout`. pub fn content(this: *LayoutType) !*std.ArrayList(u8) { return try this.vtable.content(this); } @@ -51,14 +51,14 @@ pub fn Layout(comptime Event: type) type { .object = @intFromPtr(object), .vtable = &.{ .handle = struct { - // Handle the provided `Event` for this `Widget`. + // Handle the provided `Event` for this `Layout`. fn handle(this: *LayoutType, event: Event) !*std.ArrayList(Event) { const layout: @TypeOf(object) = @ptrFromInt(this.object); return try layout.handle(event); } }.handle, .content = struct { - // Return the entire content of this `Widget`. + // Return the entire content of this `Layout`. fn content(this: *LayoutType) !*std.ArrayList(u8) { const layout: @TypeOf(object) = @ptrFromInt(this.object); return try layout.content(); diff --git a/src/layout/Pane.zig b/src/layout/Pane.zig index c7c0998..d2a2950 100644 --- a/src/layout/Pane.zig +++ b/src/layout/Pane.zig @@ -1,26 +1,27 @@ const std = @import("std"); -const lib_event = @import("../event.zig"); -const widget = @import("../widget.zig"); +const isTaggedUnion = @import("../event.zig").isTaggedUnion; +const Error = @import("../event.zig").Error; pub fn Layout(comptime Event: type) type { - if (!lib_event.isTaggedUnion(Event)) { + if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } + const Widget = @import("../widget.zig").Widget(Event); return struct { - w: widget.Widget(Event) = undefined, + widget: Widget = undefined, events: std.ArrayList(Event) = undefined, c: std.ArrayList(u8) = undefined, - pub fn init(allocator: std.mem.Allocator, w: widget.Widget(Event)) @This() { + pub fn init(allocator: std.mem.Allocator, widget: Widget) @This() { return .{ - .w = w, + .widget = widget, .events = std.ArrayList(Event).init(allocator), .c = std.ArrayList(u8).init(allocator), }; } pub fn deinit(this: *@This()) void { - this.w.deinit(); + this.widget.deinit(); this.events.deinit(); this.c.deinit(); this.* = undefined; @@ -28,14 +29,14 @@ pub fn Layout(comptime Event: type) type { pub fn handle(this: *@This(), event: Event) !*std.ArrayList(Event) { this.events.clearRetainingCapacity(); - if (this.w.handle(event)) |e| { + if (this.widget.handle(event)) |e| { try this.events.append(e); } return &this.events; } pub fn content(this: *@This()) !*std.ArrayList(u8) { - const widget_content = try this.w.content(); + const widget_content = try this.widget.content(); this.c.clearRetainingCapacity(); try this.c.appendSlice(widget_content.items); return &this.c; diff --git a/src/main.zig b/src/main.zig index 74c90dd..5595e28 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,7 +5,7 @@ const zlog = @import("zlog"); const App = @import("app.zig").App(union(enum) {}); pub const std_options = zlog.std_options; -const log = std.log.scoped(.main); +const log = std.log.scoped(.default); pub fn main() !void { errdefer |err| log.err("Application Error: {any}", .{err}); @@ -39,7 +39,6 @@ pub fn main() !void { const event = app.nextEvent(); switch (event) { - .none => continue, .quit => break, .resize => |size| { // NOTE: draw actions should not happen here (still here for testing) @@ -52,8 +51,9 @@ pub fn main() !void { }, .key => |key| { log.debug("received key: {any}", .{key}); - if (terminal.Key.matches(key, .{ .cp = 'q' })) { - app.quit = true; // TODO: who should emit the .quit event? + // ctrl+c to quit + if (terminal.Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { + app.quit.set(); } }, else => {}, diff --git a/src/widget.zig b/src/widget.zig index a9e8a65..7d6f29f 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -11,10 +11,10 @@ //! Each `Widget` may cache its content and should if the contents will not //! change for a long time. const std = @import("std"); -const lib_event = @import("event.zig"); +const isTaggedUnion = @import("event.zig").isTaggedUnion; pub fn Widget(comptime Event: type) type { - if (!lib_event.isTaggedUnion(Event)) { + if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } return struct { diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig index 5aba79c..da7fe89 100644 --- a/src/widget/RawText.zig +++ b/src/widget/RawText.zig @@ -1,8 +1,9 @@ const std = @import("std"); -const lib_event = @import("../event.zig"); +const isTaggedUnion = @import("../event.zig").isTaggedUnion; +const Error = @import("../event.zig").Error; pub fn Widget(comptime Event: type) type { - if (!lib_event.isTaggedUnion(Event)) { + if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } return struct {