this repo has no description
1const std = @import("std");
2const builtin = @import("builtin");
3
4const vaxis = @import("main.zig");
5
6const ctlseqs = vaxis.ctlseqs;
7const posix = std.posix;
8const windows = std.os.windows;
9
10const Event = vaxis.Event;
11const Key = vaxis.Key;
12const Mouse = vaxis.Mouse;
13const Parser = vaxis.Parser;
14const Winsize = vaxis.Winsize;
15
16/// The target TTY implementation
17pub const Tty = if (builtin.is_test)
18 TestTty
19else switch (builtin.os.tag) {
20 .windows => WindowsTty,
21 else => PosixTty,
22};
23
24/// global tty instance, used in case of a panic. Not guaranteed to work if
25/// for some reason there are multiple TTYs open under a single vaxis
26/// compilation unit - but this is better than nothing
27pub var global_tty: ?Tty = null;
28
29pub const PosixTty = struct {
30 /// the original state of the terminal, prior to calling makeRaw
31 termios: posix.termios,
32
33 /// The file descriptor of the tty
34 fd: posix.fd_t,
35
36 /// File.Writer for efficient buffered writing
37 writer: std.fs.File.Writer,
38
39 pub const SignalHandler = struct {
40 context: *anyopaque,
41 callback: *const fn (context: *anyopaque) void,
42 };
43
44 /// global signal handlers
45 var handlers: [8]SignalHandler = undefined;
46 var handler_mutex: std.Thread.Mutex = .{};
47 var handler_idx: usize = 0;
48
49 var handler_installed: bool = false;
50
51 /// initializes a Tty instance by opening /dev/tty and "making it raw". A
52 /// signal handler is installed for SIGWINCH. No callbacks are installed, be
53 /// sure to register a callback when initializing the event loop
54 pub fn init(buffer: []u8) !PosixTty {
55 // Open our tty
56 const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0);
57
58 // Set the termios of the tty
59 const termios = try makeRaw(fd);
60
61 var act = posix.Sigaction{
62 .handler = .{ .handler = PosixTty.handleWinch },
63 .mask = switch (builtin.os.tag) {
64 .macos => 0,
65 else => posix.sigemptyset(),
66 },
67 .flags = 0,
68 };
69 posix.sigaction(posix.SIG.WINCH, &act, null);
70 handler_installed = true;
71
72 const file = std.fs.File{ .handle = fd };
73
74 const self: PosixTty = .{
75 .fd = fd,
76 .termios = termios,
77 .writer = std.fs.File.Writer.initStreaming(file, buffer),
78 };
79
80 global_tty = self;
81
82 return self;
83 }
84
85 /// release resources associated with the Tty return it to its original state
86 pub fn deinit(self: PosixTty) void {
87 posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| {
88 std.log.err("couldn't restore terminal: {}", .{err});
89 };
90 if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos
91 posix.close(self.fd);
92 }
93
94 /// Resets the signal handler to it's default
95 pub fn resetSignalHandler() void {
96 if (!handler_installed) return;
97 handler_installed = false;
98 var act = posix.Sigaction{
99 .handler = .{ .handler = posix.SIG.DFL },
100 .mask = switch (builtin.os.tag) {
101 .macos => 0,
102 else => posix.sigemptyset(),
103 },
104 .flags = 0,
105 };
106 posix.sigaction(posix.SIG.WINCH, &act, null);
107 }
108
109 /// Write bytes to the tty
110 pub fn write(self: *PosixTty, bytes: []const u8) !usize {
111 return self.writer.interface.write(bytes);
112 }
113
114 pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize {
115 const self: *PosixTty = @ptrCast(@alignCast(ptr));
116 return self.writer.interface.write(bytes);
117 }
118
119 pub fn anyWriter(self: *const PosixTty) *std.io.Writer {
120 return @constCast(&self.writer.interface);
121 }
122
123 pub fn read(self: *const PosixTty, buf: []u8) !usize {
124 return posix.read(self.fd, buf);
125 }
126
127 pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
128 const self: *const PosixTty = @ptrCast(@alignCast(ptr));
129 return posix.read(self.fd, buf);
130 }
131
132 pub fn anyReader(self: *const PosixTty) std.io.AnyReader {
133 return .{
134 .context = self,
135 .readFn = PosixTty.opaqueRead,
136 };
137 }
138
139 /// Install a signal handler for winsize. A maximum of 8 handlers may be
140 /// installed
141 pub fn notifyWinsize(handler: SignalHandler) !void {
142 handler_mutex.lock();
143 defer handler_mutex.unlock();
144 if (handler_idx == handlers.len) return error.OutOfMemory;
145 handlers[handler_idx] = handler;
146 handler_idx += 1;
147 }
148
149 fn handleWinch(_: c_int) callconv(.c) void {
150 handler_mutex.lock();
151 defer handler_mutex.unlock();
152 var i: usize = 0;
153 while (i < handler_idx) : (i += 1) {
154 const handler = handlers[i];
155 handler.callback(handler.context);
156 }
157 }
158
159 /// makeRaw enters the raw state for the terminal.
160 pub fn makeRaw(fd: posix.fd_t) !posix.termios {
161 const state = try posix.tcgetattr(fd);
162 var raw = state;
163 // see termios(3)
164 raw.iflag.IGNBRK = false;
165 raw.iflag.BRKINT = false;
166 raw.iflag.PARMRK = false;
167 raw.iflag.ISTRIP = false;
168 raw.iflag.INLCR = false;
169 raw.iflag.IGNCR = false;
170 raw.iflag.ICRNL = false;
171 raw.iflag.IXON = false;
172
173 raw.oflag.OPOST = false;
174
175 raw.lflag.ECHO = false;
176 raw.lflag.ECHONL = false;
177 raw.lflag.ICANON = false;
178 raw.lflag.ISIG = false;
179 raw.lflag.IEXTEN = false;
180
181 raw.cflag.CSIZE = .CS8;
182 raw.cflag.PARENB = false;
183
184 raw.cc[@intFromEnum(posix.V.MIN)] = 1;
185 raw.cc[@intFromEnum(posix.V.TIME)] = 0;
186 try posix.tcsetattr(fd, .FLUSH, raw);
187 return state;
188 }
189
190 /// Get the window size from the kernel
191 pub fn getWinsize(fd: posix.fd_t) !Winsize {
192 var winsize = posix.winsize{
193 .row = 0,
194 .col = 0,
195 .xpixel = 0,
196 .ypixel = 0,
197 };
198
199 const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
200 if (posix.errno(err) == .SUCCESS)
201 return Winsize{
202 .rows = winsize.row,
203 .cols = winsize.col,
204 .x_pixel = winsize.xpixel,
205 .y_pixel = winsize.ypixel,
206 };
207 return error.IoctlError;
208 }
209
210 pub fn bufferedWriter(self: *const PosixTty) *std.io.Writer {
211 // The embedded writer is already buffered with a 4096-byte buffer
212 return self.anyWriter();
213 }
214};
215
216pub const WindowsTty = struct {
217 stdin: windows.HANDLE,
218 stdout: windows.HANDLE,
219
220 initial_codepage: c_uint,
221 initial_input_mode: CONSOLE_MODE_INPUT,
222 initial_output_mode: CONSOLE_MODE_OUTPUT,
223
224 // a buffer to write key text into
225 buf: [4]u8 = undefined,
226
227 /// File.Writer for efficient buffered writing
228 writer: std.fs.File.Writer,
229
230 /// The last mouse button that was pressed. We store the previous state of button presses on each
231 /// mouse event so we can detect which button was released
232 last_mouse_button_press: u16 = 0,
233
234 const utf8_codepage: c_uint = 65001;
235
236 /// The input mode set by init
237 pub const input_raw_mode: CONSOLE_MODE_INPUT = .{
238 .WINDOW_INPUT = 1, // resize events
239 .MOUSE_INPUT = 1,
240 .EXTENDED_FLAGS = 1, // allow mouse events
241 };
242
243 /// The output mode set by init
244 pub const output_raw_mode: CONSOLE_MODE_OUTPUT = .{
245 .PROCESSED_OUTPUT = 1, // handle control sequences
246 .VIRTUAL_TERMINAL_PROCESSING = 1, // handle ANSI sequences
247 .DISABLE_NEWLINE_AUTO_RETURN = 1, // disable inserting a new line when we write at the last column
248 .ENABLE_LVB_GRID_WORLDWIDE = 1, // enables reverse video and underline
249 };
250
251 pub fn init(buffer: []u8) !Tty {
252 const stdin = std.io.getStdIn().handle;
253 const stdout = std.io.getStdOut().handle;
254
255 // get initial modes
256 const initial_output_codepage = windows.kernel32.GetConsoleOutputCP();
257 const initial_input_mode = try getConsoleMode(CONSOLE_MODE_INPUT, stdin);
258 const initial_output_mode = try getConsoleMode(CONSOLE_MODE_OUTPUT, stdout);
259
260 // set new modes
261 try setConsoleMode(stdin, input_raw_mode);
262 try setConsoleMode(stdout, output_raw_mode);
263 if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0)
264 return windows.unexpectedError(windows.kernel32.GetLastError());
265
266 const file = std.fs.File{ .handle = stdout };
267
268 const self: Tty = .{
269 .stdin = stdin,
270 .stdout = stdout,
271 .initial_codepage = initial_output_codepage,
272 .initial_input_mode = initial_input_mode,
273 .initial_output_mode = initial_output_mode,
274 .writer = std.fs.File.Writer.initStreaming(file, buffer),
275 };
276
277 // save a copy of this tty as the global_tty for panic handling
278 global_tty = self;
279
280 return self;
281 }
282
283 pub fn deinit(self: Tty) void {
284 _ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage);
285 setConsoleMode(self.stdin, self.initial_input_mode) catch {};
286 setConsoleMode(self.stdout, self.initial_output_mode) catch {};
287 windows.CloseHandle(self.stdin);
288 windows.CloseHandle(self.stdout);
289 }
290
291 pub const CONSOLE_MODE_INPUT = packed struct(u32) {
292 PROCESSED_INPUT: u1 = 0,
293 LINE_INPUT: u1 = 0,
294 ECHO_INPUT: u1 = 0,
295 WINDOW_INPUT: u1 = 0,
296 MOUSE_INPUT: u1 = 0,
297 INSERT_MODE: u1 = 0,
298 QUICK_EDIT_MODE: u1 = 0,
299 EXTENDED_FLAGS: u1 = 0,
300 AUTO_POSITION: u1 = 0,
301 VIRTUAL_TERMINAL_INPUT: u1 = 0,
302 _: u22 = 0,
303 };
304 pub const CONSOLE_MODE_OUTPUT = packed struct(u32) {
305 PROCESSED_OUTPUT: u1 = 0,
306 WRAP_AT_EOL_OUTPUT: u1 = 0,
307 VIRTUAL_TERMINAL_PROCESSING: u1 = 0,
308 DISABLE_NEWLINE_AUTO_RETURN: u1 = 0,
309 ENABLE_LVB_GRID_WORLDWIDE: u1 = 0,
310 _: u27 = 0,
311 };
312
313 pub fn getConsoleMode(comptime T: type, handle: windows.HANDLE) !T {
314 var mode: u32 = undefined;
315 if (windows.kernel32.GetConsoleMode(handle, &mode) == 0) return switch (windows.kernel32.GetLastError()) {
316 .INVALID_HANDLE => error.InvalidHandle,
317 else => |e| windows.unexpectedError(e),
318 };
319 return @bitCast(mode);
320 }
321
322 pub fn setConsoleMode(handle: windows.HANDLE, mode: anytype) !void {
323 if (windows.kernel32.SetConsoleMode(handle, @bitCast(mode)) == 0) return switch (windows.kernel32.GetLastError()) {
324 .INVALID_HANDLE => error.InvalidHandle,
325 else => |e| windows.unexpectedError(e),
326 };
327 }
328
329 /// Write bytes to the tty
330 pub fn write(self: *Tty, bytes: []const u8) !usize {
331 return self.writer.interface.write(bytes);
332 }
333
334 pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize {
335 const self: *Tty = @ptrCast(@alignCast(ptr));
336 return self.writer.interface.write(bytes);
337 }
338
339 pub fn anyWriter(self: *const Tty) *std.io.Writer {
340 return @constCast(&self.writer.interface);
341 }
342
343 pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
344 // We use a loop so we can ignore certain events
345 var state: EventState = .{};
346 while (true) {
347 var event_count: u32 = 0;
348 var input_record: INPUT_RECORD = undefined;
349 if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0)
350 return windows.unexpectedError(windows.kernel32.GetLastError());
351
352 if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
353 return ev;
354 }
355 }
356 }
357
358 pub const EventState = struct {
359 ansi_buf: [128]u8 = undefined,
360 ansi_idx: usize = 0,
361 utf16_buf: [2]u16 = undefined,
362 utf16_half: bool = false,
363 };
364
365 pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
366 switch (record.EventType) {
367 0x0001 => { // Key event
368 const event = record.Event.KeyEvent;
369
370 if (state.utf16_half) half: {
371 state.utf16_half = false;
372 state.utf16_buf[1] = event.uChar.UnicodeChar;
373 const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half;
374 const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null;
375
376 const key: Key = .{
377 .codepoint = codepoint,
378 .base_layout_codepoint = codepoint,
379 .mods = translateMods(event.dwControlKeyState),
380 .text = self.buf[0..n],
381 };
382
383 switch (event.bKeyDown) {
384 0 => return .{ .key_release = key },
385 else => return .{ .key_press = key },
386 }
387 }
388
389 const base_layout: u16 = switch (event.wVirtualKeyCode) {
390 0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint
391 if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27)
392 break :blk event.uChar.UnicodeChar;
393 state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar;
394 state.ansi_idx += 1;
395 if (state.ansi_idx <= 2) return null;
396 const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator);
397 return if (result.n == 0) null else evt: {
398 state.ansi_idx = 0;
399 break :evt result.event;
400 };
401 },
402 0x08 => Key.backspace,
403 0x09 => Key.tab,
404 0x0D => Key.enter,
405 0x13 => Key.pause,
406 0x14 => Key.caps_lock,
407 0x1B => Key.escape,
408 0x20 => Key.space,
409 0x21 => Key.page_up,
410 0x22 => Key.page_down,
411 0x23 => Key.end,
412 0x24 => Key.home,
413 0x25 => Key.left,
414 0x26 => Key.up,
415 0x27 => Key.right,
416 0x28 => Key.down,
417 0x2c => Key.print_screen,
418 0x2d => Key.insert,
419 0x2e => Key.delete,
420 0x30...0x39 => |k| k,
421 0x41...0x5a => |k| k + 0x20, // translate to lowercase
422 0x5b => Key.left_meta,
423 0x5c => Key.right_meta,
424 0x60 => Key.kp_0,
425 0x61 => Key.kp_1,
426 0x62 => Key.kp_2,
427 0x63 => Key.kp_3,
428 0x64 => Key.kp_4,
429 0x65 => Key.kp_5,
430 0x66 => Key.kp_6,
431 0x67 => Key.kp_7,
432 0x68 => Key.kp_8,
433 0x69 => Key.kp_9,
434 0x6a => Key.kp_multiply,
435 0x6b => Key.kp_add,
436 0x6c => Key.kp_separator,
437 0x6d => Key.kp_subtract,
438 0x6e => Key.kp_decimal,
439 0x6f => Key.kp_divide,
440 0x70 => Key.f1,
441 0x71 => Key.f2,
442 0x72 => Key.f3,
443 0x73 => Key.f4,
444 0x74 => Key.f5,
445 0x75 => Key.f6,
446 0x76 => Key.f8,
447 0x77 => Key.f8,
448 0x78 => Key.f9,
449 0x79 => Key.f10,
450 0x7a => Key.f11,
451 0x7b => Key.f12,
452 0x7c => Key.f13,
453 0x7d => Key.f14,
454 0x7e => Key.f15,
455 0x7f => Key.f16,
456 0x80 => Key.f17,
457 0x81 => Key.f18,
458 0x82 => Key.f19,
459 0x83 => Key.f20,
460 0x84 => Key.f21,
461 0x85 => Key.f22,
462 0x86 => Key.f23,
463 0x87 => Key.f24,
464 0x90 => Key.num_lock,
465 0x91 => Key.scroll_lock,
466 0xa0 => Key.left_shift,
467 0x10 => Key.left_shift,
468 0xa1 => Key.right_shift,
469 0xa2 => Key.left_control,
470 0x11 => Key.left_control,
471 0xa3 => Key.right_control,
472 0xa4 => Key.left_alt,
473 0x12 => Key.left_alt,
474 0xa5 => Key.right_alt,
475 0xad => Key.mute_volume,
476 0xae => Key.lower_volume,
477 0xaf => Key.raise_volume,
478 0xb0 => Key.media_track_next,
479 0xb1 => Key.media_track_previous,
480 0xb2 => Key.media_stop,
481 0xb3 => Key.media_play_pause,
482 0xba => ';',
483 0xbb => '+',
484 0xbc => ',',
485 0xbd => '-',
486 0xbe => '.',
487 0xbf => '/',
488 0xc0 => '`',
489 0xdb => '[',
490 0xdc => '\\',
491 0xe2 => '\\',
492 0xdd => ']',
493 0xde => '\'',
494 else => {
495 const log = std.log.scoped(.vaxis);
496 log.warn("unknown wVirtualKeyCode: 0x{x}", .{event.wVirtualKeyCode});
497 return null;
498 },
499 };
500
501 if (std.unicode.utf16IsHighSurrogate(base_layout)) {
502 state.utf16_buf[0] = base_layout;
503 state.utf16_half = true;
504 return null;
505 }
506 if (std.unicode.utf16IsLowSurrogate(base_layout)) {
507 return null;
508 }
509
510 var codepoint: u21 = base_layout;
511 var text: ?[]const u8 = null;
512 switch (event.uChar.UnicodeChar) {
513 0x00...0x1F => {},
514 else => |cp| {
515 codepoint = cp;
516 const n = try std.unicode.utf8Encode(codepoint, &self.buf);
517 text = self.buf[0..n];
518 },
519 }
520
521 const key: Key = .{
522 .codepoint = codepoint,
523 .base_layout_codepoint = base_layout,
524 .mods = translateMods(event.dwControlKeyState),
525 .text = text,
526 };
527
528 switch (event.bKeyDown) {
529 0 => return .{ .key_release = key },
530 else => return .{ .key_press = key },
531 }
532 },
533 0x0002 => { // Mouse event
534 // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
535
536 const event = record.Event.MouseEvent;
537
538 // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
539 // is wheel_down
540 // Low word represents button state
541 const mouse_wheel_direction: i16 = blk: {
542 const wheelu32: u32 = event.dwButtonState >> 16;
543 const wheelu16: u16 = @truncate(wheelu32);
544 break :blk @bitCast(wheelu16);
545 };
546
547 const buttons: u16 = @truncate(event.dwButtonState);
548 // save the current state when we are done
549 defer self.last_mouse_button_press = buttons;
550 const button_xor = self.last_mouse_button_press ^ buttons;
551
552 var event_type: Mouse.Type = .press;
553 const btn: Mouse.Button = switch (button_xor) {
554 0x0000 => blk: {
555 // Check wheel event
556 if (event.dwEventFlags & 0x0004 > 0) {
557 if (mouse_wheel_direction > 0)
558 break :blk .wheel_up
559 else
560 break :blk .wheel_down;
561 }
562
563 // If we have no change but one of the buttons is still pressed we have a
564 // drag event. Find out which button is held down
565 if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
566 event_type = .drag;
567 if (buttons & 0x0001 > 0) break :blk .left;
568 if (buttons & 0x0002 > 0) break :blk .right;
569 if (buttons & 0x0004 > 0) break :blk .middle;
570 if (buttons & 0x0008 > 0) break :blk .button_8;
571 if (buttons & 0x0010 > 0) break :blk .button_9;
572 }
573
574 if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
575 break :blk .none;
576 },
577 0x0001 => blk: {
578 if (buttons & 0x0001 == 0) event_type = .release;
579 break :blk .left;
580 },
581 0x0002 => blk: {
582 if (buttons & 0x0002 == 0) event_type = .release;
583 break :blk .right;
584 },
585 0x0004 => blk: {
586 if (buttons & 0x0004 == 0) event_type = .release;
587 break :blk .middle;
588 },
589 0x0008 => blk: {
590 if (buttons & 0x0008 == 0) event_type = .release;
591 break :blk .button_8;
592 },
593 0x0010 => blk: {
594 if (buttons & 0x0010 == 0) event_type = .release;
595 break :blk .button_9;
596 },
597 else => {
598 std.log.warn("unknown mouse event: {}", .{event});
599 return null;
600 },
601 };
602
603 const shift: u32 = 0x0010;
604 const alt: u32 = 0x0001 | 0x0002;
605 const ctrl: u32 = 0x0004 | 0x0008;
606 const mods: Mouse.Modifiers = .{
607 .shift = event.dwControlKeyState & shift > 0,
608 .alt = event.dwControlKeyState & alt > 0,
609 .ctrl = event.dwControlKeyState & ctrl > 0,
610 };
611
612 const mouse: Mouse = .{
613 .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
614 .row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
615 .mods = mods,
616 .type = event_type,
617 .button = btn,
618 };
619 return .{ .mouse = mouse };
620 },
621 0x0004 => { // Screen resize events
622 // NOTE: Even though the event comes with a size, it may not be accurate. We ask for
623 // the size directly when we get this event
624 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
625 if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) {
626 return windows.unexpectedError(windows.kernel32.GetLastError());
627 }
628 const window_rect = console_info.srWindow;
629 const width = window_rect.Right - window_rect.Left + 1;
630 const height = window_rect.Bottom - window_rect.Top + 1;
631 return .{
632 .winsize = .{
633 .cols = @intCast(width),
634 .rows = @intCast(height),
635 .x_pixel = 0,
636 .y_pixel = 0,
637 },
638 };
639 },
640 0x0010 => { // Focus events
641 switch (record.Event.FocusEvent.bSetFocus) {
642 0 => return .focus_out,
643 else => return .focus_in,
644 }
645 },
646 else => {},
647 }
648 return null;
649 }
650
651 fn translateMods(mods: u32) Key.Modifiers {
652 const left_alt: u32 = 0x0002;
653 const right_alt: u32 = 0x0001;
654 const left_ctrl: u32 = 0x0008;
655 const right_ctrl: u32 = 0x0004;
656
657 const caps: u32 = 0x0080;
658 const num_lock: u32 = 0x0020;
659 const shift: u32 = 0x0010;
660 const alt: u32 = left_alt | right_alt;
661 const ctrl: u32 = left_ctrl | right_ctrl;
662
663 return .{
664 .shift = mods & shift > 0,
665 .alt = mods & alt > 0,
666 .ctrl = mods & ctrl > 0,
667 .caps_lock = mods & caps > 0,
668 .num_lock = mods & num_lock > 0,
669 };
670 }
671
672 // From gitub.com/ziglibs/zig-windows-console. Thanks :)
673 //
674 // Events
675 const union_unnamed_248 = extern union {
676 UnicodeChar: windows.WCHAR,
677 AsciiChar: windows.CHAR,
678 };
679 pub const KEY_EVENT_RECORD = extern struct {
680 bKeyDown: windows.BOOL,
681 wRepeatCount: windows.WORD,
682 wVirtualKeyCode: windows.WORD,
683 wVirtualScanCode: windows.WORD,
684 uChar: union_unnamed_248,
685 dwControlKeyState: windows.DWORD,
686 };
687 pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD;
688
689 pub const MOUSE_EVENT_RECORD = extern struct {
690 dwMousePosition: windows.COORD,
691 dwButtonState: windows.DWORD,
692 dwControlKeyState: windows.DWORD,
693 dwEventFlags: windows.DWORD,
694 };
695 pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD;
696
697 pub const WINDOW_BUFFER_SIZE_RECORD = extern struct {
698 dwSize: windows.COORD,
699 };
700 pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD;
701
702 pub const MENU_EVENT_RECORD = extern struct {
703 dwCommandId: windows.UINT,
704 };
705 pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD;
706
707 pub const FOCUS_EVENT_RECORD = extern struct {
708 bSetFocus: windows.BOOL,
709 };
710 pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD;
711
712 const union_unnamed_249 = extern union {
713 KeyEvent: KEY_EVENT_RECORD,
714 MouseEvent: MOUSE_EVENT_RECORD,
715 WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD,
716 MenuEvent: MENU_EVENT_RECORD,
717 FocusEvent: FOCUS_EVENT_RECORD,
718 };
719 pub const INPUT_RECORD = extern struct {
720 EventType: windows.WORD,
721 Event: union_unnamed_249,
722 };
723 pub const PINPUT_RECORD = *INPUT_RECORD;
724
725 pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL;
726};
727
728pub const TestTty = struct {
729 /// Used for API compat
730 fd: posix.fd_t,
731 pipe_read: posix.fd_t,
732 pipe_write: posix.fd_t,
733 writer: *std.io.Writer.Allocating,
734
735 /// Initializes a TestTty.
736 pub fn init() !TestTty {
737 if (builtin.os.tag == .windows) return error.SkipZigTest;
738 const list = try std.testing.allocator.create(std.io.Writer.Allocating);
739 list.* = .init(std.testing.allocator);
740 const r, const w = try posix.pipe();
741 return .{
742 .fd = r,
743 .pipe_read = r,
744 .pipe_write = w,
745 .writer = list,
746 };
747 }
748
749 pub fn deinit(self: TestTty) void {
750 std.posix.close(self.pipe_read);
751 std.posix.close(self.pipe_write);
752 self.writer.deinit();
753 std.testing.allocator.destroy(self.writer);
754 }
755
756 pub fn anyWriter(self: *const TestTty) *std.io.Writer {
757 return &self.writer.writer;
758 }
759
760 pub fn read(self: *const TestTty, buf: []u8) !usize {
761 return posix.read(self.fd, buf);
762 }
763
764 pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
765 const self: *const TestTty = @ptrCast(@alignCast(ptr));
766 return posix.read(self.fd, buf);
767 }
768
769 pub fn anyReader(self: *const TestTty) std.io.AnyReader {
770 return .{
771 .context = self,
772 .readFn = TestTty.opaqueRead,
773 };
774 }
775
776 /// Get the window size from the kernel
777 pub fn getWinsize(_: posix.fd_t) !Winsize {
778 return .{
779 .rows = 40,
780 .cols = 80,
781 .x_pixel = 40 * 8,
782 .y_pixel = 40 * 8 * 2,
783 };
784 }
785
786 /// Implemented for the Windows API
787 pub fn nextEvent(_: *Tty, _: *Parser, _: ?std.mem.Allocator) !Event {
788 return error.SkipZigTest;
789 }
790
791 pub fn resetSignalHandler() void {
792 return;
793 }
794};