this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

tty: move tty impls to common file

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