diff --git a/examples/continuous.zig b/examples/continuous.zig index c713da6..7f1177f 100644 --- a/examples/continuous.zig +++ b/examples/continuous.zig @@ -133,10 +133,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/demo.zig b/examples/demo.zig index a75c044..d8dab81 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -1,5 +1,5 @@ const QuitText = struct { - const text = "Press ctrl+c to quit. Press ctrl+n to launch helix."; + const text = "Press ctrl+c to quit. Press ctrl+n to launch `vim`."; pub fn element(this: *@This()) App.Element { return .{ .ptr = this, .vtable = &.{ .content = content } }; @@ -31,10 +31,8 @@ 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(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -152,8 +150,8 @@ pub fn main() !void { try app.interrupt(); renderer.size = .{}; // reset size, such that next resize will cause a full re-draw! defer app.start() catch @panic("could not start app event loop"); - var child = std.process.Child.init(&.{"hx"}, allocator); - _ = child.spawnAndWait(threaded_io.io()) catch |err| app.postEvent(.{ + var child = std.process.Child.init(&.{"vim"}, allocator); + _ = child.spawnAndWait() catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Spawning $EDITOR failed", diff --git a/examples/elements/alignment.zig b/examples/elements/alignment.zig index 5952c58..3a35bcd 100644 --- a/examples/elements/alignment.zig +++ b/examples/elements/alignment.zig @@ -32,10 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/button.zig b/examples/elements/button.zig index 0952362..e150ae8 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -82,10 +82,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/input.zig b/examples/elements/input.zig index 4455267..bf04e3d 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -65,10 +65,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/progress.zig b/examples/elements/progress.zig index 62f2f00..dd899f4 100644 --- a/examples/elements/progress.zig +++ b/examples/elements/progress.zig @@ -32,10 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); 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 ff6e0c4..15b882d 100644 --- a/examples/elements/radio-button.zig +++ b/examples/elements/radio-button.zig @@ -32,10 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index 017e4ae..ebb6305 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -65,10 +65,7 @@ pub fn main() !void { } const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/elements/selection.zig b/examples/elements/selection.zig index 2f8ec04..c19a910 100644 --- a/examples/elements/selection.zig +++ b/examples/elements/selection.zig @@ -32,10 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/errors.zig b/examples/errors.zig index 5bbba88..3a7bffb 100644 --- a/examples/errors.zig +++ b/examples/errors.zig @@ -97,10 +97,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/grid.zig b/examples/layouts/grid.zig index 9998b2b..f6f3f08 100644 --- a/examples/layouts/grid.zig +++ b/examples/layouts/grid.zig @@ -35,10 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/horizontal.zig b/examples/layouts/horizontal.zig index e9f7d3e..06c6154 100644 --- a/examples/layouts/horizontal.zig +++ b/examples/layouts/horizontal.zig @@ -35,10 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/mixed.zig b/examples/layouts/mixed.zig index 957f1db..c5bb09e 100644 --- a/examples/layouts/mixed.zig +++ b/examples/layouts/mixed.zig @@ -35,10 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/layouts/vertical.zig b/examples/layouts/vertical.zig index 928a3f9..fc5c6b3 100644 --- a/examples/layouts/vertical.zig +++ b/examples/layouts/vertical.zig @@ -35,10 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index 38d4a47..63ceb6c 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -32,10 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/examples/styles/text.zig b/examples/styles/text.zig index 06cbb50..f0f01b8 100644 --- a/examples/styles/text.zig +++ b/examples/styles/text.zig @@ -178,10 +178,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var threaded_io: std.Io.Threaded = .init(allocator, .{}); - defer threaded_io.deinit(); - - var app: App = .init(threaded_io.ioBasic(), .{}); + var app: App = .init(.{}, .{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); diff --git a/src/app.zig b/src/app.zig index 207fc93..92e9eae 100644 --- a/src/app.zig +++ b/src/app.zig @@ -27,14 +27,15 @@ pub fn App(comptime M: type, comptime E: type) type { io: std.Io, model: Model, queue: Queue, - future: ?std.Io.Future(@typeInfo(@typeInfo(@TypeOf(run)).@"fn".return_type.?).error_union.error_set!void) = null, + thread: ?Thread = null, + quit_event: Thread.ResetEvent, termios: ?posix.termios = null, winch_registered: bool = false, // global variable for the registered handler for WINCH var handler_ctx: *anyopaque = undefined; /// registered WINCH handler to report resize events - fn handleWinch(_: std.os.linux.SIG) callconv(.c) void { + fn handleWinch(_: i32) 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! @@ -46,11 +47,12 @@ pub fn App(comptime M: type, comptime E: type) type { .io = io, .model = model, .queue = .{}, + .quit_event = .{}, }; } pub fn start(this: *@This()) !void { - if (this.future) |_| return; + if (this.thread) |_| return; // post init event (as the very first element to be in the queue - event loop) this.postEvent(.init); @@ -66,7 +68,8 @@ pub fn App(comptime M: type, comptime E: type) type { this.winch_registered = true; } - this.future = this.io.async(run, .{this}); + this.quit_event.reset(); + this.thread = try Thread.spawn(.{}, @This().run, .{this}); var termios: posix.termios = undefined; try terminal.enableRawMode(&termios); @@ -79,12 +82,13 @@ 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.future) |*future| { - future.cancel(this.io) catch {}; - this.future = null; + if (this.thread) |*thread| { + thread.join(); + this.thread = null; } } @@ -104,10 +108,7 @@ pub fn App(comptime M: type, comptime E: type) type { /// Quit the application loop. /// This will cancel the internal input thread and post a **.quit** `Event`. pub fn quit(this: *@This()) void { - if (this.future) |*future| { - future.cancel(this.io) catch {}; - this.future = null; - } + this.quit_event.set(); this.postEvent(.quit); } @@ -146,285 +147,285 @@ pub fn App(comptime M: type, comptime E: type) type { }; } while (true) { - this.io.checkCancel() catch break; - var remaining_bytes: usize = 0; + this.quit_event.timedWait(20 * std.time.ns_per_ms) catch { + var remaining_bytes: usize = 0; - // non-blocking read - const read_bytes = terminal.read(buf[remaining_bytes..]) 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, - } + remaining_bytes; - - // 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, - } - } 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 + 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 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 } }); + // 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; }, - }; - this.postEvent(.{ .key = key }); - } + else => return err, + } + remaining_bytes; + + // 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, + } + } 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 + 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 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 } }); + continue; + }, + }; + this.postEvent(.{ .key = key }); + } + continue; + }; + break; } } diff --git a/src/event.zig b/src/event.zig index 3b06104..e97cb67 100644 --- a/src/event.zig +++ b/src/event.zig @@ -46,44 +46,21 @@ 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 field_names: [a_fields.len + b_fields.len][:0]const u8 = undefined; - var field_types: [a_fields.len + b_fields.len]type = undefined; - var field_attributes: [a_fields.len + b_fields.len]std.builtin.Type.UnionField.Attributes = undefined; - var enum_names: [a_fields.len + b_fields.len][:0]const u8 = undefined; - const UEnumSize = blk: { - const total_size = a_fields.len + b_fields.len; - break :blk switch (total_size) { - 1...2 => u1, - 3...4 => u2, - 5...8 => u4, - 9...16 => u4, - 17...32 => u5, - 33...64 => u6, - 65...128 => u7, - 129...256 => u8, - else => u16, // should suffice - }; - }; - var enum_values: [a_fields.len + b_fields.len]UEnumSize = undefined; + 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 i: usize = 0; for (a_fields, a_enum_fields) |field, enum_field| { - field_names[i] = field.name; - field_types[i] = field.type; - field_attributes[i] = .{ .@"align" = field.alignment }; + fields[i] = field; var enum_f = enum_field; enum_f.value = i; - enum_names[i] = enum_f.name; - enum_values[i] = enum_f.value; + enum_fields[i] = enum_f; i += 1; } for (b_fields, b_enum_fields) |field, enum_field| { - field_names[i] = field.name; - field_types[i] = field.type; - field_attributes[i] = .{ .@"align" = field.alignment }; + fields[i] = field; var enum_f = enum_field; enum_f.value = i; - enum_names[i] = enum_f.name; - enum_values[i] = enum_f.value; + enum_fields[i] = enum_f; i += 1; } @@ -94,22 +71,35 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { // user provided one) const a_enum_decls = @typeInfo(A).@"union".decls; const b_enum_decls = @typeInfo(B).@"union".decls; - var decl_names: [a_enum_decls.len + b_enum_decls.len]std.builtin.Type.Declaration = undefined; + var decls: [a_enum_decls.len + b_enum_decls.len]std.builtin.Type.Declaration = undefined; var j: usize = 0; for (a_enum_decls) |decl| { - decl_names[j] = decl.name; + decls[j] = decl; j += 1; } for (b_enum_decls) |decl| { - decl_names[j] = decl.name; + decls[j] = decl; j += 1; } - const EventType = @Int(.unsigned, @bitSizeOf(@TypeOf(i)) - @clz(i)); + const EventType = @Type(.{ .int = .{ + .signedness = .unsigned, + .bits = @bitSizeOf(@TypeOf(i)) - @clz(i), + } }); - const Event = @Enum(EventType, .exhaustive, enum_names[0..], enum_values[0..]); + const Event = @Type(.{ .@"enum" = .{ + .tag_type = EventType, + .fields = enum_fields[0..], + .decls = &.{}, + .is_exhaustive = true, + } }); - return @Union(.auto, Event, field_names[0..], field_types[0..], field_attributes[0..]); + return @Type(.{ .@"union" = .{ + .layout = .auto, + .tag_type = Event, + .fields = fields[0..], + .decls = &.{}, + } }); } /// Determine whether the provided type `T` is a tagged union: `union(enum)`. diff --git a/src/testing.zig b/src/testing.zig index 1fd96b8..5bcee18 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -211,13 +211,16 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu if (!differ) return; // test failed - var stdout_buffer: [1024]u8 = undefined; - const io = debug.lockStderr(&stdout_buffer); - defer debug.unlockStderr(); + var buf: [1024]u8 = undefined; - const error_writer = &io.file_writer.interface; + std.debug.lockStdErr(); + defer std.debug.unlockStdErr(); + + var buffer = std.fs.File.stderr().writer(&buf); + var error_writer = &buffer.interface; try error_writer.writeAll(writer.buffer[0..writer.end]); try error_writer.flush(); + return error.TestExpectEqualCells; }