//! Application type for TUI-applications const std = @import("std"); const code_point = @import("code_point"); const event = @import("event.zig"); const input = @import("input.zig"); const terminal = @import("terminal.zig"); const queue = @import("queue.zig"); const mergeTaggedUnions = event.mergeTaggedUnions; const isTaggedUnion = event.isTaggedUnion; const Mouse = input.Mouse; const Key = input.Key; const Size = @import("size.zig").Size; 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. /// /// # Example /// /// Create an `App` which renders using the `PlainRenderer` in fullscreen with /// an empty user Event: /// /// ```zig /// const zterm = @import("zterm"); /// const App = zterm.App( /// union(enum) {}, /// ); /// // later on create an `App` instance and start the event loop /// var app: App = .init; /// try app.start(); /// defer app.stop() catch unreachable; /// ``` pub fn App(comptime E: type) 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(event.SystemEvent, E); pub const Container = @import("container.zig").Container(Event); const element = @import("element.zig"); pub const Element = element.Element(Event); pub const Scrollable = element.Scrollable(Event); pub const Queue = queue.Queue(Event, 256); queue: Queue, thread: ?std.Thread, quit_event: std.Thread.ResetEvent, termios: ?std.posix.termios = null, attached_handler: bool = false, prev_size: Size, pub const SignalHandler = struct { context: *anyopaque, callback: *const fn (context: *anyopaque) void, }; pub const init: @This() = .{ .queue = .{}, .thread = null, .quit_event = .{}, .termios = null, .attached_handler = false, .prev_size = .{}, }; 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); try registerWinch(.{ .context = this, .callback = @This().winsizeCallback, }); this.attached_handler = true; // post init event (as the very first element to be in the queue - event loop) this.postEvent(.init); } 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; try terminal.saveScreen(); try terminal.enterAltScreen(); try terminal.hideCursor(); try terminal.enableMouseSupport(); // send initial size afterwards const size = terminal.getTerminalSize(); this.postEvent(.{ .resize = size }); this.prev_size = size; } pub fn interrupt(this: *@This()) !void { this.quit_event.set(); try terminal.disableMouseSupport(); try terminal.exitAltScreen(); 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.disableMouseSupport(); try terminal.showCursor(); try terminal.exitAltScreen(); try terminal.disableRawMode(termios); 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(), e: Event) void { this.queue.push(e); } fn winsizeCallback(ptr: *anyopaque) void { const this: *@This() = @ptrCast(@alignCast(ptr)); const size = terminal.getTerminalSize(); if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) { this.postEvent(.{ .resize = size }); this.prev_size = size; } } 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 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 { // FIX in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue const read_bytes = try terminal.read(buf[0..]); // TODO `break` should not terminate the reading of the user inputs, but instead only the received faulty input! // escape key presses if (buf[0] == 0x1b and read_bytes > 1) { switch (buf[1]) { 0x4F => { // ss3 if (read_bytes < 3) 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; // We start iterating at index 2 to get past the '[' const sequence = for (buf[2..], 2..) |b, i| { switch (b) { 0x40...0xFF => break 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} 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, // switch case prevents in this case form ever happening }, }; this.postEvent(.{ .key = key }); }, '~' => { // Legacy keys // CSI number ~ // CSI number ; modifier ~ // CSI number ; modifier:event_type ; text_as_codepoint ~ var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';'); const number_buf = field_iter.next() orelse unreachable; // always will have one field const number = std.fmt.parseUnsigned(u16, number_buf, 10) catch break; const key: Key = .{ .cp = 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, // 200 => return .{ .event = .paste_start, .n = sequence.len }, // 201 => return .{ .event = .paste_end, .n = sequence.len }, 57427 => input.KpBegin, else => unreachable, }, }; 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' => { std.debug.assert(sequence.len >= 4); if (sequence[2] != '<') break; const delim1 = std.mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break; const button_mask = std.fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break; const delim2 = std.mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break; const px = std.fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break; const py = std.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; const mouse: Mouse = .{ .button = button, .col = px -| 1, .row = 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; }, }; this.postEvent(.{ .mouse = mouse }); }, 'c' => { // Primary DA (CSI ? Pm c) }, 'n' => { // Device Status Report // CSI Ps n // CSI ? Ps n std.debug.assert(sequence.len >= 3); }, 't' => { // XTWINOPS // Split first into fields delimited by ';' var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';'); const ps = iter.first(); if (std.mem.eql(u8, "48", ps)) { // in band window resize // CSI 48 ; height ; width ; height_pix ; width_pix t const height_char = iter.next() orelse break; const width_char = iter.next() orelse break; // TODO only post the event if the size has changed? // because there might be too many resize events (which force a re-draw of the entire screen) const size: Size = .{ .rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break, .cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break, }; if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) { this.postEvent(.{ .resize = size }); this.prev_size = size; } } }, '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 => {}, } }, // TODO parse corresponding codes // 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig else => {}, } } else { const b = buf[0]; const key: Key = switch (b) { 0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } }, 0x08 => .{ .cp = input.Backspace }, 0x09 => .{ .cp = input.Tab }, 0x0a, 0x0d => .{ .cp = input.Enter }, 0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } }, 0x1b => escape: { std.debug.assert(read_bytes == 1); break :escape .{ .cp = input.Escape }; }, 0x7f => .{ .cp = input.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; } } }; }