this repo has no description
13
fork

Configure Feed

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

tty(windows): refactor and complete windows tty

Refactor the tty implementations. Complete the windows implementation

+785 -345
+166 -125
src/Loop.zig
··· 6 6 const GraphemeCache = @import("GraphemeCache.zig"); 7 7 const Parser = @import("Parser.zig"); 8 8 const Queue = @import("queue.zig").Queue; 9 - const tty = @import("tty.zig"); 10 - const Tty = tty.Tty; 9 + const Tty = @import("main.zig").Tty; 11 10 const Vaxis = @import("Vaxis.zig"); 12 11 13 12 pub fn Loop(comptime T: type) type { ··· 29 28 /// a stable pointer to register signal callbacks with posix TTYs 30 29 pub fn init(self: *Self) !void { 31 30 switch (builtin.os.tag) { 32 - .windows => @compileError("windows not supported"), 31 + .windows => {}, 33 32 else => { 34 33 const handler: Tty.SignalHandler = .{ 35 34 .context = self, ··· 104 103 grapheme_data: *const grapheme.GraphemeData, 105 104 paste_allocator: ?std.mem.Allocator, 106 105 ) !void { 107 - // get our initial winsize 108 - const winsize = try Tty.getWinsize(self.tty.fd); 109 - if (@hasField(Event, "winsize")) { 110 - self.postEvent(.{ .winsize = winsize }); 111 - } 112 - 113 106 // initialize a grapheme cache 114 107 var cache: GraphemeCache = .{}; 115 108 116 - var parser: Parser = .{ 117 - .grapheme_data = grapheme_data, 118 - }; 119 - 120 - // initialize the read buffer 121 - var buf: [1024]u8 = undefined; 122 - var read_start: usize = 0; 123 - // read loop 124 - while (!self.should_quit) { 125 - const n = try self.tty.read(buf[read_start..]); 126 - var seq_start: usize = 0; 127 - while (seq_start < n) { 128 - const result = try parser.parse(buf[seq_start..n], paste_allocator); 129 - if (result.n == 0) { 130 - // copy the read to the beginning. We don't use memcpy because 131 - // this could be overlapping, and it's also rare 132 - const initial_start = seq_start; 133 - while (seq_start < n) : (seq_start += 1) { 134 - buf[seq_start - initial_start] = buf[seq_start]; 109 + switch (builtin.os.tag) { 110 + .windows => { 111 + while (!self.should_quit) { 112 + const event = try self.tty.nextEvent(); 113 + switch (event) { 114 + .winsize => |ws| { 115 + if (@hasField(Event, "winsize")) { 116 + self.postEvent(.{ .winsize = ws }); 117 + } 118 + }, 119 + .key_press => |key| { 120 + if (@hasField(Event, "key_press")) { 121 + // HACK: yuck. there has to be a better way 122 + var mut_key = key; 123 + if (key.text) |text| { 124 + mut_key.text = cache.put(text); 125 + } 126 + self.postEvent(.{ .key_press = mut_key }); 127 + } 128 + }, 129 + .key_release => |*key| { 130 + if (@hasField(Event, "key_release")) { 131 + // HACK: yuck. there has to be a better way 132 + var mut_key = key; 133 + if (key.text) |text| { 134 + mut_key.text = cache.put(text); 135 + } 136 + self.postEvent(.{ .key_release = mut_key }); 137 + } 138 + }, 139 + .cap_da1 => { 140 + std.Thread.Futex.wake(&self.vaxis.query_futex, 10); 141 + }, 142 + .mouse => {}, // Unsupported currently 143 + else => {}, 135 144 } 136 - read_start = seq_start - initial_start + 1; 137 - continue; 138 145 } 139 - read_start = 0; 140 - seq_start += result.n; 146 + }, 147 + else => { 148 + // get our initial winsize 149 + const winsize = try Tty.getWinsize(self.tty.fd); 150 + if (@hasField(Event, "winsize")) { 151 + self.postEvent(.{ .winsize = winsize }); 152 + } 141 153 142 - const event = result.event orelse continue; 143 - switch (event) { 144 - .key_press => |key| { 145 - if (@hasField(Event, "key_press")) { 146 - // HACK: yuck. there has to be a better way 147 - var mut_key = key; 148 - if (key.text) |text| { 149 - mut_key.text = cache.put(text); 154 + var parser: Parser = .{ 155 + .grapheme_data = grapheme_data, 156 + }; 157 + 158 + // initialize the read buffer 159 + var buf: [1024]u8 = undefined; 160 + var read_start: usize = 0; 161 + // read loop 162 + while (!self.should_quit) { 163 + const n = try self.tty.read(buf[read_start..]); 164 + var seq_start: usize = 0; 165 + while (seq_start < n) { 166 + const result = try parser.parse(buf[seq_start..n], paste_allocator); 167 + if (result.n == 0) { 168 + // copy the read to the beginning. We don't use memcpy because 169 + // this could be overlapping, and it's also rare 170 + const initial_start = seq_start; 171 + while (seq_start < n) : (seq_start += 1) { 172 + buf[seq_start - initial_start] = buf[seq_start]; 150 173 } 151 - self.postEvent(.{ .key_press = mut_key }); 174 + read_start = seq_start - initial_start + 1; 175 + continue; 152 176 } 153 - }, 154 - .key_release => |*key| { 155 - if (@hasField(Event, "key_release")) { 156 - // HACK: yuck. there has to be a better way 157 - var mut_key = key; 158 - if (key.text) |text| { 159 - mut_key.text = cache.put(text); 160 - } 161 - self.postEvent(.{ .key_release = mut_key }); 177 + read_start = 0; 178 + seq_start += result.n; 179 + 180 + const event = result.event orelse continue; 181 + switch (event) { 182 + .key_press => |key| { 183 + if (@hasField(Event, "key_press")) { 184 + // HACK: yuck. there has to be a better way 185 + var mut_key = key; 186 + if (key.text) |text| { 187 + mut_key.text = cache.put(text); 188 + } 189 + self.postEvent(.{ .key_press = mut_key }); 190 + } 191 + }, 192 + .key_release => |*key| { 193 + if (@hasField(Event, "key_release")) { 194 + // HACK: yuck. there has to be a better way 195 + var mut_key = key; 196 + if (key.text) |text| { 197 + mut_key.text = cache.put(text); 198 + } 199 + self.postEvent(.{ .key_release = mut_key }); 200 + } 201 + }, 202 + .mouse => |mouse| { 203 + if (@hasField(Event, "mouse")) { 204 + self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); 205 + } 206 + }, 207 + .focus_in => { 208 + if (@hasField(Event, "focus_in")) { 209 + self.postEvent(.focus_in); 210 + } 211 + }, 212 + .focus_out => { 213 + if (@hasField(Event, "focus_out")) { 214 + self.postEvent(.focus_out); 215 + } 216 + }, 217 + .paste_start => { 218 + if (@hasField(Event, "paste_start")) { 219 + self.postEvent(.paste_start); 220 + } 221 + }, 222 + .paste_end => { 223 + if (@hasField(Event, "paste_end")) { 224 + self.postEvent(.paste_end); 225 + } 226 + }, 227 + .paste => |text| { 228 + if (@hasField(Event, "paste")) { 229 + self.postEvent(.{ .paste = text }); 230 + } else { 231 + if (paste_allocator) |_| 232 + paste_allocator.?.free(text); 233 + } 234 + }, 235 + .color_report => |report| { 236 + if (@hasField(Event, "color_report")) { 237 + self.postEvent(.{ .color_report = report }); 238 + } 239 + }, 240 + .color_scheme => |scheme| { 241 + if (@hasField(Event, "color_scheme")) { 242 + self.postEvent(.{ .color_scheme = scheme }); 243 + } 244 + }, 245 + .cap_kitty_keyboard => { 246 + log.info("kitty keyboard capability detected", .{}); 247 + self.vaxis.caps.kitty_keyboard = true; 248 + }, 249 + .cap_kitty_graphics => { 250 + if (!self.vaxis.caps.kitty_graphics) { 251 + log.info("kitty graphics capability detected", .{}); 252 + self.vaxis.caps.kitty_graphics = true; 253 + } 254 + }, 255 + .cap_rgb => { 256 + log.info("rgb capability detected", .{}); 257 + self.vaxis.caps.rgb = true; 258 + }, 259 + .cap_unicode => { 260 + log.info("unicode capability detected", .{}); 261 + self.vaxis.caps.unicode = .unicode; 262 + self.vaxis.screen.width_method = .unicode; 263 + }, 264 + .cap_sgr_pixels => { 265 + log.info("pixel mouse capability detected", .{}); 266 + self.vaxis.caps.sgr_pixels = true; 267 + }, 268 + .cap_color_scheme_updates => { 269 + log.info("color_scheme_updates capability detected", .{}); 270 + self.vaxis.caps.color_scheme_updates = true; 271 + }, 272 + .cap_da1 => { 273 + std.Thread.Futex.wake(&self.vaxis.query_futex, 10); 274 + }, 275 + .winsize => unreachable, // handled elsewhere for posix 162 276 } 163 - }, 164 - .mouse => |mouse| { 165 - if (@hasField(Event, "mouse")) { 166 - self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); 167 - } 168 - }, 169 - .focus_in => { 170 - if (@hasField(Event, "focus_in")) { 171 - self.postEvent(.focus_in); 172 - } 173 - }, 174 - .focus_out => { 175 - if (@hasField(Event, "focus_out")) { 176 - self.postEvent(.focus_out); 177 - } 178 - }, 179 - .paste_start => { 180 - if (@hasField(Event, "paste_start")) { 181 - self.postEvent(.paste_start); 182 - } 183 - }, 184 - .paste_end => { 185 - if (@hasField(Event, "paste_end")) { 186 - self.postEvent(.paste_end); 187 - } 188 - }, 189 - .paste => |text| { 190 - if (@hasField(Event, "paste")) { 191 - self.postEvent(.{ .paste = text }); 192 - } else { 193 - if (paste_allocator) |_| 194 - paste_allocator.?.free(text); 195 - } 196 - }, 197 - .color_report => |report| { 198 - if (@hasField(Event, "color_report")) { 199 - self.postEvent(.{ .color_report = report }); 200 - } 201 - }, 202 - .color_scheme => |scheme| { 203 - if (@hasField(Event, "color_scheme")) { 204 - self.postEvent(.{ .color_scheme = scheme }); 205 - } 206 - }, 207 - .cap_kitty_keyboard => { 208 - log.info("kitty keyboard capability detected", .{}); 209 - self.vaxis.caps.kitty_keyboard = true; 210 - }, 211 - .cap_kitty_graphics => { 212 - if (!self.vaxis.caps.kitty_graphics) { 213 - log.info("kitty graphics capability detected", .{}); 214 - self.vaxis.caps.kitty_graphics = true; 215 - } 216 - }, 217 - .cap_rgb => { 218 - log.info("rgb capability detected", .{}); 219 - self.vaxis.caps.rgb = true; 220 - }, 221 - .cap_unicode => { 222 - log.info("unicode capability detected", .{}); 223 - self.vaxis.caps.unicode = .unicode; 224 - self.vaxis.screen.width_method = .unicode; 225 - }, 226 - .cap_sgr_pixels => { 227 - log.info("pixel mouse capability detected", .{}); 228 - self.vaxis.caps.sgr_pixels = true; 229 - }, 230 - .cap_color_scheme_updates => { 231 - log.info("color_scheme_updates capability detected", .{}); 232 - self.vaxis.caps.color_scheme_updates = true; 233 - }, 234 - .cap_da1 => { 235 - std.Thread.Futex.wake(&self.vaxis.query_futex, 10); 236 - }, 277 + } 237 278 } 238 - } 279 + }, 239 280 } 240 281 } 241 282 };
+1 -1
src/Screen.zig
··· 4 4 const Cell = @import("Cell.zig"); 5 5 const Shape = @import("Mouse.zig").Shape; 6 6 const Image = @import("Image.zig"); 7 - const Winsize = @import("tty.zig").Winsize; 7 + const Winsize = @import("main.zig").Winsize; 8 8 const Unicode = @import("Unicode.zig"); 9 9 const Method = @import("gwidth.zig").Method; 10 10
+32 -23
src/Vaxis.zig
··· 1 1 const std = @import("std"); 2 + const builtin = @import("builtin"); 2 3 const atomic = std.atomic; 3 4 const base64Encoder = std.base64.standard.Encoder; 4 5 const zigimg = @import("zigimg"); ··· 17 18 const KittyFlags = Key.KittyFlags; 18 19 const Shape = Mouse.Shape; 19 20 const Style = Cell.Style; 20 - const Winsize = @import("tty.zig").Winsize; 21 + const Winsize = @import("main.zig").Winsize; 21 22 22 23 const ctlseqs = @import("ctlseqs.zig"); 23 24 const gwidth = @import("gwidth.zig"); ··· 253 254 /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if 254 255 /// you are using Loop.run() 255 256 pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void { 256 - // Apply any environment variables 257 - if (std.posix.getenv("ASCIINEMA_REC")) |_| 258 - self.sgr = .legacy; 259 - if (std.posix.getenv("TERMUX_VERSION")) |_| 260 - self.sgr = .legacy; 261 - if (std.posix.getenv("VHS_RECORD")) |_| { 262 - self.caps.unicode = .wcwidth; 263 - self.caps.kitty_keyboard = false; 264 - self.sgr = .legacy; 265 - } 266 - if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_| 267 - self.sgr = .legacy; 268 - if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_| 269 - self.caps.unicode = .wcwidth; 270 - if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_| 271 - self.caps.unicode = .unicode; 257 + switch (builtin.os.tag) { 258 + .windows => { 259 + // No feature detection on windows. We just hard enable some knowns for ConPTY 260 + self.sgr = .legacy; 261 + }, 262 + else => { 263 + // Apply any environment variables 264 + if (std.posix.getenv("ASCIINEMA_REC")) |_| 265 + self.sgr = .legacy; 266 + if (std.posix.getenv("TERMUX_VERSION")) |_| 267 + self.sgr = .legacy; 268 + if (std.posix.getenv("VHS_RECORD")) |_| { 269 + self.caps.unicode = .wcwidth; 270 + self.caps.kitty_keyboard = false; 271 + self.sgr = .legacy; 272 + } 273 + if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_| 274 + self.sgr = .legacy; 275 + if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_| 276 + self.caps.unicode = .wcwidth; 277 + if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_| 278 + self.caps.unicode = .unicode; 272 279 273 - // enable detected features 274 - if (self.caps.kitty_keyboard) { 275 - try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags); 276 - } 277 - if (self.caps.unicode == .unicode) { 278 - try tty.writeAll(ctlseqs.unicode_set); 280 + // enable detected features 281 + if (self.caps.kitty_keyboard) { 282 + try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags); 283 + } 284 + if (self.caps.unicode == .unicode) { 285 + try tty.writeAll(ctlseqs.unicode_set); 286 + } 287 + }, 279 288 } 280 289 } 281 290
+2
src/event.zig
··· 1 1 pub const Key = @import("Key.zig"); 2 2 pub const Mouse = @import("Mouse.zig"); 3 3 pub const Color = @import("Cell.zig").Color; 4 + pub const Winsize = @import("main.zig").Winsize; 4 5 5 6 /// The events that Vaxis emits internally 6 7 pub const Event = union(enum) { ··· 14 15 paste: []const u8, // osc 52 paste, caller must free 15 16 color_report: Color.Report, // osc 4, 10, 11, 12 response 16 17 color_scheme: Color.Scheme, 18 + winsize: Winsize, 17 19 18 20 // these are delivered as discovered terminal capabilities 19 21 cap_kitty_keyboard,
+14 -4
src/main.zig
··· 19 19 pub const AllocatingScreen = @import("InternalScreen.zig"); 20 20 pub const Parser = @import("Parser.zig"); 21 21 pub const Window = @import("Window.zig"); 22 - pub const tty = @import("tty.zig"); 23 - pub const Tty = tty.Tty; 24 - pub const Winsize = tty.Winsize; 25 - 26 22 pub const widgets = @import("widgets.zig"); 27 23 pub const gwidth = @import("gwidth.zig"); 28 24 pub const ctlseqs = @import("ctlseqs.zig"); 25 + 26 + /// The target TTY implementation 27 + pub const Tty = switch (builtin.os.tag) { 28 + .windows => @import("windows/Tty.zig"), 29 + else => @import("posix/Tty.zig"), 30 + }; 31 + 32 + /// The size of the terminal screen 33 + pub const Winsize = struct { 34 + rows: usize, 35 + cols: usize, 36 + x_pixel: usize, 37 + y_pixel: usize, 38 + }; 29 39 30 40 /// Initialize a Vaxis application. 31 41 pub fn init(alloc: std.mem.Allocator, opts: Vaxis.Options) !Vaxis {
+177
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 + /// global tty instance, used in case of a panic. Not guaranteed to work if 27 + /// for some reason there are multiple TTYs open under a single vaxis 28 + /// compilation unit - but this is better than nothing 29 + pub var global_tty: ?Posix = null; 30 + 31 + /// initializes a Tty instance by opening /dev/tty and "making it raw". A 32 + /// signal handler is installed for SIGWINCH. No callbacks are installed, be 33 + /// sure to register a callback when initializing the event loop 34 + pub fn init() !Posix { 35 + // Open our tty 36 + const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); 37 + 38 + // Set the termios of the tty 39 + const termios = try makeRaw(fd); 40 + 41 + var act = posix.Sigaction{ 42 + .handler = .{ .handler = Posix.handleWinch }, 43 + .mask = switch (builtin.os.tag) { 44 + .macos => 0, 45 + .linux => posix.empty_sigset, 46 + else => @compileError("os not supported"), 47 + }, 48 + .flags = 0, 49 + }; 50 + try posix.sigaction(posix.SIG.WINCH, &act, null); 51 + 52 + const self: Posix = .{ 53 + .fd = fd, 54 + .termios = termios, 55 + }; 56 + 57 + global_tty = self; 58 + 59 + return self; 60 + } 61 + 62 + /// release resources associated with the Tty return it to its original state 63 + pub fn deinit(self: Posix) void { 64 + posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| { 65 + std.log.err("couldn't restore terminal: {}", .{err}); 66 + }; 67 + if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos 68 + posix.close(self.fd); 69 + } 70 + 71 + /// Write bytes to the tty 72 + pub fn write(self: *const Posix, bytes: []const u8) !usize { 73 + return posix.write(self.fd, bytes); 74 + } 75 + 76 + pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { 77 + const self: *const Posix = @ptrCast(@alignCast(ptr)); 78 + return posix.write(self.fd, bytes); 79 + } 80 + 81 + pub fn anyWriter(self: *const Posix) std.io.AnyWriter { 82 + return .{ 83 + .context = self, 84 + .writeFn = Posix.opaqueWrite, 85 + }; 86 + } 87 + 88 + pub fn read(self: *const Posix, buf: []u8) !usize { 89 + return posix.read(self.fd, buf); 90 + } 91 + 92 + pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { 93 + const self: *const Posix = @ptrCast(@alignCast(ptr)); 94 + return posix.read(self.fd, buf); 95 + } 96 + 97 + pub fn anyReader(self: *const Posix) std.io.AnyReader { 98 + return .{ 99 + .context = self, 100 + .readFn = Posix.opaqueRead, 101 + }; 102 + } 103 + 104 + /// Install a signal handler for winsize. A maximum of 8 handlers may be 105 + /// installed 106 + pub fn notifyWinsize(handler: SignalHandler) !void { 107 + handler_mutex.lock(); 108 + defer handler_mutex.unlock(); 109 + if (handler_idx == handlers.len) return error.OutOfMemory; 110 + handlers[handler_idx] = handler; 111 + handler_idx += 1; 112 + } 113 + 114 + fn handleWinch(_: c_int) callconv(.C) void { 115 + handler_mutex.lock(); 116 + defer handler_mutex.unlock(); 117 + var i: usize = 0; 118 + while (i < handler_idx) : (i += 1) { 119 + const handler = handlers[i]; 120 + handler.callback(handler.context); 121 + } 122 + } 123 + 124 + /// makeRaw enters the raw state for the terminal. 125 + pub fn makeRaw(fd: posix.fd_t) !posix.termios { 126 + const state = try posix.tcgetattr(fd); 127 + var raw = state; 128 + // see termios(3) 129 + raw.iflag.IGNBRK = false; 130 + raw.iflag.BRKINT = false; 131 + raw.iflag.PARMRK = false; 132 + raw.iflag.ISTRIP = false; 133 + raw.iflag.INLCR = false; 134 + raw.iflag.IGNCR = false; 135 + raw.iflag.ICRNL = false; 136 + raw.iflag.IXON = false; 137 + 138 + raw.oflag.OPOST = false; 139 + 140 + raw.lflag.ECHO = false; 141 + raw.lflag.ECHONL = false; 142 + raw.lflag.ICANON = false; 143 + raw.lflag.ISIG = false; 144 + raw.lflag.IEXTEN = false; 145 + 146 + raw.cflag.CSIZE = .CS8; 147 + raw.cflag.PARENB = false; 148 + 149 + raw.cc[@intFromEnum(posix.V.MIN)] = 1; 150 + raw.cc[@intFromEnum(posix.V.TIME)] = 0; 151 + try posix.tcsetattr(fd, .FLUSH, raw); 152 + return state; 153 + } 154 + 155 + /// Get the window size from the kernel 156 + pub fn getWinsize(fd: posix.fd_t) !Winsize { 157 + var winsize = posix.winsize{ 158 + .ws_row = 0, 159 + .ws_col = 0, 160 + .ws_xpixel = 0, 161 + .ws_ypixel = 0, 162 + }; 163 + 164 + const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); 165 + if (posix.errno(err) == .SUCCESS) 166 + return Winsize{ 167 + .rows = winsize.ws_row, 168 + .cols = winsize.ws_col, 169 + .x_pixel = winsize.ws_xpixel, 170 + .y_pixel = winsize.ws_ypixel, 171 + }; 172 + return error.IoctlError; 173 + } 174 + 175 + pub fn bufferedWriter(self: *const Posix) std.io.BufferedWriter(4096, std.io.AnyWriter) { 176 + return std.io.bufferedWriter(self.anyWriter()); 177 + }
-190
src/tty.zig
··· 1 - const std = @import("std"); 2 - const builtin = @import("builtin"); 3 - const posix = std.posix; 4 - 5 - const log = std.log.scoped(.tty); 6 - 7 - pub const Tty = switch (builtin.os.tag) { 8 - .windows => @compileError("windows not supported, try wsl"), 9 - else => PosixTty, 10 - }; 11 - 12 - /// The size of the terminal screen 13 - pub const Winsize = struct { 14 - rows: usize, 15 - cols: usize, 16 - x_pixel: usize, 17 - y_pixel: usize, 18 - }; 19 - 20 - /// TTY implementation conforming to posix standards 21 - pub const PosixTty = struct { 22 - /// the original state of the terminal, prior to calling makeRaw 23 - termios: posix.termios, 24 - 25 - /// The file descriptor of the tty 26 - fd: posix.fd_t, 27 - 28 - pub const SignalHandler = struct { 29 - context: *anyopaque, 30 - callback: *const fn (context: *anyopaque) void, 31 - }; 32 - 33 - /// global signal handlers 34 - var handlers: [8]SignalHandler = undefined; 35 - var handler_mutex: std.Thread.Mutex = .{}; 36 - var handler_idx: usize = 0; 37 - 38 - /// global tty instance, used in case of a panic. Not guaranteed to work if 39 - /// for some reason there are multiple TTYs open under a single vaxis 40 - /// compilation unit - but this is better than nothing 41 - pub var global_tty: ?PosixTty = null; 42 - 43 - /// initializes a Tty instance by opening /dev/tty and "making it raw". A 44 - /// signal handler is installed for SIGWINCH. No callbacks are installed, be 45 - /// sure to register a callback when initializing the event loop 46 - pub fn init() !PosixTty { 47 - // Open our tty 48 - const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); 49 - 50 - // Set the termios of the tty 51 - const termios = try makeRaw(fd); 52 - 53 - var act = posix.Sigaction{ 54 - .handler = .{ .handler = PosixTty.handleWinch }, 55 - .mask = switch (builtin.os.tag) { 56 - .macos => 0, 57 - .linux => posix.empty_sigset, 58 - else => @compileError("os not supported"), 59 - }, 60 - .flags = 0, 61 - }; 62 - try posix.sigaction(posix.SIG.WINCH, &act, null); 63 - 64 - const self: PosixTty = .{ 65 - .fd = fd, 66 - .termios = termios, 67 - }; 68 - 69 - global_tty = self; 70 - 71 - return self; 72 - } 73 - 74 - /// release resources associated with the Tty return it to its original state 75 - pub fn deinit(self: PosixTty) void { 76 - posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| { 77 - log.err("couldn't restore terminal: {}", .{err}); 78 - }; 79 - if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos 80 - posix.close(self.fd); 81 - } 82 - 83 - /// Write bytes to the tty 84 - pub fn write(self: *const PosixTty, bytes: []const u8) !usize { 85 - return posix.write(self.fd, bytes); 86 - } 87 - 88 - pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { 89 - const self: *const PosixTty = @ptrCast(@alignCast(ptr)); 90 - return posix.write(self.fd, bytes); 91 - } 92 - 93 - pub fn anyWriter(self: *const PosixTty) std.io.AnyWriter { 94 - return .{ 95 - .context = self, 96 - .writeFn = PosixTty.opaqueWrite, 97 - }; 98 - } 99 - 100 - pub fn read(self: *const PosixTty, buf: []u8) !usize { 101 - return posix.read(self.fd, buf); 102 - } 103 - 104 - pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { 105 - const self: *const PosixTty = @ptrCast(@alignCast(ptr)); 106 - return posix.read(self.fd, buf); 107 - } 108 - 109 - pub fn anyReader(self: *const PosixTty) std.io.AnyReader { 110 - return .{ 111 - .context = self, 112 - .readFn = PosixTty.opaqueRead, 113 - }; 114 - } 115 - 116 - /// Install a signal handler for winsize. A maximum of 8 handlers may be 117 - /// installed 118 - pub fn notifyWinsize(handler: SignalHandler) !void { 119 - handler_mutex.lock(); 120 - defer handler_mutex.unlock(); 121 - if (handler_idx == handlers.len) return error.OutOfMemory; 122 - handlers[handler_idx] = handler; 123 - handler_idx += 1; 124 - } 125 - 126 - fn handleWinch(_: c_int) callconv(.C) void { 127 - handler_mutex.lock(); 128 - defer handler_mutex.unlock(); 129 - var i: usize = 0; 130 - while (i < handler_idx) : (i += 1) { 131 - const handler = handlers[i]; 132 - handler.callback(handler.context); 133 - } 134 - } 135 - 136 - /// makeRaw enters the raw state for the terminal. 137 - pub fn makeRaw(fd: posix.fd_t) !posix.termios { 138 - const state = try posix.tcgetattr(fd); 139 - var raw = state; 140 - // see termios(3) 141 - raw.iflag.IGNBRK = false; 142 - raw.iflag.BRKINT = false; 143 - raw.iflag.PARMRK = false; 144 - raw.iflag.ISTRIP = false; 145 - raw.iflag.INLCR = false; 146 - raw.iflag.IGNCR = false; 147 - raw.iflag.ICRNL = false; 148 - raw.iflag.IXON = false; 149 - 150 - raw.oflag.OPOST = false; 151 - 152 - raw.lflag.ECHO = false; 153 - raw.lflag.ECHONL = false; 154 - raw.lflag.ICANON = false; 155 - raw.lflag.ISIG = false; 156 - raw.lflag.IEXTEN = false; 157 - 158 - raw.cflag.CSIZE = .CS8; 159 - raw.cflag.PARENB = false; 160 - 161 - raw.cc[@intFromEnum(posix.V.MIN)] = 1; 162 - raw.cc[@intFromEnum(posix.V.TIME)] = 0; 163 - try posix.tcsetattr(fd, .FLUSH, raw); 164 - return state; 165 - } 166 - 167 - /// Get the window size from the kernel 168 - pub fn getWinsize(fd: posix.fd_t) !Winsize { 169 - var winsize = posix.winsize{ 170 - .ws_row = 0, 171 - .ws_col = 0, 172 - .ws_xpixel = 0, 173 - .ws_ypixel = 0, 174 - }; 175 - 176 - const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); 177 - if (posix.errno(err) == .SUCCESS) 178 - return Winsize{ 179 - .rows = winsize.ws_row, 180 - .cols = winsize.ws_col, 181 - .x_pixel = winsize.ws_xpixel, 182 - .y_pixel = winsize.ws_ypixel, 183 - }; 184 - return error.IoctlError; 185 - } 186 - 187 - pub fn bufferedWriter(self: *const PosixTty) std.io.BufferedWriter(4096, std.io.AnyWriter) { 188 - return std.io.bufferedWriter(self.anyWriter()); 189 - } 190 - };
+389
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 windows = std.os.windows; 9 + 10 + stdin: windows.HANDLE, 11 + stdout: windows.HANDLE, 12 + 13 + initial_codepage: c_uint, 14 + initial_input_mode: u32, 15 + initial_output_mode: u32, 16 + 17 + // a buffer to write key text into 18 + buf: [4]u8 = undefined, 19 + 20 + pub var global_tty: ?Tty = null; 21 + 22 + const utf8_codepage: c_uint = 65001; 23 + 24 + const InputMode = struct { 25 + const enable_window_input: u32 = 0x0008; // resize events 26 + const enable_mouse_input: u32 = 0x0010; 27 + 28 + pub fn rawMode() u32 { 29 + return enable_window_input | enable_mouse_input; 30 + } 31 + }; 32 + 33 + const OutputMode = struct { 34 + const enable_processed_output: u32 = 0x0001; // handle control sequences 35 + const enable_virtual_terminal_processing: u32 = 0x0004; // handle ANSI sequences 36 + const disable_newline_auto_return: u32 = 0x0008; // disable inserting a new line when we write at the last column 37 + const enable_lvb_grid_worldwide: u32 = 0x0010; // enables reverse video and underline 38 + 39 + fn rawMode() u32 { 40 + return enable_processed_output | 41 + enable_virtual_terminal_processing | 42 + disable_newline_auto_return | 43 + enable_lvb_grid_worldwide; 44 + } 45 + }; 46 + 47 + pub fn init() !Tty { 48 + const stdin = try windows.GetStdHandle(windows.STD_INPUT_HANDLE); 49 + const stdout = try windows.GetStdHandle(windows.STD_OUTPUT_HANDLE); 50 + 51 + // get initial modes 52 + var initial_input_mode: windows.DWORD = undefined; 53 + var initial_output_mode: windows.DWORD = undefined; 54 + const initial_output_codepage = windows.kernel32.GetConsoleOutputCP(); 55 + { 56 + if (windows.kernel32.GetConsoleMode(stdin, &initial_input_mode) == 0) { 57 + return windows.unexpectedError(windows.kernel32.GetLastError()); 58 + } 59 + if (windows.kernel32.GetConsoleMode(stdout, &initial_output_mode) == 0) { 60 + return windows.unexpectedError(windows.kernel32.GetLastError()); 61 + } 62 + } 63 + 64 + // set new modes 65 + { 66 + if (SetConsoleMode(stdin, InputMode.rawMode()) == 0) 67 + return windows.unexpectedError(windows.kernel32.GetLastError()); 68 + 69 + if (SetConsoleMode(stdout, OutputMode.rawMode()) == 0) 70 + return windows.unexpectedError(windows.kernel32.GetLastError()); 71 + 72 + if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0) 73 + return windows.unexpectedError(windows.kernel32.GetLastError()); 74 + } 75 + 76 + const self: Tty = .{ 77 + .stdin = stdin, 78 + .stdout = stdout, 79 + .initial_codepage = initial_output_codepage, 80 + .initial_input_mode = initial_input_mode, 81 + .initial_output_mode = initial_output_mode, 82 + }; 83 + 84 + // save a copy of this tty as the global_tty for panic handling 85 + global_tty = self; 86 + 87 + return self; 88 + } 89 + 90 + pub fn deinit(self: Tty) void { 91 + _ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage); 92 + _ = SetConsoleMode(self.stdin, self.initial_input_mode); 93 + _ = SetConsoleMode(self.stdout, self.initial_output_mode); 94 + windows.CloseHandle(self.stdin); 95 + windows.CloseHandle(self.stdout); 96 + } 97 + 98 + pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { 99 + const self: *const Tty = @ptrCast(@alignCast(ptr)); 100 + return windows.WriteFile(self.stdout, bytes, null); 101 + } 102 + 103 + pub fn anyWriter(self: *const Tty) std.io.AnyWriter { 104 + return .{ 105 + .context = self, 106 + .writeFn = Tty.opaqueWrite, 107 + }; 108 + } 109 + 110 + pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWriter) { 111 + return std.io.bufferedWriter(self.anyWriter()); 112 + } 113 + 114 + pub fn nextEvent(self: *Tty) !Event { 115 + // We use a loop so we can ignore certain events 116 + var ansi_buf: [128]u8 = undefined; 117 + var ansi_idx: usize = 0; 118 + var escape_st: bool = false; 119 + while (true) { 120 + var event_count: u32 = 0; 121 + var input_record: INPUT_RECORD = undefined; 122 + if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0) 123 + return windows.unexpectedError(windows.kernel32.GetLastError()); 124 + 125 + switch (input_record.EventType) { 126 + 0x0001 => { // Key event 127 + const event = input_record.Event.KeyEvent; 128 + 129 + const base_layout: u21 = switch (event.wVirtualKeyCode) { 130 + 0x00 => { // delivered when we get an escape sequence 131 + ansi_buf[ansi_idx] = event.uChar.AsciiChar; 132 + ansi_idx += 1; 133 + if (ansi_idx <= 2) { 134 + continue; 135 + } 136 + switch (ansi_buf[1]) { 137 + '[' => { // CSI, read until 0x40 to 0xFF 138 + switch (event.uChar.AsciiChar) { 139 + 0x40...0xFF => { 140 + return .cap_da1; 141 + }, 142 + else => continue, 143 + } 144 + }, 145 + ']' => { // OSC, read until ESC \ or BEL 146 + switch (event.uChar.AsciiChar) { 147 + 0x07 => { 148 + return .cap_da1; 149 + }, 150 + 0x1B => { 151 + escape_st = true; 152 + continue; 153 + }, 154 + '\\' => { 155 + if (escape_st) { 156 + return .cap_da1; 157 + } 158 + continue; 159 + }, 160 + else => continue, 161 + } 162 + }, 163 + else => continue, 164 + } 165 + }, 166 + 0x08 => Key.backspace, 167 + 0x09 => Key.tab, 168 + 0x0D => Key.enter, 169 + 0x13 => Key.pause, 170 + 0x14 => Key.caps_lock, 171 + 0x1B => Key.escape, 172 + 0x20 => Key.space, 173 + 0x21 => Key.page_up, 174 + 0x22 => Key.page_down, 175 + 0x23 => Key.end, 176 + 0x24 => Key.home, 177 + 0x25 => Key.left, 178 + 0x26 => Key.up, 179 + 0x27 => Key.right, 180 + 0x28 => Key.down, 181 + 0x2c => Key.print_screen, 182 + 0x2d => Key.insert, 183 + 0x2e => Key.delete, 184 + 0x30...0x39 => |k| k, 185 + 0x41...0x5a => |k| k + 0x20, // translate to lowercase 186 + 0x5b => Key.left_meta, 187 + 0x5c => Key.right_meta, 188 + 0x60 => Key.kp_0, 189 + 0x61 => Key.kp_1, 190 + 0x62 => Key.kp_2, 191 + 0x63 => Key.kp_3, 192 + 0x64 => Key.kp_4, 193 + 0x65 => Key.kp_5, 194 + 0x66 => Key.kp_6, 195 + 0x67 => Key.kp_7, 196 + 0x68 => Key.kp_8, 197 + 0x69 => Key.kp_9, 198 + 0x6a => Key.kp_multiply, 199 + 0x6b => Key.kp_add, 200 + 0x6c => Key.kp_separator, 201 + 0x6d => Key.kp_subtract, 202 + 0x6e => Key.kp_decimal, 203 + 0x6f => Key.kp_divide, 204 + 0x70 => Key.f1, 205 + 0x71 => Key.f2, 206 + 0x72 => Key.f3, 207 + 0x73 => Key.f4, 208 + 0x74 => Key.f5, 209 + 0x75 => Key.f6, 210 + 0x76 => Key.f8, 211 + 0x77 => Key.f8, 212 + 0x78 => Key.f9, 213 + 0x79 => Key.f10, 214 + 0x7a => Key.f11, 215 + 0x7b => Key.f12, 216 + 0x7c => Key.f13, 217 + 0x7d => Key.f14, 218 + 0x7e => Key.f15, 219 + 0x7f => Key.f16, 220 + 0x80 => Key.f17, 221 + 0x81 => Key.f18, 222 + 0x82 => Key.f19, 223 + 0x83 => Key.f20, 224 + 0x84 => Key.f21, 225 + 0x85 => Key.f22, 226 + 0x86 => Key.f23, 227 + 0x87 => Key.f24, 228 + 0x90 => Key.num_lock, 229 + 0x91 => Key.scroll_lock, 230 + 0xa0 => Key.left_shift, 231 + 0xa1 => Key.right_shift, 232 + 0xa2 => Key.left_control, 233 + 0xa3 => Key.right_control, 234 + 0xa4 => Key.left_alt, 235 + 0xa5 => Key.right_alt, 236 + 0xad => Key.mute_volume, 237 + 0xae => Key.lower_volume, 238 + 0xaf => Key.raise_volume, 239 + 0xb0 => Key.media_track_next, 240 + 0xb1 => Key.media_track_previous, 241 + 0xb2 => Key.media_stop, 242 + 0xb3 => Key.media_play_pause, 243 + 0xba => ';', 244 + 0xbb => '+', 245 + 0xbc => ',', 246 + 0xbd => '-', 247 + 0xbe => '.', 248 + 0xbf => '/', 249 + 0xc0 => '`', 250 + 0xdb => '[', 251 + 0xdc => '\\', 252 + 0xdd => ']', 253 + 0xde => '\'', 254 + else => continue, 255 + }; 256 + 257 + var codepoint: u21 = base_layout; 258 + var text: ?[]const u8 = null; 259 + switch (event.uChar.UnicodeChar) { 260 + 0x00...0x1F => {}, 261 + else => |cp| { 262 + codepoint = cp; 263 + const n = try std.unicode.utf8Encode(cp, &self.buf); 264 + text = self.buf[0..n]; 265 + }, 266 + } 267 + 268 + const key: Key = .{ 269 + .codepoint = codepoint, 270 + .base_layout_codepoint = base_layout, 271 + .mods = translateMods(event.dwControlKeyState), 272 + .text = text, 273 + }; 274 + 275 + switch (event.bKeyDown) { 276 + 0 => return .{ .key_release = key }, 277 + else => return .{ .key_press = key }, 278 + } 279 + }, 280 + 0x0002 => { // TODO: ConPTY doesn't pass through MOUSE_EVENT input records yet. 281 + // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str 282 + }, 283 + 0x0004 => { // Screen resize events 284 + // NOTE: Even though the event comes with a size, it may not be accurate. We ask for 285 + // the size directly when we get this event 286 + var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; 287 + if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) { 288 + return windows.unexpectedError(windows.kernel32.GetLastError()); 289 + } 290 + const window_rect = console_info.srWindow; 291 + const width = window_rect.Right - window_rect.Left; 292 + const height = window_rect.Bottom - window_rect.Top; 293 + return .{ 294 + .winsize = .{ 295 + .cols = @intCast(width), 296 + .rows = @intCast(height), 297 + .x_pixel = 0, 298 + .y_pixel = 0, 299 + }, 300 + }; 301 + }, 302 + 0x0010 => { // Focus events 303 + switch (input_record.Event.FocusEvent.bSetFocus) { 304 + 0 => return .focus_out, 305 + else => return .focus_in, 306 + } 307 + }, 308 + else => {}, 309 + } 310 + } 311 + } 312 + 313 + fn translateMods(mods: u32) Key.Modifiers { 314 + const left_alt: u32 = 0x0002; 315 + const right_alt: u32 = 0x0001; 316 + const left_ctrl: u32 = 0x0008; 317 + const right_ctrl: u32 = 0x0004; 318 + 319 + const caps: u32 = 0x0080; 320 + const num_lock: u32 = 0x0020; 321 + const shift: u32 = 0x0010; 322 + const alt: u32 = left_alt | right_alt; 323 + const ctrl: u32 = left_ctrl | right_ctrl; 324 + 325 + return .{ 326 + .shift = mods & shift > 0, 327 + .alt = mods & alt > 0, 328 + .ctrl = mods & ctrl > 0, 329 + .caps_lock = mods & caps > 0, 330 + .num_lock = mods & num_lock > 0, 331 + }; 332 + } 333 + 334 + // From gitub.com/ziglibs/zig-windows-console. Thanks :) 335 + // 336 + // Events 337 + const union_unnamed_248 = extern union { 338 + UnicodeChar: windows.WCHAR, 339 + AsciiChar: windows.CHAR, 340 + }; 341 + pub const KEY_EVENT_RECORD = extern struct { 342 + bKeyDown: windows.BOOL, 343 + wRepeatCount: windows.WORD, 344 + wVirtualKeyCode: windows.WORD, 345 + wVirtualScanCode: windows.WORD, 346 + uChar: union_unnamed_248, 347 + dwControlKeyState: windows.DWORD, 348 + }; 349 + pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD; 350 + 351 + pub const MOUSE_EVENT_RECORD = extern struct { 352 + dwMousePosition: windows.COORD, 353 + dwButtonState: windows.DWORD, 354 + dwControlKeyState: windows.DWORD, 355 + dwEventFlags: windows.DWORD, 356 + }; 357 + pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD; 358 + 359 + pub const WINDOW_BUFFER_SIZE_RECORD = extern struct { 360 + dwSize: windows.COORD, 361 + }; 362 + pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD; 363 + 364 + pub const MENU_EVENT_RECORD = extern struct { 365 + dwCommandId: windows.UINT, 366 + }; 367 + pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD; 368 + 369 + pub const FOCUS_EVENT_RECORD = extern struct { 370 + bSetFocus: windows.BOOL, 371 + }; 372 + pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD; 373 + 374 + const union_unnamed_249 = extern union { 375 + KeyEvent: KEY_EVENT_RECORD, 376 + MouseEvent: MOUSE_EVENT_RECORD, 377 + WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD, 378 + MenuEvent: MENU_EVENT_RECORD, 379 + FocusEvent: FOCUS_EVENT_RECORD, 380 + }; 381 + pub const INPUT_RECORD = extern struct { 382 + EventType: windows.WORD, 383 + Event: union_unnamed_249, 384 + }; 385 + pub const PINPUT_RECORD = *INPUT_RECORD; 386 + 387 + pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL; 388 + // TODO: remove this in zig 0.13.0 389 + pub extern "kernel32" fn SetConsoleMode(in_hConsoleHandle: windows.HANDLE, in_dwMode: windows.DWORD) callconv(windows.WINAPI) windows.BOOL;
+4 -2
src/xev.zig
··· 1 1 const std = @import("std"); 2 2 const xev = @import("xev"); 3 3 4 - const Tty = @import("tty.zig").Tty; 5 - const Winsize = @import("tty.zig").Winsize; 4 + const Tty = @import("main.zig").Tty; 5 + const Winsize = @import("main.zig").Winsize; 6 6 const Vaxis = @import("Vaxis.zig"); 7 7 const Parser = @import("Parser.zig"); 8 8 const Key = @import("Key.zig"); ··· 161 161 .paste => |paste| .{ .paste = paste }, 162 162 .color_report => |report| .{ .color_report = report }, 163 163 .color_scheme => |scheme| .{ .color_scheme = scheme }, 164 + .winsize => |ws| .{ .winsize = ws }, 164 165 165 166 // capability events which we handle below 166 167 .cap_kitty_keyboard, ··· 192 193 .paste, 193 194 .color_report, 194 195 .color_scheme, 196 + .winsize, 195 197 => unreachable, // handled above 196 198 197 199 .cap_kitty_keyboard => {