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