//! Application type for TUI-applications const std = @import("std"); const terminal = @import("terminal.zig"); const mergeTaggedUnions = @import("event.zig").mergeTaggedUnions; const isTaggedUnion = @import("event.zig").isTaggedUnion; const Key = @import("key.zig"); const SystemEvent = @import("event.zig").SystemEvent; const Queue = @import("queue.zig").Queue; const log = std.log.scoped(.app); /// Create the App Type with the associated user events _E_ which describes /// an tagged union for all the user events that can be send through the /// applications event loop. /// /// _R_ is the type function for the `Renderer` to use. The parameter boolean /// will be set to the _fullscreen_ value at compile time. The corresponding /// `Renderer` type is accessable through the generated type of this function. /// /// _fullscreen_ will be used to configure the `App` and the `Renderer` to /// respect the corresponding configuration whether to render a fullscreen tui /// or an inline tui. /// /// # Example /// /// Create an `App` which renders using the `PlainRenderer` in fullscreen with /// an empty user Event: /// /// ```zig /// const App = @import("app.zig").App( /// union(enum) {}, /// @import("render.zig").PlainRenderer, /// true, /// ); /// // later on use /// var app: App = .{}; /// var renderer: App.Renderer = .{}; /// ``` pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fullscreen: bool) type { if (!isTaggedUnion(E)) { @compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`."); } return struct { pub const Event = mergeTaggedUnions(SystemEvent, E); pub const Layout = @import("layout.zig").Layout(Event); pub const Widget = @import("widget.zig").Widget(Event); pub const Renderer = R(fullscreen); queue: Queue(Event, 256) = .{}, thread: ?std.Thread = null, quit_event: std.Thread.ResetEvent = .{}, termios: ?std.posix.termios = null, attached_handler: bool = false, pub const SignalHandler = struct { context: *anyopaque, callback: *const fn (context: *anyopaque) void, }; pub fn start(this: *@This()) !void { if (this.thread) |_| return; if (!this.attached_handler) { 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"); try registerWinch(.{ .context = this, .callback = @This().winsizeCallback, }); this.attached_handler = true; } this.quit_event.reset(); this.thread = try std.Thread.spawn(.{}, @This().run, .{this}); var termios: std.posix.termios = undefined; try terminal.enableRawMode(&termios); if (this.termios) |_| {} else { this.termios = termios; } if (fullscreen) { try terminal.saveScreen(); try terminal.enterAltScreen(); } } pub fn interrupt(this: *@This()) !void { this.quit_event.set(); if (fullscreen) { try terminal.existAltScreen(); try terminal.restoreScreen(); } if (this.thread) |thread| { thread.join(); this.thread = null; } } pub fn stop(this: *@This()) !void { try this.interrupt(); if (this.termios) |*termios| { try terminal.disableRawMode(termios); if (fullscreen) { try terminal.existAltScreen(); try terminal.restoreScreen(); } } this.termios = null; } /// Quit the application loop. /// This will stop the internal input thread and post a **.quit** `Event`. pub fn quit(this: *@This()) void { this.quit_event.set(); this.postEvent(.quit); } /// Returns the next available event, blocking until one is available. pub fn nextEvent(this: *@This()) Event { return this.queue.pop(); } /// Post an event into the queue. Blocks if there is no capacity for the event. pub fn postEvent(this: *@This(), event: Event) void { 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 { // send initial terminal size // changes are handled by the winch signal handler // see `App.start` and `App.registerWinch` for details this.postEvent(.{ .resize = terminal.getTerminalSize() }); // thread to read user inputs var buf: [256]u8 = undefined; while (true) { // FIX: I still think that there is a race condition (I'm just waiting 'long' enough) this.quit_event.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: Key = switch (b) { 0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } }, 0x08 => .{ .cp = Key.backspace }, 0x09 => .{ .cp = Key.tab }, 0x0a, 0x0d => .{ .cp = 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 = Key.escape }; }, 0x7f => .{ .cp = 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 }); } continue; }; break; } } }; }