a modern tui library written in zig
at v0.5.1 497 lines 18 kB view raw
1//! A Windows TTY implementation, using virtual terminal process output and 2//! native windows input 3const Tty = @This(); 4 5const std = @import("std"); 6const Event = @import("../event.zig").Event; 7const Key = @import("../Key.zig"); 8const Mouse = @import("../Mouse.zig"); 9const Parser = @import("../Parser.zig"); 10const windows = std.os.windows; 11 12stdin: windows.HANDLE, 13stdout: windows.HANDLE, 14 15initial_codepage: c_uint, 16initial_input_mode: u32, 17initial_output_mode: u32, 18 19// a buffer to write key text into 20buf: [4]u8 = undefined, 21 22/// The last mouse button that was pressed. We store the previous state of button presses on each 23/// mouse event so we can detect which button was released 24last_mouse_button_press: u16 = 0, 25 26pub var global_tty: ?Tty = null; 27 28const utf8_codepage: c_uint = 65001; 29 30const InputMode = struct { 31 const enable_window_input: u32 = 0x0008; // resize events 32 const enable_mouse_input: u32 = 0x0010; 33 const enable_extended_flags: u32 = 0x0080; // allows mouse events 34 35 pub fn rawMode() u32 { 36 return enable_window_input | enable_mouse_input | enable_extended_flags; 37 } 38}; 39 40const OutputMode = struct { 41 const enable_processed_output: u32 = 0x0001; // handle control sequences 42 const enable_virtual_terminal_processing: u32 = 0x0004; // handle ANSI sequences 43 const disable_newline_auto_return: u32 = 0x0008; // disable inserting a new line when we write at the last column 44 const enable_lvb_grid_worldwide: u32 = 0x0010; // enables reverse video and underline 45 46 fn rawMode() u32 { 47 return enable_processed_output | 48 enable_virtual_terminal_processing | 49 disable_newline_auto_return | 50 enable_lvb_grid_worldwide; 51 } 52}; 53 54pub fn init() !Tty { 55 const stdin = try windows.GetStdHandle(windows.STD_INPUT_HANDLE); 56 const stdout = try windows.GetStdHandle(windows.STD_OUTPUT_HANDLE); 57 58 // get initial modes 59 var initial_input_mode: windows.DWORD = undefined; 60 var initial_output_mode: windows.DWORD = undefined; 61 const initial_output_codepage = windows.kernel32.GetConsoleOutputCP(); 62 { 63 if (windows.kernel32.GetConsoleMode(stdin, &initial_input_mode) == 0) { 64 return windows.unexpectedError(windows.kernel32.GetLastError()); 65 } 66 if (windows.kernel32.GetConsoleMode(stdout, &initial_output_mode) == 0) { 67 return windows.unexpectedError(windows.kernel32.GetLastError()); 68 } 69 } 70 71 // set new modes 72 { 73 if (SetConsoleMode(stdin, InputMode.rawMode()) == 0) 74 return windows.unexpectedError(windows.kernel32.GetLastError()); 75 76 if (SetConsoleMode(stdout, OutputMode.rawMode()) == 0) 77 return windows.unexpectedError(windows.kernel32.GetLastError()); 78 79 if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0) 80 return windows.unexpectedError(windows.kernel32.GetLastError()); 81 } 82 83 const self: Tty = .{ 84 .stdin = stdin, 85 .stdout = stdout, 86 .initial_codepage = initial_output_codepage, 87 .initial_input_mode = initial_input_mode, 88 .initial_output_mode = initial_output_mode, 89 }; 90 91 // save a copy of this tty as the global_tty for panic handling 92 global_tty = self; 93 94 return self; 95} 96 97pub fn deinit(self: Tty) void { 98 _ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage); 99 _ = SetConsoleMode(self.stdin, self.initial_input_mode); 100 _ = SetConsoleMode(self.stdout, self.initial_output_mode); 101 windows.CloseHandle(self.stdin); 102 windows.CloseHandle(self.stdout); 103} 104 105pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { 106 const self: *const Tty = @ptrCast(@alignCast(ptr)); 107 return windows.WriteFile(self.stdout, bytes, null); 108} 109 110pub fn anyWriter(self: *const Tty) std.io.AnyWriter { 111 return .{ 112 .context = self, 113 .writeFn = Tty.opaqueWrite, 114 }; 115} 116 117pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWriter) { 118 return std.io.bufferedWriter(self.anyWriter()); 119} 120 121pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event { 122 // We use a loop so we can ignore certain events 123 var state: EventState = .{}; 124 while (true) { 125 var event_count: u32 = 0; 126 var input_record: INPUT_RECORD = undefined; 127 if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0) 128 return windows.unexpectedError(windows.kernel32.GetLastError()); 129 130 if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| { 131 return ev; 132 } 133 } 134} 135 136pub const EventState = struct { 137 ansi_buf: [128]u8 = undefined, 138 ansi_idx: usize = 0, 139 utf16_buf: [2]u16 = undefined, 140 utf16_half: bool = false, 141}; 142 143pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event { 144 switch (record.EventType) { 145 0x0001 => { // Key event 146 const event = record.Event.KeyEvent; 147 148 if (state.utf16_half) half: { 149 state.utf16_half = false; 150 state.utf16_buf[1] = event.uChar.UnicodeChar; 151 const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half; 152 const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null; 153 154 const key: Key = .{ 155 .codepoint = codepoint, 156 .base_layout_codepoint = codepoint, 157 .mods = translateMods(event.dwControlKeyState), 158 .text = self.buf[0..n], 159 }; 160 161 switch (event.bKeyDown) { 162 0 => return .{ .key_release = key }, 163 else => return .{ .key_press = key }, 164 } 165 } 166 167 const base_layout: u16 = switch (event.wVirtualKeyCode) { 168 0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint 169 if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27) 170 break :blk event.uChar.UnicodeChar; 171 state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar; 172 state.ansi_idx += 1; 173 if (state.ansi_idx <= 2) return null; 174 const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator); 175 return if (result.n == 0) null else evt: { 176 state.ansi_idx = 0; 177 break :evt result.event; 178 }; 179 }, 180 0x08 => Key.backspace, 181 0x09 => Key.tab, 182 0x0D => Key.enter, 183 0x13 => Key.pause, 184 0x14 => Key.caps_lock, 185 0x1B => Key.escape, 186 0x20 => Key.space, 187 0x21 => Key.page_up, 188 0x22 => Key.page_down, 189 0x23 => Key.end, 190 0x24 => Key.home, 191 0x25 => Key.left, 192 0x26 => Key.up, 193 0x27 => Key.right, 194 0x28 => Key.down, 195 0x2c => Key.print_screen, 196 0x2d => Key.insert, 197 0x2e => Key.delete, 198 0x30...0x39 => |k| k, 199 0x41...0x5a => |k| k + 0x20, // translate to lowercase 200 0x5b => Key.left_meta, 201 0x5c => Key.right_meta, 202 0x60 => Key.kp_0, 203 0x61 => Key.kp_1, 204 0x62 => Key.kp_2, 205 0x63 => Key.kp_3, 206 0x64 => Key.kp_4, 207 0x65 => Key.kp_5, 208 0x66 => Key.kp_6, 209 0x67 => Key.kp_7, 210 0x68 => Key.kp_8, 211 0x69 => Key.kp_9, 212 0x6a => Key.kp_multiply, 213 0x6b => Key.kp_add, 214 0x6c => Key.kp_separator, 215 0x6d => Key.kp_subtract, 216 0x6e => Key.kp_decimal, 217 0x6f => Key.kp_divide, 218 0x70 => Key.f1, 219 0x71 => Key.f2, 220 0x72 => Key.f3, 221 0x73 => Key.f4, 222 0x74 => Key.f5, 223 0x75 => Key.f6, 224 0x76 => Key.f8, 225 0x77 => Key.f8, 226 0x78 => Key.f9, 227 0x79 => Key.f10, 228 0x7a => Key.f11, 229 0x7b => Key.f12, 230 0x7c => Key.f13, 231 0x7d => Key.f14, 232 0x7e => Key.f15, 233 0x7f => Key.f16, 234 0x80 => Key.f17, 235 0x81 => Key.f18, 236 0x82 => Key.f19, 237 0x83 => Key.f20, 238 0x84 => Key.f21, 239 0x85 => Key.f22, 240 0x86 => Key.f23, 241 0x87 => Key.f24, 242 0x90 => Key.num_lock, 243 0x91 => Key.scroll_lock, 244 0xa0 => Key.left_shift, 245 0xa1 => Key.right_shift, 246 0xa2 => Key.left_control, 247 0xa3 => Key.right_control, 248 0xa4 => Key.left_alt, 249 0xa5 => Key.right_alt, 250 0xad => Key.mute_volume, 251 0xae => Key.lower_volume, 252 0xaf => Key.raise_volume, 253 0xb0 => Key.media_track_next, 254 0xb1 => Key.media_track_previous, 255 0xb2 => Key.media_stop, 256 0xb3 => Key.media_play_pause, 257 0xba => ';', 258 0xbb => '+', 259 0xbc => ',', 260 0xbd => '-', 261 0xbe => '.', 262 0xbf => '/', 263 0xc0 => '`', 264 0xdb => '[', 265 0xdc => '\\', 266 0xdd => ']', 267 0xde => '\'', 268 else => return null, 269 }; 270 271 if (std.unicode.utf16IsHighSurrogate(base_layout)) { 272 state.utf16_buf[0] = base_layout; 273 state.utf16_half = true; 274 return null; 275 } 276 if (std.unicode.utf16IsLowSurrogate(base_layout)) { 277 return null; 278 } 279 280 var codepoint: u21 = base_layout; 281 var text: ?[]const u8 = null; 282 switch (event.uChar.UnicodeChar) { 283 0x00...0x1F => {}, 284 else => |cp| { 285 codepoint = cp; 286 const n = try std.unicode.utf8Encode(codepoint, &self.buf); 287 text = self.buf[0..n]; 288 }, 289 } 290 291 const key: Key = .{ 292 .codepoint = codepoint, 293 .base_layout_codepoint = base_layout, 294 .mods = translateMods(event.dwControlKeyState), 295 .text = text, 296 }; 297 298 switch (event.bKeyDown) { 299 0 => return .{ .key_release = key }, 300 else => return .{ .key_press = key }, 301 } 302 }, 303 0x0002 => { // Mouse event 304 // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str 305 306 const event = record.Event.MouseEvent; 307 308 // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative 309 // is wheel_down 310 // Low word represents button state 311 const mouse_wheel_direction: i16 = blk: { 312 const wheelu32: u32 = event.dwButtonState >> 16; 313 const wheelu16: u16 = @truncate(wheelu32); 314 break :blk @bitCast(wheelu16); 315 }; 316 317 const buttons: u16 = @truncate(event.dwButtonState); 318 // save the current state when we are done 319 defer self.last_mouse_button_press = buttons; 320 const button_xor = self.last_mouse_button_press ^ buttons; 321 322 var event_type: Mouse.Type = .press; 323 const btn: Mouse.Button = switch (button_xor) { 324 0x0000 => blk: { 325 // Check wheel event 326 if (event.dwEventFlags & 0x0004 > 0) { 327 if (mouse_wheel_direction > 0) 328 break :blk .wheel_up 329 else 330 break :blk .wheel_down; 331 } 332 333 // If we have no change but one of the buttons is still pressed we have a 334 // drag event. Find out which button is held down 335 if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) { 336 event_type = .drag; 337 if (buttons & 0x0001 > 0) break :blk .left; 338 if (buttons & 0x0002 > 0) break :blk .right; 339 if (buttons & 0x0004 > 0) break :blk .middle; 340 if (buttons & 0x0008 > 0) break :blk .button_8; 341 if (buttons & 0x0010 > 0) break :blk .button_9; 342 } 343 344 if (event.dwEventFlags & 0x0001 > 0) event_type = .motion; 345 break :blk .none; 346 }, 347 0x0001 => blk: { 348 if (buttons & 0x0001 == 0) event_type = .release; 349 break :blk .left; 350 }, 351 0x0002 => blk: { 352 if (buttons & 0x0002 == 0) event_type = .release; 353 break :blk .right; 354 }, 355 0x0004 => blk: { 356 if (buttons & 0x0004 == 0) event_type = .release; 357 break :blk .middle; 358 }, 359 0x0008 => blk: { 360 if (buttons & 0x0008 == 0) event_type = .release; 361 break :blk .button_8; 362 }, 363 0x0010 => blk: { 364 if (buttons & 0x0010 == 0) event_type = .release; 365 break :blk .button_9; 366 }, 367 else => { 368 std.log.warn("unknown mouse event: {}", .{event}); 369 return null; 370 }, 371 }; 372 373 const shift: u32 = 0x0010; 374 const alt: u32 = 0x0001 | 0x0002; 375 const ctrl: u32 = 0x0004 | 0x0008; 376 const mods: Mouse.Modifiers = .{ 377 .shift = event.dwControlKeyState & shift > 0, 378 .alt = event.dwControlKeyState & alt > 0, 379 .ctrl = event.dwControlKeyState & ctrl > 0, 380 }; 381 382 const mouse: Mouse = .{ 383 .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 384 .row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index 385 .mods = mods, 386 .type = event_type, 387 .button = btn, 388 }; 389 return .{ .mouse = mouse }; 390 }, 391 0x0004 => { // Screen resize events 392 // NOTE: Even though the event comes with a size, it may not be accurate. We ask for 393 // the size directly when we get this event 394 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; 395 if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) { 396 return windows.unexpectedError(windows.kernel32.GetLastError()); 397 } 398 const window_rect = console_info.srWindow; 399 const width = window_rect.Right - window_rect.Left + 1; 400 const height = window_rect.Bottom - window_rect.Top + 1; 401 return .{ 402 .winsize = .{ 403 .cols = @intCast(width), 404 .rows = @intCast(height), 405 .x_pixel = 0, 406 .y_pixel = 0, 407 }, 408 }; 409 }, 410 0x0010 => { // Focus events 411 switch (record.Event.FocusEvent.bSetFocus) { 412 0 => return .focus_out, 413 else => return .focus_in, 414 } 415 }, 416 else => {}, 417 } 418 return null; 419} 420 421fn translateMods(mods: u32) Key.Modifiers { 422 const left_alt: u32 = 0x0002; 423 const right_alt: u32 = 0x0001; 424 const left_ctrl: u32 = 0x0008; 425 const right_ctrl: u32 = 0x0004; 426 427 const caps: u32 = 0x0080; 428 const num_lock: u32 = 0x0020; 429 const shift: u32 = 0x0010; 430 const alt: u32 = left_alt | right_alt; 431 const ctrl: u32 = left_ctrl | right_ctrl; 432 433 return .{ 434 .shift = mods & shift > 0, 435 .alt = mods & alt > 0, 436 .ctrl = mods & ctrl > 0, 437 .caps_lock = mods & caps > 0, 438 .num_lock = mods & num_lock > 0, 439 }; 440} 441 442// From gitub.com/ziglibs/zig-windows-console. Thanks :) 443// 444// Events 445const union_unnamed_248 = extern union { 446 UnicodeChar: windows.WCHAR, 447 AsciiChar: windows.CHAR, 448}; 449pub const KEY_EVENT_RECORD = extern struct { 450 bKeyDown: windows.BOOL, 451 wRepeatCount: windows.WORD, 452 wVirtualKeyCode: windows.WORD, 453 wVirtualScanCode: windows.WORD, 454 uChar: union_unnamed_248, 455 dwControlKeyState: windows.DWORD, 456}; 457pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD; 458 459pub const MOUSE_EVENT_RECORD = extern struct { 460 dwMousePosition: windows.COORD, 461 dwButtonState: windows.DWORD, 462 dwControlKeyState: windows.DWORD, 463 dwEventFlags: windows.DWORD, 464}; 465pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD; 466 467pub const WINDOW_BUFFER_SIZE_RECORD = extern struct { 468 dwSize: windows.COORD, 469}; 470pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD; 471 472pub const MENU_EVENT_RECORD = extern struct { 473 dwCommandId: windows.UINT, 474}; 475pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD; 476 477pub const FOCUS_EVENT_RECORD = extern struct { 478 bSetFocus: windows.BOOL, 479}; 480pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD; 481 482const union_unnamed_249 = extern union { 483 KeyEvent: KEY_EVENT_RECORD, 484 MouseEvent: MOUSE_EVENT_RECORD, 485 WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD, 486 MenuEvent: MENU_EVENT_RECORD, 487 FocusEvent: FOCUS_EVENT_RECORD, 488}; 489pub const INPUT_RECORD = extern struct { 490 EventType: windows.WORD, 491 Event: union_unnamed_249, 492}; 493pub const PINPUT_RECORD = *INPUT_RECORD; 494 495pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL; 496// TODO: remove this in zig 0.13.0 497pub extern "kernel32" fn SetConsoleMode(in_hConsoleHandle: windows.HANDLE, in_dwMode: windows.DWORD) callconv(windows.WINAPI) windows.BOOL;