Something went wrong. Try again.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253const std = @import("std");const builtin = @import("builtin");
const vaxis = @import("main.zig");
const ctlseqs = vaxis.ctlseqs;const posix = std.posix;const windows = std.os.windows;
const Event = vaxis.Event;const Key = vaxis.Key;const Mouse = vaxis.Mouse;const Parser = vaxis.Parser;const Winsize = vaxis.Winsize;
/// The target TTY implementationpub const Tty = if (builtin.is_test) TestTtyelse switch (builtin.os.tag) { .windows => WindowsTty, else => PosixTty,};
/// global tty instance, used in case of a panic. Not guaranteed to work if/// for some reason there are multiple TTYs open under a single vaxis/// compilation unit - but this is better than nothingpub var global_tty: ?Tty = null;
pub const PosixTty = struct { io: std.Io,
/// the original state of the terminal, prior to calling makeRaw termios: posix.termios,
/// The file descriptor of the tty fd: std.Io.File,
/// File.Writer for efficient buffered writing tty_writer: std.Io.File.Writer,
pub const SignalHandler = struct { context: *anyopaque, callback: *const fn (context: *anyopaque) void, };
/// global signal handlers var handlers: [8]SignalHandler = undefined; var handler_io: std.Io = undefined; var handler_mutex: std.Io.Mutex = .init; var handler_idx: usize = 0;
var signal_pipe: [2]std.Io.File = undefined; var signal_thread: std.Io.Future(void) = undefined; var signal_thread_running = false; var signal_thread_quit: std.atomic.Value(bool) = .init(false);
var handler_installed: bool = false;
/// initializes a Tty instance by opening /dev/tty and "making it raw". A /// signal handler is installed for SIGWINCH. No callbacks are installed, be /// sure to register a callback when initializing the event loop pub fn init(io: std.Io, buffer: []u8) !PosixTty { // Open our tty var f = try std.Io.Dir.openFileAbsolute(io, "/dev/tty", .{ .mode = .read_write });
// Set the termios of the tty const termios = try makeRaw(f.handle); errdefer { posix.tcsetattr(f.handle, .FLUSH, termios) catch {}; f.close(io); }
if (!handler_installed) { try startSignalThread(io); handler_io = io; installSignalHandler(); }
const self: PosixTty = .{ .io = io, .fd = f, .termios = termios, .tty_writer = f.writerStreaming(io, buffer), };
global_tty = self;
return self; }
/// release resources associated with the Tty return it to its original state pub fn deinit(self: PosixTty) void { resetSignalHandler(); stopSignalThread(); posix.tcsetattr(self.fd.handle, .FLUSH, self.termios) catch |err| { std.log.err("couldn't restore terminal: {}", .{err}); }; if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos self.fd.close(self.io); }
/// Resets the signal handler to it's default pub fn resetSignalHandler() void { if (!handler_installed) { handler_idx = 0; return; }
handler_mutex.lock(handler_io) catch @panic("unable to lock SIGWINCH handlers"); defer handler_mutex.unlock(handler_io); handler_idx = 0; resetSignalHandlerLocked(); }
fn installSignalHandler() void { var act = posix.Sigaction{ .handler = .{ .handler = PosixTty.handleWinch }, .mask = switch (builtin.os.tag) { .macos => 0, else => posix.sigemptyset(), }, .flags = 0, }; posix.sigaction(posix.SIG.WINCH, &act, null); handler_installed = true; }
fn resetSignalHandlerLocked() void { if (!handler_installed) return; handler_installed = false; var act = posix.Sigaction{ .handler = .{ .handler = posix.SIG.DFL }, .mask = switch (builtin.os.tag) { .macos => 0, else => posix.sigemptyset(), }, .flags = 0, }; posix.sigaction(posix.SIG.WINCH, &act, null); }
pub fn writer(self: *PosixTty) *std.Io.Writer { return &self.tty_writer.interface; }
pub fn read(self: *const PosixTty, buf: []u8) !usize { return try self.fd.readStreaming(self.io, &.{buf}); }
/// Install a signal handler for winsize. A maximum of 8 handlers may be /// installed pub fn notifyWinsize(handler: SignalHandler) !void { try handler_mutex.lock(handler_io); defer handler_mutex.unlock(handler_io); if (handler_idx == handlers.len) return error.OutOfMemory; // Removing the last subscriber restores SIG_DFL, but leaves the // dispatch thread alive until deinit. Re-arm it for a new subscriber. if (!handler_installed) installSignalHandler(); handlers[handler_idx] = handler; handler_idx += 1; }
/// Remove a previously installed winsize signal handler pub fn removeWinsize(handler: SignalHandler) void { handler_mutex.lock(handler_io) catch @panic("unable to lock SIGWINCH handlers"); defer handler_mutex.unlock(handler_io);
var i: usize = 0; while (i < handler_idx) : (i += 1) { if (handlers[i].context == handler.context and handlers[i].callback == handler.callback) { handler_idx -= 1; if (i < handler_idx) { std.mem.copyForwards(SignalHandler, handlers[i..handler_idx], handlers[i + 1 .. handler_idx + 1]); } handlers[handler_idx] = undefined; if (handler_idx == 0) resetSignalHandlerLocked(); return; } } }
fn handleWinch(_: std.posix.SIG) callconv(.c) void { // write(2) is async-signal-safe. All other work is deferred to a // normal thread, where taking locks and posting events is safe. const byte = [1]u8{0}; _ = posix.system.write(signal_pipe[1].handle, &byte, byte.len); }
fn startSignalThread(io: std.Io) !void { if (signal_thread_running) return;
var fds: [2]posix.fd_t = undefined; switch (posix.errno(posix.system.pipe(&fds))) { .SUCCESS => {}, .NFILE => return error.SystemFdQuotaExceeded, .MFILE => return error.ProcessFdQuotaExceeded, else => |err| return posix.unexpectedErrno(err), } errdefer { _ = posix.system.close(fds[0]); _ = posix.system.close(fds[1]); }
for (fds) |fd| { switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(u32, posix.FD_CLOEXEC)))) { .SUCCESS => {}, else => |err| return posix.unexpectedErrno(err), } }
const nonblocking: u32 = @bitCast(posix.O{ .NONBLOCK = true }); switch (posix.errno(posix.system.fcntl(fds[1], posix.F.SETFL, nonblocking))) { .SUCCESS => {}, else => |err| return posix.unexpectedErrno(err), }
signal_pipe = .{ .{ .handle = fds[0], .flags = .{ .nonblocking = false } }, .{ .handle = fds[1], .flags = .{ .nonblocking = true } }, }; signal_thread_quit.store(false, .release); signal_thread = try io.concurrent(runSignalThread, .{io}); signal_thread_running = true; }
fn stopSignalThread() void { if (!signal_thread_running) return; signal_thread_quit.store(true, .release); const byte = [1]u8{0}; _ = posix.system.write(signal_pipe[1].handle, &byte, byte.len); signal_thread.await(handler_io); signal_pipe[0].close(handler_io); signal_pipe[1].close(handler_io); signal_thread_running = false; }
fn runSignalThread(io: std.Io) void { var buf: [64]u8 = undefined; while (true) { _ = signal_pipe[0].readStreaming(io, &.{&buf}) catch return; if (signal_thread_quit.load(.acquire)) return;
{ handler_mutex.lock(io) catch return; defer handler_mutex.unlock(io); var i: usize = 0; while (i < handler_idx) : (i += 1) { const handler = handlers[i]; handler.callback(handler.context); } } } }
/// makeRaw enters the raw state for the terminal. pub fn makeRaw(fd: posix.fd_t) !posix.termios { const state = try posix.tcgetattr(fd); var raw = state; // see termios(3) raw.iflag.IGNBRK = false; raw.iflag.BRKINT = false; raw.iflag.PARMRK = false; raw.iflag.ISTRIP = false; raw.iflag.INLCR = false; raw.iflag.IGNCR = false; raw.iflag.ICRNL = false; raw.iflag.IXON = false;
raw.oflag.OPOST = false;
raw.lflag.ECHO = false; raw.lflag.ECHONL = false; raw.lflag.ICANON = false; raw.lflag.ISIG = false; raw.lflag.IEXTEN = false;
raw.cflag.CSIZE = .CS8; raw.cflag.PARENB = false;
raw.cc[@backingInt(posix.V.MIN)] = 1; raw.cc[@backingInt(posix.V.TIME)] = 0; try posix.tcsetattr(fd, .FLUSH, raw); return state; }
/// Get the window size from the kernel pub fn getWinsize(self: *PosixTty) !Winsize { var winsize = posix.winsize{ .row = 0, .col = 0, .xpixel = 0, .ypixel = 0, };
const err = posix.system.ioctl(self.fd.handle, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); if (posix.errno(err) == .SUCCESS) return Winsize{ .rows = winsize.row, .cols = winsize.col, .x_pixel = winsize.xpixel, .y_pixel = winsize.ypixel, }; return error.IoctlError; }};
pub const WindowsTty = struct { stdin: windows.HANDLE, stdout: windows.HANDLE, input_stop: windows.HANDLE,
initial_codepage: c_uint, initial_input_mode: CONSOLE_MODE_INPUT, initial_output_mode: CONSOLE_MODE_OUTPUT,
// a buffer to write key text into buf: [4]u8 = undefined,
event_state: EventState = .{},
/// File.Writer for efficient buffered writing tty_writer: std.Io.File.Writer,
/// The last mouse button that was pressed. We store the previous state of button presses on each /// mouse event so we can detect which button was released last_mouse_button_press: u16 = 0,
const utf8_codepage: c_uint = 65001;
pub const SignalHandler = struct { context: *anyopaque, callback: *const fn (context: *anyopaque) void, };
/// The input mode set by init pub const input_raw_mode: CONSOLE_MODE_INPUT = .{ .WINDOW_INPUT = 1, // resize events .MOUSE_INPUT = 1, .EXTENDED_FLAGS = 1, // allow mouse events .VIRTUAL_TERMINAL_INPUT = 1, // preserve bracketed paste delimiters };
/// The output mode set by init pub const output_raw_mode: CONSOLE_MODE_OUTPUT = .{ .PROCESSED_OUTPUT = 1, // handle control sequences .VIRTUAL_TERMINAL_PROCESSING = 1, // handle ANSI sequences .DISABLE_NEWLINE_AUTO_RETURN = 1, // disable inserting a new line when we write at the last column .ENABLE_LVB_GRID_WORLDWIDE = 1, // enables reverse video and underline };
pub fn init(io: std.Io, buffer: []u8) !WindowsTty { const stdin: std.Io.File = .stdin(); const stdout: std.Io.File = .stdout(); const input_stop = CreateEventW(null, .TRUE, .FALSE, null) orelse return windows.unexpectedError(windows.GetLastError()); errdefer windows.CloseHandle(input_stop);
// get initial modes const initial_output_codepage = GetConsoleOutputCP(); const initial_input_mode = try getConsoleMode(CONSOLE_MODE_INPUT, stdin.handle); const initial_output_mode = try getConsoleMode(CONSOLE_MODE_OUTPUT, stdout.handle);
// set new modes try setConsoleMode(stdin.handle, input_raw_mode); errdefer setConsoleMode(stdin.handle, initial_input_mode) catch {}; try setConsoleMode(stdout.handle, output_raw_mode); errdefer setConsoleMode(stdout.handle, initial_output_mode) catch {}; if (SetConsoleOutputCP(utf8_codepage) == .FALSE) return windows.unexpectedError(windows.GetLastError()); errdefer _ = SetConsoleOutputCP(initial_output_codepage);
var self: WindowsTty = .{ .stdin = stdin.handle, .stdout = stdout.handle, .input_stop = input_stop, .initial_codepage = initial_output_codepage, .initial_input_mode = initial_input_mode, .initial_output_mode = initial_output_mode, .tty_writer = .initStreaming(stdout, io, buffer), };
// VT input alone loses key releases and modifier keys. Win32-input-mode // carries the original KEY_EVENT_RECORD fields in CSI ... _ sequences. try self.writer().writeAll("\x1b[?9001h"); try self.writer().flush();
// save a copy of this tty as the global_tty for panic handling global_tty = self;
return self; }
pub fn deinit(self: WindowsTty) void { var output: std.Io.File.Writer = .initStreaming(self.tty_writer.file, self.tty_writer.io, &.{}); output.interface.writeAll("\x1b[?9001l") catch {}; _ = SetConsoleOutputCP(self.initial_codepage); setConsoleMode(self.stdin, self.initial_input_mode) catch {}; setConsoleMode(self.stdout, self.initial_output_mode) catch {}; windows.CloseHandle(self.input_stop); windows.CloseHandle(self.stdin); windows.CloseHandle(self.stdout); }
pub const CONSOLE_MODE_INPUT = packed struct(u32) { PROCESSED_INPUT: u1 = 0, LINE_INPUT: u1 = 0, ECHO_INPUT: u1 = 0, WINDOW_INPUT: u1 = 0, MOUSE_INPUT: u1 = 0, INSERT_MODE: u1 = 0, QUICK_EDIT_MODE: u1 = 0, EXTENDED_FLAGS: u1 = 0, AUTO_POSITION: u1 = 0, VIRTUAL_TERMINAL_INPUT: u1 = 0, _: u22 = 0, };
pub const CONSOLE_MODE_OUTPUT = packed struct(u32) { PROCESSED_OUTPUT: u1 = 0, WRAP_AT_EOL_OUTPUT: u1 = 0, VIRTUAL_TERMINAL_PROCESSING: u1 = 0, DISABLE_NEWLINE_AUTO_RETURN: u1 = 0, ENABLE_LVB_GRID_WORLDWIDE: u1 = 0, _: u27 = 0, };
pub fn getConsoleMode(comptime T: type, handle: windows.HANDLE) !T { var mode: u32 = undefined; if (GetConsoleMode(handle, &mode) == .FALSE) return switch (windows.GetLastError()) { .INVALID_HANDLE => error.InvalidHandle, else => |e| windows.unexpectedError(e), }; return @bitCast(mode); }
pub fn setConsoleMode(handle: windows.HANDLE, mode: anytype) !void { if (SetConsoleMode(handle, @bitCast(mode)) == .FALSE) return switch (windows.GetLastError()) { .INVALID_HANDLE => error.InvalidHandle, else => |e| windows.unexpectedError(e), }; }
pub fn writer(self: *WindowsTty) *std.Io.Writer { return &self.tty_writer.interface; }
pub fn read(self: *const WindowsTty, buf: []u8) !usize { return posix.read(self.fd, buf); }
pub fn resetInput(self: *WindowsTty) !void { if (ResetEvent(self.input_stop) == .FALSE) return windows.unexpectedError(windows.GetLastError()); self.event_state = .{}; }
pub fn interruptInput(self: *WindowsTty) void { // The handle is owned by this Tty and remains open until deinit. if (SetEvent(self.input_stop) == .FALSE) @panic("invalid console stop event"); }
fn inputError(code: windows.Win32Error) anyerror { return switch (code) { .INVALID_HANDLE => error.InvalidHandle, .ACCESS_DENIED => error.AccessDenied, .OPERATION_ABORTED => error.InputInterrupted, .NOT_READY, .BUSY, .RETRY => error.WouldBlock, .NOT_ENOUGH_MEMORY, .NO_SYSTEM_RESOURCES => error.SystemResources, else => { std.log.scoped(.vaxis).warn("console input failed: Win32 error {d}", .{@backingInt(code)}); return error.Unexpected; }, }; }
pub fn nextEvent(self: *WindowsTty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event { // Keep partial ANSI and UTF-16 input across transient console read errors. while (true) { // A console handle is signaled while input is available. Put the stop // event first so shutdown wins even during a continuous input stream. // This Tty must be the sole reader of the console input buffer. const handles = [_]windows.HANDLE{ self.input_stop, self.stdin }; switch (WaitForMultipleObjects(handles.len, &handles, .FALSE, 0xffffffff)) { 0 => return error.Canceled, 1 => {}, else => return inputError(windows.GetLastError()), } var event_count: u32 = 0; var input_record: INPUT_RECORD = undefined; if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == .FALSE) return inputError(windows.GetLastError());
const event = self.eventFromRecord(&input_record, &self.event_state, parser, paste_allocator) catch |err| { self.event_state = .{}; return err; }; if (event) |ev| { return ev; } } }
pub const EventState = struct { // Outer VT transport, including win32-input-mode keyboard records. vt_buf: [128]u8 = undefined, vt_idx: usize = 0, // Some console versions also wrap pasted VT bytes in win32 records. ansi_buf: [128]u8 = undefined, ansi_idx: usize = 0, ansi_key_up: ?u16 = null, utf16_buf: [2]u16 = undefined, utf16_half: bool = false, };
// https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md fn parseWin32Input(sequence: []const u8) ?KEY_EVENT_RECORD { var params: [6]u32 = .{ 0, 0, 0, 0, 0, 1 }; var fields = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';'); var i: usize = 0; while (fields.next()) |field| : (i += 1) { if (i == params.len) return null; if (field.len > 0) params[i] = std.fmt.parseInt(u32, field, 10) catch return null; } return .{ .wVirtualKeyCode = std.math.cast(u16, params[0]) orelse return null, .wVirtualScanCode = std.math.cast(u16, params[1]) orelse return null, .uChar = .{ .UnicodeChar = std.math.cast(u16, params[2]) orelse return null }, .bKeyDown = switch (params[3]) { 0 => .FALSE, 1 => .TRUE, else => return null, }, .dwControlKeyState = params[4], .wRepeatCount = std.math.cast(u16, params[5]) orelse return null, }; }
pub const SMALL_RECT = extern struct { Left: windows.SHORT, Top: windows.SHORT, Right: windows.SHORT, Bottom: windows.SHORT, };
pub const COORD = extern struct { X: windows.SHORT, Y: windows.SHORT, };
pub const CONSOLE_SCREEN_BUFFER_INFO = extern struct { dwSize: COORD, dwCursorPosition: COORD, wAttributes: windows.WORD, srWindow: SMALL_RECT, dwMaximumWindowSize: COORD, };
pub fn eventFromRecord(self: *WindowsTty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event { switch (record.EventType) { 0x0001 => { // Key event var event = record.Event.KeyEvent;
if (event.wVirtualKeyCode == 0) { if (event.bKeyDown == .FALSE) return null; if (state.vt_idx > 0 or event.uChar.UnicodeChar == 27) { if (state.vt_idx == state.vt_buf.len or event.uChar.UnicodeChar > 127) { state.vt_idx = 0; return null; } state.vt_buf[state.vt_idx] = @intCast(event.uChar.UnicodeChar); state.vt_idx += 1; if (state.vt_idx <= 2) return null; const sequence = state.vt_buf[0..state.vt_idx]; if (std.mem.startsWith(u8, sequence, "\x1b[") and sequence[sequence.len - 1] == '_') { state.vt_idx = 0; event = parseWin32Input(sequence) orelse return null; } else { const result = try parser.parse(sequence, paste_allocator); if (result.n > 0) state.vt_idx = 0; return result.event; } } }
// Encoded paste records may contain key-up duplicates. Only // key-down text belongs to the inner escape-sequence stream. if (event.wVirtualKeyCode == 0 and event.bKeyDown == .FALSE and state.ansi_key_up == event.uChar.UnicodeChar) { state.ansi_key_up = null; return null; } state.ansi_key_up = null;
if (state.utf16_half) half: { state.utf16_half = false; state.utf16_buf[1] = event.uChar.UnicodeChar; const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half; const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null;
const key: Key = .{ .codepoint = codepoint, .base_layout_codepoint = codepoint, .mods = translateMods(event.dwControlKeyState), .text = self.buf[0..n], };
switch (event.bKeyDown) { .FALSE => return .{ .key_release = key }, else => return .{ .key_press = key }, } }
const base_layout: u16 = switch (event.wVirtualKeyCode) { 0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint if (state.ansi_idx == 0 and event.uChar.UnicodeChar != 27) break :blk event.uChar.UnicodeChar; if (state.ansi_idx == state.ansi_buf.len or event.uChar.UnicodeChar > 127) { state.ansi_idx = 0; return null; } state.ansi_key_up = event.uChar.UnicodeChar; state.ansi_buf[state.ansi_idx] = @intCast(event.uChar.UnicodeChar); state.ansi_idx += 1; if (state.ansi_idx <= 2) return null; const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator); return if (result.n == 0) null else evt: { state.ansi_idx = 0; break :evt result.event; }; }, 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.f7, 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, 0x10 => Key.left_shift, 0xa1 => Key.right_shift, 0xa2 => Key.left_control, 0x11 => Key.left_control, 0xa3 => Key.right_control, 0xa4 => Key.left_alt, 0x12 => 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 => '\\', 0xdf => '\\', 0xe2 => '\\', 0xdd => ']', 0xde => '\'', else => { const log = std.log.scoped(.vaxis); log.warn("unknown wVirtualKeyCode: 0x{x}", .{event.wVirtualKeyCode}); return null; }, };
if (std.unicode.utf16IsHighSurrogate(base_layout)) { state.utf16_buf[0] = base_layout; state.utf16_half = true; return null; } if (std.unicode.utf16IsLowSurrogate(base_layout)) { 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(codepoint, &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) { .FALSE => 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(i16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index .row = @as(i16, @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: CONSOLE_SCREEN_BUFFER_INFO = undefined; if (GetConsoleScreenBufferInfo(self.stdout, &console_info) == .FALSE) { return inputError(windows.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) { .FALSE => 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; const left_ctrl: u32 = 0x0008; const right_ctrl: u32 = 0x0004;
const caps: u32 = 0x0080; const num_lock: u32 = 0x0020; const shift: u32 = 0x0010; const alt: u32 = left_alt | right_alt; const ctrl: u32 = left_ctrl | right_ctrl;
const altGr = (mods & right_alt > 0) and (mods & left_ctrl > 0);
return .{ .shift = mods & shift > 0, .alt = if (altGr) mods & left_alt > 0 else mods & alt > 0, .ctrl = if (altGr) mods & right_ctrl > 0 else mods & ctrl > 0, .caps_lock = mods & caps > 0, .num_lock = mods & num_lock > 0, }; }
pub fn getWinsize(self: *WindowsTty) !Winsize { var console_info: CONSOLE_SCREEN_BUFFER_INFO = undefined; if (GetConsoleScreenBufferInfo(self.stdout, &console_info) == .FALSE) { return windows.unexpectedError(windows.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 .{ .cols = @intCast(width), .rows = @intCast(height), .x_pixel = 0, .y_pixel = 0, }; }
// From gitub.com/ziglibs/zig-windows-console. Thanks :) // // Events const union_unnamed_248 = extern union { UnicodeChar: windows.WCHAR, AsciiChar: windows.CHAR, };
pub const KEY_EVENT_RECORD = extern struct { bKeyDown: windows.BOOL, wRepeatCount: windows.WORD, wVirtualKeyCode: windows.WORD, wVirtualScanCode: windows.WORD, uChar: union_unnamed_248, dwControlKeyState: windows.DWORD, };
pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD;
pub const MOUSE_EVENT_RECORD = extern struct { dwMousePosition: windows.COORD, dwButtonState: windows.DWORD, dwControlKeyState: windows.DWORD, dwEventFlags: windows.DWORD, };
pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD;
pub const WINDOW_BUFFER_SIZE_RECORD = extern struct { dwSize: windows.COORD, };
pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD;
pub const MENU_EVENT_RECORD = extern struct { dwCommandId: windows.UINT, };
pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD;
pub const FOCUS_EVENT_RECORD = extern struct { bSetFocus: windows.BOOL, };
pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD;
const union_unnamed_249 = extern union { KeyEvent: KEY_EVENT_RECORD, MouseEvent: MOUSE_EVENT_RECORD, WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD, MenuEvent: MENU_EVENT_RECORD, FocusEvent: FOCUS_EVENT_RECORD, };
pub const INPUT_RECORD = extern struct { EventType: windows.WORD, Event: union_unnamed_249, };
pub const PINPUT_RECORD = *INPUT_RECORD;
extern "kernel32" fn CreateEventW(?*windows.SECURITY_ATTRIBUTES, windows.BOOL, windows.BOOL, ?windows.LPCWSTR) callconv(.winapi) ?windows.HANDLE; extern "kernel32" fn SetEvent(windows.HANDLE) callconv(.winapi) windows.BOOL; extern "kernel32" fn ResetEvent(windows.HANDLE) callconv(.winapi) windows.BOOL; extern "kernel32" fn WaitForMultipleObjects(windows.DWORD, [*]const windows.HANDLE, windows.BOOL, windows.DWORD) callconv(.winapi) windows.DWORD; pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn GetConsoleOutputCP() callconv(.winapi) windows.UINT; pub extern "kernel32" fn GetConsoleMode(kConsoleHandle: windows.HANDLE, lpMode: *windows.DWORD) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn SetConsoleMode(hConsoleHandle: windows.HANDLE, dwMode: windows.DWORD) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn SetConsoleOutputCP(wCodePageId: windows.UINT) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn GetConsoleScreenBufferInfo(hConsoleOutpur: windows.HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) callconv(.winapi) windows.BOOL;};
pub const TestTty = switch (builtin.os.tag) { .linux => struct { const linux = std.os.linux; /// Used for API compat fd: posix.fd_t, pipe_read: posix.fd_t, pipe_write: posix.fd_t, tty_writer: *std.Io.Writer.Allocating,
pub const SignalHandler = struct { context: *anyopaque, callback: *const fn (context: *anyopaque) void, };
/// Initializes a TestTty. pub fn init(_: std.Io, buffer: []u8) !@This() { _ = buffer;
const list = try std.testing.allocator.create(std.Io.Writer.Allocating); list.* = .init(std.testing.allocator); var fds: [2]i32 = undefined; const rc = linux.pipe(&fds); switch (linux.errno(rc)) { .SUCCESS => {}, else => return error.PipeCreateFailed, } return .{ .fd = fds[0], .pipe_read = fds[0], .pipe_write = fds[1], .tty_writer = list, }; }
pub fn deinit(self: *@This()) void { _ = linux.close(self.pipe_read); _ = linux.close(self.pipe_write); self.tty_writer.deinit(); std.testing.allocator.destroy(self.tty_writer); }
pub fn writer(self: *@This()) *std.Io.Writer { return &self.tty_writer.writer; }
pub fn read(self: *const @This(), buf: []u8) !usize { return posix.read(self.fd, buf); }
/// Get the window size from the kernel pub fn getWinsize(_: *@This()) !Winsize { return .{ .rows = 40, .cols = 80, .x_pixel = 40 * 8, .y_pixel = 40 * 8 * 2, }; }
/// Implemented for the Windows API pub fn nextEvent(_: *@This(), _: *Parser, _: ?std.mem.Allocator) !Event { return error.SkipZigTest; }
pub fn resetSignalHandler() void { return; } }, else => struct { d: std.Io.Writer.Discarding,
pub const SignalHandler = struct { context: *anyopaque, callback: *const fn (context: *anyopaque) void, };
pub fn init(_: std.Io, buf: []u8) !@This() { return .{ .d = .init(buf), }; }
pub fn deinit(_: *const @This()) void {}
pub fn writer(self: *@This()) *std.Io.Writer { return &self.d.writer; }
pub fn getWinsize(_: *@This()) !Winsize { return .{ .rows = 40, .cols = 80, .x_pixel = 40 * 8, .y_pixel = 40 * 8 * 2, }; }
/// Implemented for the Windows API pub fn nextEvent(_: *@This(), _: *Parser, _: ?std.mem.Allocator) !Event { return error.SkipZigTest; }
pub fn resetSignalHandler() void { return; } },};
test "Windows win32-input-mode fields and defaults" { const record = WindowsTty.parseWin32Input("\x1b[65;30;65;1;16;3_").?; try std.testing.expectEqual(@as(u16, 65), record.wVirtualKeyCode); try std.testing.expectEqual(@as(u16, 30), record.wVirtualScanCode); try std.testing.expectEqual(@as(u16, 65), record.uChar.UnicodeChar); try std.testing.expectEqual(windows.BOOL.TRUE, record.bKeyDown); try std.testing.expectEqual(@as(u32, 16), record.dwControlKeyState); try std.testing.expectEqual(@as(u16, 3), record.wRepeatCount); const defaults = WindowsTty.parseWin32Input("\x1b[65;;97;1_").?; try std.testing.expectEqual(@as(u16, 0), defaults.wVirtualScanCode); try std.testing.expectEqual(@as(u32, 0), defaults.dwControlKeyState); try std.testing.expectEqual(@as(u16, 1), defaults.wRepeatCount); for ([_][]const u8{ "\x1b[65536;0;0;1;0;1_", "\x1b[0;0;65536;1;0;1_", "\x1b[0;0;0;2;0;1_", "\x1b[0;0;0;1;4294967296;1_", "\x1b[0;0;0;1;0;65536_", "\x1b[0;0;0;1;0;1;0_", }) |sequence| try std.testing.expect(WindowsTty.parseWin32Input(sequence) == null);}
const WindowsInputTest = struct { tty: WindowsTty = .{ .stdin = undefined, .stdout = undefined, .input_stop = undefined, .initial_codepage = 0, .initial_input_mode = .{}, .initial_output_mode = .{}, .tty_writer = undefined, }, parser: Parser = .{},
fn expectSequence(self: *@This(), input: []const u16, expected: ?Event) !void { for (input, 0..) |unit, i| { const record: WindowsTty.INPUT_RECORD = .{ .EventType = 0x0001, .Event = .{ .KeyEvent = .{ .bKeyDown = .TRUE, .wRepeatCount = 1, .wVirtualKeyCode = 0, .wVirtualScanCode = 0, .uChar = .{ .UnicodeChar = unit }, .dwControlKeyState = 0, } }, }; const event = try self.tty.eventFromRecord(&record, &self.tty.event_state, &self.parser, null); try std.testing.expectEqualDeep(if (i == input.len - 1) expected else null, event); } }};
test "Windows paste boundaries, Unicode, and ordinary keyboard events" { if (builtin.os.tag != .windows) return error.SkipZigTest; const utf16 = std.unicode.utf8ToUtf16LeStringLiteral; var input: WindowsInputTest = .{}; try std.testing.expectEqual(@as(u1, 1), WindowsTty.input_raw_mode.VIRTUAL_TERMINAL_INPUT); try input.expectSequence(utf16("\x1b[200~"), .paste_start); try input.expectSequence(utf16("a"), .{ .key_press = .{ .codepoint = 'a', .base_layout_codepoint = 'a', .text = "a" } }); try input.expectSequence(utf16("\r"), .{ .key_press = .{ .codepoint = Key.enter, .base_layout_codepoint = Key.enter } }); // U+011B has ESC as its low byte; it must not start a control sequence. try input.expectSequence(utf16("ě"), .{ .key_press = .{ .codepoint = 'ě', .base_layout_codepoint = 'ě', .text = "ě" } }); try input.expectSequence(utf16("😀"), .{ .key_press = .{ .codepoint = '😀', .base_layout_codepoint = '😀', .text = "😀" } }); try input.expectSequence(utf16("\r"), .{ .key_press = .{ .codepoint = Key.enter, .base_layout_codepoint = Key.enter } }); try input.expectSequence(utf16("z"), .{ .key_press = .{ .codepoint = 'z', .base_layout_codepoint = 'z', .text = "z" } }); try input.expectSequence(utf16("\x1b[201~"), .paste_end);
// Physical keys arrive as win32-input-mode, including releases and Escape. try input.expectSequence(utf16("\x1b[13;28;13;1;0;1_"), .{ .key_press = .{ .codepoint = Key.enter, .base_layout_codepoint = Key.enter } }); try input.expectSequence(utf16("\x1b[13;28;13;0;0;1_"), .{ .key_release = .{ .codepoint = Key.enter, .base_layout_codepoint = Key.enter } }); try input.expectSequence(utf16("\x1b[27;1;27;1;0;1_"), .{ .key_press = .{ .codepoint = Key.escape, .base_layout_codepoint = Key.escape } }); try input.expectSequence(utf16("\x1b[65;30;65;1;16;1_"), .{ .key_press = .{ .codepoint = 'A', .base_layout_codepoint = 'a', .mods = .{ .shift = true }, .text = "A" } }); try input.expectSequence(utf16("\x1b[65;30;65;0;16;1_"), .{ .key_release = .{ .codepoint = 'A', .base_layout_codepoint = 'a', .mods = .{ .shift = true }, .text = "A" } }); try input.expectSequence(utf16("\x1b[67;46;3;1;8;1_"), .{ .key_press = .{ .codepoint = 'c', .base_layout_codepoint = 'c', .mods = .{ .ctrl = true } } }); try input.expectSequence(utf16("\x1b[81;16;64;1;9;1_"), .{ .key_press = .{ .codepoint = '@', .base_layout_codepoint = 'q', .text = "@" } }); try input.expectSequence(utf16("\x1b[16;42;0;0;0;1_"), .{ .key_release = .{ .codepoint = Key.left_shift, .base_layout_codepoint = Key.left_shift } }); try input.expectSequence(utf16("\x1b[0;0;283;1;0;1_"), .{ .key_press = .{ .codepoint = 'ě', .base_layout_codepoint = 'ě', .text = "ě" } }); try input.expectSequence(utf16("\x1b[0;0;283;0;0;1_"), .{ .key_release = .{ .codepoint = 'ě', .base_layout_codepoint = 'ě', .text = "ě" } });}
test "Windows paste delimiters wrapped in win32-input-mode" { if (builtin.os.tag != .windows) return error.SkipZigTest; var input: WindowsInputTest = .{}; for ([_][]const u8{ "\x1b[200~", "\x1b[201~" }, 0..) |marker, m| { for (marker, 0..) |byte, i| { for ([_]u1{ 1, 0 }) |down| { var buffer: [64]u8 = undefined; const sequence = try std.fmt.bufPrint(&buffer, "\x1b[0;0;{d};{d};0;1_", .{ byte, down }); var units: [64]u16 = undefined; for (sequence, 0..) |b, j| units[j] = b; const expected: ?Event = if (down == 1 and i == marker.len - 1) (if (m == 0) .paste_start else .paste_end) else null; try input.expectSequence(units[0..sequence.len], expected); } } }}
test "console errors distinguish interruption from permanent failures" { try std.testing.expectEqual(error.InputInterrupted, WindowsTty.inputError(.OPERATION_ABORTED)); try std.testing.expectEqual(error.InvalidHandle, WindowsTty.inputError(.INVALID_HANDLE)); try std.testing.expectEqual(error.AccessDenied, WindowsTty.inputError(.ACCESS_DENIED)); try std.testing.expectEqual(error.SystemResources, WindowsTty.inputError(.NO_SYSTEM_RESOURCES));}
test "Windows input wait is interruptible without a console response" { if (builtin.os.tag != .windows) return error.SkipZigTest; const io = std.testing.io; // An unsignaled event stands in for an idle console handle. Cancellation // must return without ever reaching ReadConsoleInputW. const input = WindowsTty.CreateEventW(null, .TRUE, .FALSE, null) orelse return error.Unexpected; defer windows.CloseHandle(input); const stop = WindowsTty.CreateEventW(null, .TRUE, .FALSE, null) orelse return error.Unexpected; defer windows.CloseHandle(stop); var tty: WindowsTty = undefined; tty.stdin = input; tty.input_stop = stop; const Reader = struct { fn run(t: *WindowsTty, ready: *std.Io.Event) !void { var parser: Parser = .{}; ready.set(std.testing.io); try std.testing.expectError(error.Canceled, t.nextEvent(&parser, null)); } }; var ready: std.Io.Event = .unset; var task = try io.concurrent(Reader.run, .{ &tty, &ready }); defer { tty.interruptInput(); task.cancel(io) catch {}; } try ready.wait(io); try io.sleep(.fromMilliseconds(10), .awake); tty.interruptInput(); try task.await(io);
try tty.resetInput(); try std.testing.expectEqual(@as(u32, 258), WindowsTty.WaitForMultipleObjects(1, &.{stop}, .FALSE, 0)); // Shutdown also wins if input and stop are both already signaled. try std.testing.expect(WindowsTty.SetEvent(input) != .FALSE); tty.interruptInput(); var parser: Parser = .{}; try std.testing.expectError(error.Canceled, tty.nextEvent(&parser, null));}
test { std.testing.refAllDecls(@This());}