From 79a0d17a66f8ae2a868d5c868200a1718c9f9019 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Mon, 10 Nov 2025 16:44:02 +0100 Subject: [PATCH] feat(io): introduce `std.Io` parameter for `App.init` The provided `std.Io` is used for running the input reading and the rendering asynch. The user can provide their desired `std.Io` implementation as they wish to use. The examples use `std.Io.Threaded` as a simple threaded solution. --- examples/continuous.zig | 5 +- examples/demo.zig | 4 +- examples/elements/alignment.zig | 5 +- examples/elements/button.zig | 5 +- examples/elements/input.zig | 5 +- examples/elements/progress.zig | 5 +- examples/elements/radio-button.zig | 5 +- examples/elements/scrollable.zig | 5 +- examples/elements/selection.zig | 5 +- examples/errors.zig | 5 +- examples/layouts/grid.zig | 5 +- examples/layouts/horizontal.zig | 5 +- examples/layouts/mixed.zig | 5 +- examples/layouts/vertical.zig | 5 +- examples/styles/palette.zig | 5 +- examples/styles/text.zig | 5 +- src/app.zig | 552 +++++++++++++++-------------- 17 files changed, 354 insertions(+), 277 deletions(-) diff --git a/examples/continuous.zig b/examples/continuous.zig index 123df86..2c737cc 100644 --- a/examples/continuous.zig +++ b/examples/continuous.zig @@ -133,7 +133,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/demo.zig b/examples/demo.zig index bb526b7..7d7c253 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -31,8 +31,10 @@ pub fn main() !void { defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); const allocator = gpa.allocator(); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); - var app: App = .init(.{}); + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/alignment.zig b/examples/elements/alignment.zig index 5580a98..604e477 100644 --- a/examples/elements/alignment.zig +++ b/examples/elements/alignment.zig @@ -32,7 +32,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/button.zig b/examples/elements/button.zig index 2921ec5..a070001 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -82,7 +82,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/input.zig b/examples/elements/input.zig index 78ec150..61cf21b 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -65,7 +65,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/progress.zig b/examples/elements/progress.zig index 349c251..5d48ef9 100644 --- a/examples/elements/progress.zig +++ b/examples/elements/progress.zig @@ -32,7 +32,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/radio-button.zig b/examples/elements/radio-button.zig index 96ce4fc..6b23fda 100644 --- a/examples/elements/radio-button.zig +++ b/examples/elements/radio-button.zig @@ -32,7 +32,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index dd17227..99c8d2e 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -65,7 +65,10 @@ pub fn main() !void { } const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/selection.zig b/examples/elements/selection.zig index 6227278..61c5716 100644 --- a/examples/elements/selection.zig +++ b/examples/elements/selection.zig @@ -32,7 +32,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/errors.zig b/examples/errors.zig index 81c53e3..0e67d22 100644 --- a/examples/errors.zig +++ b/examples/errors.zig @@ -97,7 +97,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/grid.zig b/examples/layouts/grid.zig index f7dde7b..225c4a7 100644 --- a/examples/layouts/grid.zig +++ b/examples/layouts/grid.zig @@ -35,7 +35,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/horizontal.zig b/examples/layouts/horizontal.zig index 45b3368..534870c 100644 --- a/examples/layouts/horizontal.zig +++ b/examples/layouts/horizontal.zig @@ -35,7 +35,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/mixed.zig b/examples/layouts/mixed.zig index 58d7fa5..1bad8c8 100644 --- a/examples/layouts/mixed.zig +++ b/examples/layouts/mixed.zig @@ -35,7 +35,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/vertical.zig b/examples/layouts/vertical.zig index 7a16505..d1a56c6 100644 --- a/examples/layouts/vertical.zig +++ b/examples/layouts/vertical.zig @@ -35,7 +35,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index 4ce9d25..64359c0 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -32,7 +32,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/styles/text.zig b/examples/styles/text.zig index d244510..7013fc4 100644 --- a/examples/styles/text.zig +++ b/examples/styles/text.zig @@ -178,7 +178,10 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init(.{}); + var threaded_io: std.Io.Threaded = .init(allocator); + defer threaded_io.deinit(); + + var app: App = .init(threaded_io.ioBasic(), .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/src/app.zig b/src/app.zig index c5f7523..76bae82 100644 --- a/src/app.zig +++ b/src/app.zig @@ -16,7 +16,7 @@ /// union(enum) {}, // no additional user event's /// ); /// // later on create an `App` instance and start the event loop -/// var app: App = .init(.{}); // provide instance of the model that shall be used +/// var app: App = .init(io, .{}); // provide instance of the `std.Io` and `App` model that shall be used /// try app.start(); /// defer app.stop() catch unreachable; // does not clean-up the resources used in the model /// ``` @@ -24,10 +24,10 @@ pub fn App(comptime M: type, comptime E: type) type { if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`"); if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`."); return struct { + io: std.Io, model: Model, queue: Queue, - 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, winch_registered: bool = false, @@ -41,16 +41,16 @@ pub fn App(comptime M: type, comptime E: type) type { this.postEvent(.resize); } - pub fn init(model: Model) @This() { + pub fn init(io: std.Io, model: Model) @This() { return .{ + .io = io, .model = model, .queue = .{}, - .quit_event = .unset, }; } pub fn start(this: *@This()) !void { - if (this.thread) |_| return; + if (this.future) |_| return; // post init event (as the very first element to be in the queue - event loop) this.postEvent(.init); @@ -66,8 +66,7 @@ pub fn App(comptime M: type, comptime E: type) type { this.winch_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; try terminal.enableRawMode(&termios); @@ -80,13 +79,12 @@ pub fn App(comptime M: type, comptime E: type) type { } pub fn interrupt(this: *@This()) !void { - this.quit_event.set(); try terminal.disableMouseSupport(); try terminal.restoreScreen(); try terminal.exitAltScreen(); - if (this.thread) |thread| { - thread.join(); - this.thread = null; + if (this.future) |*future| { + future.cancel(this.io) catch {}; + this.future = null; } } @@ -98,14 +96,17 @@ pub fn App(comptime M: type, comptime E: type) type { try terminal.restoreScreen(); try terminal.disableRawMode(&termios); try terminal.exitAltScreen(); + this.termios = null; } - this.termios = null; } /// Quit the application loop. - /// This will stop the internal input thread and post a **.quit** `Event`. + /// This will cancel the internal input thread and post a **.quit** `Event`. pub fn quit(this: *@This()) void { - this.quit_event.set(); + if (this.future) |*future| { + future.cancel(this.io) catch {}; + this.future = null; + } this.postEvent(.quit); } @@ -122,172 +123,204 @@ pub fn App(comptime M: type, comptime E: type) type { fn run(this: *@This()) !void { // thread to read user inputs var buf: [256]u8 = undefined; + // NOTE set the `NONBLOCK` option for the stdin file, such that reading is not blocking! + { + // 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, + }; + 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, + }; + } 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; + if (this.io.cancelRequested()) break; - 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 }, + // non-blocking read + const read_bytes = terminal.read(&buf) catch |err| switch (err) { + error.WouldBlock => { + // wait a bit + this.io.sleep(.fromMilliseconds(20), .awake) catch |e| switch (e) { + error.Canceled => break, + else => return e, + }; + continue; + }, + else => return err, + }; + + // 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: []u8 = blk: for (buf[2..], 2..) |b, i| { + switch (b) { + 0x40...0xFF => break :blk buf[0 .. i + 1], else => continue, - }; - this.postEvent(.{ .key = key }); - }, - 0x5B => { // csi - if (read_bytes < 3) continue; + } + } else 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} + var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';'); + _ = field_iter.next(); // skip first field - 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 = 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, + 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 }); - }, - '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], ';'); + }; + }, + .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 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 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 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 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 = .{ + this.postEvent(.{ + .mouse = .{ .button = button, .x = px -| 1, .y = py -| 1, @@ -297,99 +330,96 @@ pub fn App(comptime M: type, comptime E: type) type { 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 - 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; + }, + }); + }, + '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 + keypress - this.postEvent(.{ - .key = .{ - .cp = buf[1], - .mod = .{ .alt = true }, - }, - }); - }, - } - } else { - 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 iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..read_bytes], .i = 0 }; - while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } }); - continue; - }, - }; - this.postEvent(.{ .key = key }); + _ = 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 + keypress + this.postEvent(.{ + .key = .{ + .cp = buf[1], + .mod = .{ .alt = true }, + }, + }); + }, } - continue; - }; - break; + } else { + 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 iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..read_bytes], .i = 0 }; + while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } }); + continue; + }, + }; + this.postEvent(.{ .key = key }); + } } }