a modern tui library written in zig
at main 28 kB view raw
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};