diff --git a/build.zig.zon b/build.zig.zon index f3f9b2d..24f97ef 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -23,8 +23,8 @@ .lazy = true, }, .aio = .{ - .url = "git+https://github.com/Cloudef/zig-aio#be8e2b374bf223202090e282447fa4581029c2eb", - .hash = "122012a11b37a350395a32fdb514e57ff54a0f9d8d4ce09498b6c45ffb7211232920", + .url = "git+https://github.com/Cloudef/zig-aio#407bb416136b61087cec2c561fa4b4103a44c5b1", + .hash = "12202405ca6dd40f314dba6472983fcbb388118ab7446d75065b1efb982d03f515d2", .lazy = true, }, }, diff --git a/examples/aio.zig b/examples/aio.zig index 4ea9ed8..6f7b3ca 100644 --- a/examples/aio.zig +++ b/examples/aio.zig @@ -41,7 +41,7 @@ fn audioTask(allocator: std.mem.Allocator) !void { const sound = blk: { var tpool: coro.ThreadPool = .{}; - try tpool.start(allocator, 1); + try tpool.start(allocator, .{}); defer tpool.deinit(); break :blk try tpool.yieldForCompletition(downloadTask, .{ allocator, "https://keroserene.net/lol/roll.s16" }); }; diff --git a/src/Loop.zig b/src/Loop.zig index c510d7c..b9bf030 100644 --- a/src/Loop.zig +++ b/src/Loop.zig @@ -8,6 +8,7 @@ const Parser = @import("Parser.zig"); const Queue = @import("queue.zig").Queue; const Tty = @import("main.zig").Tty; const Vaxis = @import("Vaxis.zig"); +const log = std.log.scoped(.loop); pub fn Loop(comptime T: type) type { return struct { @@ -15,8 +16,6 @@ pub fn Loop(comptime T: type) type { const Event = T; - const log = std.log.scoped(.loop); - tty: *Tty, vaxis: *Vaxis, @@ -110,38 +109,7 @@ pub fn Loop(comptime T: type) type { .windows => { while (!self.should_quit) { const event = try self.tty.nextEvent(); - switch (event) { - .winsize => |ws| { - if (@hasField(Event, "winsize")) { - self.postEvent(.{ .winsize = ws }); - } - }, - .key_press => |key| { - if (@hasField(Event, "key_press")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); - } - self.postEvent(.{ .key_press = mut_key }); - } - }, - .key_release => |*key| { - if (@hasField(Event, "key_release")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); - } - self.postEvent(.{ .key_release = mut_key }); - } - }, - .cap_da1 => { - std.Thread.Futex.wake(&self.vaxis.query_futex, 10); - }, - .mouse => {}, // Unsupported currently - else => {}, - } + try handleEventGeneric(self, self.vaxis, &cache, Event, event, null); } }, else => { @@ -178,102 +146,7 @@ pub fn Loop(comptime T: type) type { seq_start += result.n; const event = result.event orelse continue; - switch (event) { - .key_press => |key| { - if (@hasField(Event, "key_press")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); - } - self.postEvent(.{ .key_press = mut_key }); - } - }, - .key_release => |*key| { - if (@hasField(Event, "key_release")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); - } - self.postEvent(.{ .key_release = mut_key }); - } - }, - .mouse => |mouse| { - if (@hasField(Event, "mouse")) { - self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); - } - }, - .focus_in => { - if (@hasField(Event, "focus_in")) { - self.postEvent(.focus_in); - } - }, - .focus_out => { - if (@hasField(Event, "focus_out")) { - self.postEvent(.focus_out); - } - }, - .paste_start => { - if (@hasField(Event, "paste_start")) { - self.postEvent(.paste_start); - } - }, - .paste_end => { - if (@hasField(Event, "paste_end")) { - self.postEvent(.paste_end); - } - }, - .paste => |text| { - if (@hasField(Event, "paste")) { - self.postEvent(.{ .paste = text }); - } else { - if (paste_allocator) |_| - paste_allocator.?.free(text); - } - }, - .color_report => |report| { - if (@hasField(Event, "color_report")) { - self.postEvent(.{ .color_report = report }); - } - }, - .color_scheme => |scheme| { - if (@hasField(Event, "color_scheme")) { - self.postEvent(.{ .color_scheme = scheme }); - } - }, - .cap_kitty_keyboard => { - log.info("kitty keyboard capability detected", .{}); - self.vaxis.caps.kitty_keyboard = true; - }, - .cap_kitty_graphics => { - if (!self.vaxis.caps.kitty_graphics) { - log.info("kitty graphics capability detected", .{}); - self.vaxis.caps.kitty_graphics = true; - } - }, - .cap_rgb => { - log.info("rgb capability detected", .{}); - self.vaxis.caps.rgb = true; - }, - .cap_unicode => { - log.info("unicode capability detected", .{}); - self.vaxis.caps.unicode = .unicode; - self.vaxis.screen.width_method = .unicode; - }, - .cap_sgr_pixels => { - log.info("pixel mouse capability detected", .{}); - self.vaxis.caps.sgr_pixels = true; - }, - .cap_color_scheme_updates => { - log.info("color_scheme_updates capability detected", .{}); - self.vaxis.caps.color_scheme_updates = true; - }, - .cap_da1 => { - std.Thread.Futex.wake(&self.vaxis.query_futex, 10); - }, - .winsize => unreachable, // handled elsewhere for posix - } + try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator); } } }, @@ -281,3 +154,140 @@ pub fn Loop(comptime T: type) type { } }; } + +pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void { + switch (builtin.os.tag) { + .windows => { + switch (event) { + .winsize => |ws| { + if (@hasField(Event, "winsize")) { + return self.postEvent(.{ .winsize = ws }); + } + }, + .key_press => |key| { + if (@hasField(Event, "key_press")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + return self.postEvent(.{ .key_press = mut_key }); + } + }, + .key_release => |*key| { + if (@hasField(Event, "key_release")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + return self.postEvent(.{ .key_release = mut_key }); + } + }, + .cap_da1 => { + std.Thread.Futex.wake(&vx.query_futex, 10); + }, + .mouse => {}, // Unsupported currently + else => {}, + } + }, + else => { + switch (event) { + .key_press => |key| { + if (@hasField(Event, "key_press")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + return self.postEvent(.{ .key_press = mut_key }); + } + }, + .key_release => |*key| { + if (@hasField(Event, "key_release")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + return self.postEvent(.{ .key_release = mut_key }); + } + }, + .mouse => |mouse| { + if (@hasField(Event, "mouse")) { + return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); + } + }, + .focus_in => { + if (@hasField(Event, "focus_in")) { + return self.postEvent(.focus_in); + } + }, + .focus_out => { + if (@hasField(Event, "focus_out")) { + return self.postEvent(.focus_out); + } + }, + .paste_start => { + if (@hasField(Event, "paste_start")) { + return self.postEvent(.paste_start); + } + }, + .paste_end => { + if (@hasField(Event, "paste_end")) { + return self.postEvent(.paste_end); + } + }, + .paste => |text| { + if (@hasField(Event, "paste")) { + return self.postEvent(.{ .paste = text }); + } else { + if (paste_allocator) |_| + paste_allocator.?.free(text); + } + }, + .color_report => |report| { + if (@hasField(Event, "color_report")) { + return self.postEvent(.{ .color_report = report }); + } + }, + .color_scheme => |scheme| { + if (@hasField(Event, "color_scheme")) { + return self.postEvent(.{ .color_scheme = scheme }); + } + }, + .cap_kitty_keyboard => { + log.info("kitty keyboard capability detected", .{}); + vx.caps.kitty_keyboard = true; + }, + .cap_kitty_graphics => { + if (!vx.caps.kitty_graphics) { + log.info("kitty graphics capability detected", .{}); + vx.caps.kitty_graphics = true; + } + }, + .cap_rgb => { + log.info("rgb capability detected", .{}); + vx.caps.rgb = true; + }, + .cap_unicode => { + log.info("unicode capability detected", .{}); + vx.caps.unicode = .unicode; + vx.screen.width_method = .unicode; + }, + .cap_sgr_pixels => { + log.info("pixel mouse capability detected", .{}); + vx.caps.sgr_pixels = true; + }, + .cap_color_scheme_updates => { + log.info("color_scheme_updates capability detected", .{}); + vx.caps.color_scheme_updates = true; + }, + .cap_da1 => { + std.Thread.Futex.wake(&vx.query_futex, 10); + }, + .winsize => unreachable, // handled elsewhere for posix + } + }, + } +} diff --git a/src/aio.zig b/src/aio.zig index ee1733e..53596eb 100644 --- a/src/aio.zig +++ b/src/aio.zig @@ -3,14 +3,9 @@ const std = @import("std"); const aio = @import("aio"); const coro = @import("coro"); const vaxis = @import("main.zig"); +const handleEventGeneric = @import("Loop.zig").handleEventGeneric; const log = std.log.scoped(.vaxis_aio); -comptime { - if (builtin.target.os.tag == .windows) { - @compileError("Windows is not supported right now"); - } -} - const Yield = enum { no_state, took_event }; /// zig-aio based event loop @@ -52,9 +47,11 @@ pub fn Loop(comptime T: type) type { // keep on stack var ctx: Context = .{ .loop = self, .tty = tty }; - if (@hasField(Event, "winsize")) { - const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; - try vaxis.Tty.notifyWinsize(handler); + if (builtin.target.os.tag != .windows) { + if (@hasField(Event, "winsize")) { + const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; + try vaxis.Tty.notifyWinsize(handler); + } } while (true) { @@ -74,7 +71,32 @@ pub fn Loop(comptime T: type) type { }; } - fn ttyReaderInner(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void { + fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event { + var state: vaxis.Tty.EventState = .{}; + while (true) { + var bytes_read: usize = 0; + var input_record: vaxis.Tty.INPUT_RECORD = undefined; + try coro.io.single(aio.ReadTty{ + .tty = .{ .handle = tty.stdin }, + .buffer = std.mem.asBytes(&input_record), + .out_read = &bytes_read, + }); + + if (try tty.eventFromRecord(&input_record, &state)) |ev| { + return ev; + } + } + } + + fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void { + var cache: vaxis.GraphemeCache = .{}; + while (true) { + const event = try windowsReadEvent(tty); + try handleEventGeneric(self, vx, &cache, Event, event, null); + } + } + + fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void { // initialize a grapheme cache var cache: vaxis.GraphemeCache = .{}; @@ -93,7 +115,7 @@ pub fn Loop(comptime T: type) type { var buf: [4096]u8 = undefined; var n: usize = undefined; var read_start: usize = 0; - try coro.io.single(aio.Read{ .file = file, .buffer = buf[read_start..], .out_read = &n }); + try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n }); var seq_start: usize = 0; while (seq_start < n) { const result = try parser.parse(buf[seq_start..n], paste_allocator); @@ -111,108 +133,16 @@ pub fn Loop(comptime T: type) type { seq_start += result.n; const event = result.event orelse continue; - switch (event) { - .key_press => |key| { - if (@hasField(Event, "key_press")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); - } - try self.postEvent(.{ .key_press = mut_key }); - } - }, - .key_release => |*key| { - if (@hasField(Event, "key_release")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); - } - try self.postEvent(.{ .key_release = mut_key }); - } - }, - .mouse => |mouse| { - if (@hasField(Event, "mouse")) { - try self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); - } - }, - .focus_in => { - if (@hasField(Event, "focus_in")) { - try self.postEvent(.focus_in); - } - }, - .focus_out => { - if (@hasField(Event, "focus_out")) { - try self.postEvent(.focus_out); - } - }, - .paste_start => { - if (@hasField(Event, "paste_start")) { - try self.postEvent(.paste_start); - } - }, - .paste_end => { - if (@hasField(Event, "paste_end")) { - try self.postEvent(.paste_end); - } - }, - .paste => |text| { - if (@hasField(Event, "paste")) { - try self.postEvent(.{ .paste = text }); - } else { - if (paste_allocator) |_| - paste_allocator.?.free(text); - } - }, - .color_report => |report| { - if (@hasField(Event, "color_report")) { - try self.postEvent(.{ .color_report = report }); - } - }, - .color_scheme => |scheme| { - if (@hasField(Event, "color_scheme")) { - try self.postEvent(.{ .color_scheme = scheme }); - } - }, - .cap_kitty_keyboard => { - log.info("kitty keyboard capability detected", .{}); - vx.caps.kitty_keyboard = true; - }, - .cap_kitty_graphics => { - if (!vx.caps.kitty_graphics) { - log.info("kitty graphics capability detected", .{}); - vx.caps.kitty_graphics = true; - } - }, - .cap_rgb => { - log.info("rgb capability detected", .{}); - vx.caps.rgb = true; - }, - .cap_unicode => { - log.info("unicode capability detected", .{}); - vx.caps.unicode = .unicode; - vx.screen.width_method = .unicode; - }, - .cap_sgr_pixels => { - log.info("pixel mouse capability detected", .{}); - vx.caps.sgr_pixels = true; - }, - .cap_color_scheme_updates => { - log.info("color_scheme_updates capability detected", .{}); - vx.caps.color_scheme_updates = true; - }, - .cap_da1 => { - std.Thread.Futex.wake(&vx.query_futex, 10); - }, - .winsize => unreachable, // handled elsewhere for posix - } + try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator); } } } fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void { - self.ttyReaderInner(vx, tty, paste_allocator) catch |err| { + return switch (builtin.target.os.tag) { + .windows => self.ttyReaderWindows(vx, tty), + else => self.ttyReaderPosix(vx, tty, paste_allocator), + } catch |err| { if (err != error.Canceled) log.err("ttyReader: {}", .{err}); self.fatal = true; }; diff --git a/src/windows/Tty.zig b/src/windows/Tty.zig index 53423a1..c778714 100644 --- a/src/windows/Tty.zig +++ b/src/windows/Tty.zig @@ -119,288 +119,299 @@ pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWr pub fn nextEvent(self: *Tty) !Event { // We use a loop so we can ignore certain events - var ansi_buf: [128]u8 = undefined; - var ansi_idx: usize = 0; - var escape_st: bool = false; + var state: EventState = .{}; while (true) { var event_count: u32 = 0; var input_record: INPUT_RECORD = undefined; if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0) return windows.unexpectedError(windows.kernel32.GetLastError()); - switch (input_record.EventType) { - 0x0001 => { // Key event - const event = input_record.Event.KeyEvent; - - const base_layout: u21 = switch (event.wVirtualKeyCode) { - 0x00 => { // delivered when we get an escape sequence - ansi_buf[ansi_idx] = event.uChar.AsciiChar; - ansi_idx += 1; - if (ansi_idx <= 2) { - continue; - } - switch (ansi_buf[1]) { - '[' => { // CSI, read until 0x40 to 0xFF - switch (event.uChar.AsciiChar) { - 0x40...0xFF => { - return .cap_da1; - }, - else => continue, - } - }, - ']' => { // OSC, read until ESC \ or BEL - switch (event.uChar.AsciiChar) { - 0x07 => { - return .cap_da1; - }, - 0x1B => { - escape_st = true; - continue; - }, - '\\' => { - if (escape_st) { - return .cap_da1; - } - continue; - }, - else => continue, - } - }, - else => continue, - } - }, - 0x08 => Key.backspace, - 0x09 => Key.tab, - 0x0D => Key.enter, - 0x13 => Key.pause, - 0x14 => Key.caps_lock, - 0x1B => Key.escape, - 0x20 => Key.space, - 0x21 => Key.page_up, - 0x22 => Key.page_down, - 0x23 => Key.end, - 0x24 => Key.home, - 0x25 => Key.left, - 0x26 => Key.up, - 0x27 => Key.right, - 0x28 => Key.down, - 0x2c => Key.print_screen, - 0x2d => Key.insert, - 0x2e => Key.delete, - 0x30...0x39 => |k| k, - 0x41...0x5a => |k| k + 0x20, // translate to lowercase - 0x5b => Key.left_meta, - 0x5c => Key.right_meta, - 0x60 => Key.kp_0, - 0x61 => Key.kp_1, - 0x62 => Key.kp_2, - 0x63 => Key.kp_3, - 0x64 => Key.kp_4, - 0x65 => Key.kp_5, - 0x66 => Key.kp_6, - 0x67 => Key.kp_7, - 0x68 => Key.kp_8, - 0x69 => Key.kp_9, - 0x6a => Key.kp_multiply, - 0x6b => Key.kp_add, - 0x6c => Key.kp_separator, - 0x6d => Key.kp_subtract, - 0x6e => Key.kp_decimal, - 0x6f => Key.kp_divide, - 0x70 => Key.f1, - 0x71 => Key.f2, - 0x72 => Key.f3, - 0x73 => Key.f4, - 0x74 => Key.f5, - 0x75 => Key.f6, - 0x76 => Key.f8, - 0x77 => Key.f8, - 0x78 => Key.f9, - 0x79 => Key.f10, - 0x7a => Key.f11, - 0x7b => Key.f12, - 0x7c => Key.f13, - 0x7d => Key.f14, - 0x7e => Key.f15, - 0x7f => Key.f16, - 0x80 => Key.f17, - 0x81 => Key.f18, - 0x82 => Key.f19, - 0x83 => Key.f20, - 0x84 => Key.f21, - 0x85 => Key.f22, - 0x86 => Key.f23, - 0x87 => Key.f24, - 0x90 => Key.num_lock, - 0x91 => Key.scroll_lock, - 0xa0 => Key.left_shift, - 0xa1 => Key.right_shift, - 0xa2 => Key.left_control, - 0xa3 => Key.right_control, - 0xa4 => Key.left_alt, - 0xa5 => Key.right_alt, - 0xad => Key.mute_volume, - 0xae => Key.lower_volume, - 0xaf => Key.raise_volume, - 0xb0 => Key.media_track_next, - 0xb1 => Key.media_track_previous, - 0xb2 => Key.media_stop, - 0xb3 => Key.media_play_pause, - 0xba => ';', - 0xbb => '+', - 0xbc => ',', - 0xbd => '-', - 0xbe => '.', - 0xbf => '/', - 0xc0 => '`', - 0xdb => '[', - 0xdc => '\\', - 0xdd => ']', - 0xde => '\'', - else => continue, - }; - - var codepoint: u21 = base_layout; - var text: ?[]const u8 = null; - switch (event.uChar.UnicodeChar) { - 0x00...0x1F => {}, - else => |cp| { - codepoint = cp; - const n = try std.unicode.utf8Encode(cp, &self.buf); - text = self.buf[0..n]; - }, - } - - const key: Key = .{ - .codepoint = codepoint, - .base_layout_codepoint = base_layout, - .mods = translateMods(event.dwControlKeyState), - .text = text, - }; - - switch (event.bKeyDown) { - 0 => return .{ .key_release = key }, - else => return .{ .key_press = key }, - } - }, - 0x0002 => { // Mouse event - // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str - - const event = input_record.Event.MouseEvent; - - // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative - // is wheel_down - // Low word represents button state - const mouse_wheel_direction: i16 = blk: { - const wheelu32: u32 = event.dwButtonState >> 16; - const wheelu16: u16 = @truncate(wheelu32); - break :blk @bitCast(wheelu16); - }; - - const buttons: u16 = @truncate(event.dwButtonState); - // save the current state when we are done - defer self.last_mouse_button_press = buttons; - const button_xor = self.last_mouse_button_press ^ buttons; - - var event_type: Mouse.Type = .press; - const btn: Mouse.Button = switch (button_xor) { - 0x0000 => blk: { - // Check wheel event - if (event.dwEventFlags & 0x0004 > 0) { - if (mouse_wheel_direction > 0) - break :blk .wheel_up - else - break :blk .wheel_down; - } - - // If we have no change but one of the buttons is still pressed we have a - // drag event. Find out which button is held down - if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) { - event_type = .drag; - if (buttons & 0x0001 > 0) break :blk .left; - if (buttons & 0x0002 > 0) break :blk .right; - if (buttons & 0x0004 > 0) break :blk .middle; - if (buttons & 0x0008 > 0) break :blk .button_8; - if (buttons & 0x0010 > 0) break :blk .button_9; - } - - if (event.dwEventFlags & 0x0001 > 0) event_type = .motion; - break :blk .none; - }, - 0x0001 => blk: { - if (buttons & 0x0001 == 0) event_type = .release; - break :blk .left; - }, - 0x0002 => blk: { - if (buttons & 0x0002 == 0) event_type = .release; - break :blk .right; - }, - 0x0004 => blk: { - if (buttons & 0x0004 == 0) event_type = .release; - break :blk .middle; - }, - 0x0008 => blk: { - if (buttons & 0x0008 == 0) event_type = .release; - break :blk .button_8; - }, - 0x0010 => blk: { - if (buttons & 0x0010 == 0) event_type = .release; - break :blk .button_9; - }, - else => { - std.log.warn("unknown mouse event: {}", .{event}); - continue; - }, - }; - - const shift: u32 = 0x0010; - const alt: u32 = 0x0001 | 0x0002; - const ctrl: u32 = 0x0004 | 0x0008; - const mods: Mouse.Modifiers = .{ - .shift = event.dwControlKeyState & shift > 0, - .alt = event.dwControlKeyState & alt > 0, - .ctrl = event.dwControlKeyState & ctrl > 0, - }; - - const mouse: Mouse = .{ - .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index - .row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index - .mods = mods, - .type = event_type, - .button = btn, - }; - return .{ .mouse = mouse }; - }, - 0x0004 => { // Screen resize events - // NOTE: Even though the event comes with a size, it may not be accurate. We ask for - // the size directly when we get this event - var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); - } - const window_rect = console_info.srWindow; - const width = window_rect.Right - window_rect.Left + 1; - const height = window_rect.Bottom - window_rect.Top + 1; - return .{ - .winsize = .{ - .cols = @intCast(width), - .rows = @intCast(height), - .x_pixel = 0, - .y_pixel = 0, - }, - }; - }, - 0x0010 => { // Focus events - switch (input_record.Event.FocusEvent.bSetFocus) { - 0 => return .focus_out, - else => return .focus_in, - } - }, - else => {}, + if (try self.eventFromRecord(&input_record, &state)) |ev| { + return ev; } } } +pub const EventState = struct { + ansi_buf: [128]u8 = undefined, + ansi_idx: usize = 0, + escape_st: bool = false, +}; + +pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState) !?Event { + switch (record.EventType) { + 0x0001 => { // Key event + const event = record.Event.KeyEvent; + + const base_layout: u21 = switch (event.wVirtualKeyCode) { + 0x00 => { // delivered when we get an escape sequence + state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar; + state.ansi_idx += 1; + if (state.ansi_idx <= 2) { + return null; + } + switch (state.ansi_buf[1]) { + '[' => { // CSI, read until 0x40 to 0xFF + switch (event.uChar.AsciiChar) { + 0x40...0xFF => { + return .cap_da1; + }, + else => return null, + } + }, + ']' => { // OSC, read until ESC \ or BEL + switch (event.uChar.AsciiChar) { + 0x07 => { + return .cap_da1; + }, + 0x1B => { + state.escape_st = true; + return null; + }, + '\\' => { + if (state.escape_st) { + return .cap_da1; + } + return null; + }, + else => return null, + } + }, + else => return null, + } + }, + 0x08 => Key.backspace, + 0x09 => Key.tab, + 0x0D => Key.enter, + 0x13 => Key.pause, + 0x14 => Key.caps_lock, + 0x1B => Key.escape, + 0x20 => Key.space, + 0x21 => Key.page_up, + 0x22 => Key.page_down, + 0x23 => Key.end, + 0x24 => Key.home, + 0x25 => Key.left, + 0x26 => Key.up, + 0x27 => Key.right, + 0x28 => Key.down, + 0x2c => Key.print_screen, + 0x2d => Key.insert, + 0x2e => Key.delete, + 0x30...0x39 => |k| k, + 0x41...0x5a => |k| k + 0x20, // translate to lowercase + 0x5b => Key.left_meta, + 0x5c => Key.right_meta, + 0x60 => Key.kp_0, + 0x61 => Key.kp_1, + 0x62 => Key.kp_2, + 0x63 => Key.kp_3, + 0x64 => Key.kp_4, + 0x65 => Key.kp_5, + 0x66 => Key.kp_6, + 0x67 => Key.kp_7, + 0x68 => Key.kp_8, + 0x69 => Key.kp_9, + 0x6a => Key.kp_multiply, + 0x6b => Key.kp_add, + 0x6c => Key.kp_separator, + 0x6d => Key.kp_subtract, + 0x6e => Key.kp_decimal, + 0x6f => Key.kp_divide, + 0x70 => Key.f1, + 0x71 => Key.f2, + 0x72 => Key.f3, + 0x73 => Key.f4, + 0x74 => Key.f5, + 0x75 => Key.f6, + 0x76 => Key.f8, + 0x77 => Key.f8, + 0x78 => Key.f9, + 0x79 => Key.f10, + 0x7a => Key.f11, + 0x7b => Key.f12, + 0x7c => Key.f13, + 0x7d => Key.f14, + 0x7e => Key.f15, + 0x7f => Key.f16, + 0x80 => Key.f17, + 0x81 => Key.f18, + 0x82 => Key.f19, + 0x83 => Key.f20, + 0x84 => Key.f21, + 0x85 => Key.f22, + 0x86 => Key.f23, + 0x87 => Key.f24, + 0x90 => Key.num_lock, + 0x91 => Key.scroll_lock, + 0xa0 => Key.left_shift, + 0xa1 => Key.right_shift, + 0xa2 => Key.left_control, + 0xa3 => Key.right_control, + 0xa4 => Key.left_alt, + 0xa5 => Key.right_alt, + 0xad => Key.mute_volume, + 0xae => Key.lower_volume, + 0xaf => Key.raise_volume, + 0xb0 => Key.media_track_next, + 0xb1 => Key.media_track_previous, + 0xb2 => Key.media_stop, + 0xb3 => Key.media_play_pause, + 0xba => ';', + 0xbb => '+', + 0xbc => ',', + 0xbd => '-', + 0xbe => '.', + 0xbf => '/', + 0xc0 => '`', + 0xdb => '[', + 0xdc => '\\', + 0xdd => ']', + 0xde => '\'', + else => return null, + }; + + var codepoint: u21 = base_layout; + var text: ?[]const u8 = null; + switch (event.uChar.UnicodeChar) { + 0x00...0x1F => {}, + else => |cp| { + codepoint = cp; + const n = try std.unicode.utf8Encode(cp, &self.buf); + text = self.buf[0..n]; + }, + } + + const key: Key = .{ + .codepoint = codepoint, + .base_layout_codepoint = base_layout, + .mods = translateMods(event.dwControlKeyState), + .text = text, + }; + + switch (event.bKeyDown) { + 0 => return .{ .key_release = key }, + else => return .{ .key_press = key }, + } + }, + 0x0002 => { // Mouse event + // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str + + const event = record.Event.MouseEvent; + + // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative + // is wheel_down + // Low word represents button state + const mouse_wheel_direction: i16 = blk: { + const wheelu32: u32 = event.dwButtonState >> 16; + const wheelu16: u16 = @truncate(wheelu32); + break :blk @bitCast(wheelu16); + }; + + const buttons: u16 = @truncate(event.dwButtonState); + // save the current state when we are done + defer self.last_mouse_button_press = buttons; + const button_xor = self.last_mouse_button_press ^ buttons; + + var event_type: Mouse.Type = .press; + const btn: Mouse.Button = switch (button_xor) { + 0x0000 => blk: { + // Check wheel event + if (event.dwEventFlags & 0x0004 > 0) { + if (mouse_wheel_direction > 0) + break :blk .wheel_up + else + break :blk .wheel_down; + } + + // If we have no change but one of the buttons is still pressed we have a + // drag event. Find out which button is held down + if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) { + event_type = .drag; + if (buttons & 0x0001 > 0) break :blk .left; + if (buttons & 0x0002 > 0) break :blk .right; + if (buttons & 0x0004 > 0) break :blk .middle; + if (buttons & 0x0008 > 0) break :blk .button_8; + if (buttons & 0x0010 > 0) break :blk .button_9; + } + + if (event.dwEventFlags & 0x0001 > 0) event_type = .motion; + break :blk .none; + }, + 0x0001 => blk: { + if (buttons & 0x0001 == 0) event_type = .release; + break :blk .left; + }, + 0x0002 => blk: { + if (buttons & 0x0002 == 0) event_type = .release; + break :blk .right; + }, + 0x0004 => blk: { + if (buttons & 0x0004 == 0) event_type = .release; + break :blk .middle; + }, + 0x0008 => blk: { + if (buttons & 0x0008 == 0) event_type = .release; + break :blk .button_8; + }, + 0x0010 => blk: { + if (buttons & 0x0010 == 0) event_type = .release; + break :blk .button_9; + }, + else => { + std.log.warn("unknown mouse event: {}", .{event}); + return null; + }, + }; + + const shift: u32 = 0x0010; + const alt: u32 = 0x0001 | 0x0002; + const ctrl: u32 = 0x0004 | 0x0008; + const mods: Mouse.Modifiers = .{ + .shift = event.dwControlKeyState & shift > 0, + .alt = event.dwControlKeyState & alt > 0, + .ctrl = event.dwControlKeyState & ctrl > 0, + }; + + const mouse: Mouse = .{ + .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index + .row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index + .mods = mods, + .type = event_type, + .button = btn, + }; + return .{ .mouse = mouse }; + }, + 0x0004 => { // Screen resize events + // NOTE: Even though the event comes with a size, it may not be accurate. We ask for + // the size directly when we get this event + var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); + } + const window_rect = console_info.srWindow; + const width = window_rect.Right - window_rect.Left + 1; + const height = window_rect.Bottom - window_rect.Top + 1; + return .{ + .winsize = .{ + .cols = @intCast(width), + .rows = @intCast(height), + .x_pixel = 0, + .y_pixel = 0, + }, + }; + }, + 0x0010 => { // Focus events + switch (record.Event.FocusEvent.bSetFocus) { + 0 => return .focus_out, + else => return .focus_in, + } + }, + else => {}, + } + return null; +} + fn translateMods(mods: u32) Key.Modifiers { const left_alt: u32 = 0x0002; const right_alt: u32 = 0x0001;