diff --git a/src/Loop.zig b/src/Loop.zig index 4c1ecd6..c510d7c 100644 --- a/src/Loop.zig +++ b/src/Loop.zig @@ -6,8 +6,7 @@ const grapheme = @import("grapheme"); const GraphemeCache = @import("GraphemeCache.zig"); const Parser = @import("Parser.zig"); const Queue = @import("queue.zig").Queue; -const tty = @import("tty.zig"); -const Tty = tty.Tty; +const Tty = @import("main.zig").Tty; const Vaxis = @import("Vaxis.zig"); pub fn Loop(comptime T: type) type { @@ -29,7 +28,7 @@ pub fn Loop(comptime T: type) type { /// a stable pointer to register signal callbacks with posix TTYs pub fn init(self: *Self) !void { switch (builtin.os.tag) { - .windows => @compileError("windows not supported"), + .windows => {}, else => { const handler: Tty.SignalHandler = .{ .context = self, @@ -104,138 +103,180 @@ pub fn Loop(comptime T: type) type { grapheme_data: *const grapheme.GraphemeData, paste_allocator: ?std.mem.Allocator, ) !void { - // get our initial winsize - const winsize = try Tty.getWinsize(self.tty.fd); - if (@hasField(Event, "winsize")) { - self.postEvent(.{ .winsize = winsize }); - } - // initialize a grapheme cache var cache: GraphemeCache = .{}; - var parser: Parser = .{ - .grapheme_data = grapheme_data, - }; - - // initialize the read buffer - var buf: [1024]u8 = undefined; - var read_start: usize = 0; - // read loop - while (!self.should_quit) { - const n = try self.tty.read(buf[read_start..]); - var seq_start: usize = 0; - while (seq_start < n) { - const result = try parser.parse(buf[seq_start..n], paste_allocator); - if (result.n == 0) { - // copy the read to the beginning. We don't use memcpy because - // this could be overlapping, and it's also rare - const initial_start = seq_start; - while (seq_start < n) : (seq_start += 1) { - buf[seq_start - initial_start] = buf[seq_start]; + switch (builtin.os.tag) { + .windows => { + while (!self.should_quit) { + const event = try self.tty.nextEvent(); + switch (event) { + .winsize => |ws| { + if (@hasField(Event, "winsize")) { + self.postEvent(.{ .winsize = ws }); + } + }, + .key_press => |key| { + if (@hasField(Event, "key_press")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + self.postEvent(.{ .key_press = mut_key }); + } + }, + .key_release => |*key| { + if (@hasField(Event, "key_release")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + self.postEvent(.{ .key_release = mut_key }); + } + }, + .cap_da1 => { + std.Thread.Futex.wake(&self.vaxis.query_futex, 10); + }, + .mouse => {}, // Unsupported currently + else => {}, } - read_start = seq_start - initial_start + 1; - continue; } - read_start = 0; - seq_start += result.n; + }, + else => { + // get our initial winsize + const winsize = try Tty.getWinsize(self.tty.fd); + if (@hasField(Event, "winsize")) { + self.postEvent(.{ .winsize = winsize }); + } - const event = result.event orelse continue; - switch (event) { - .key_press => |key| { - if (@hasField(Event, "key_press")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); + var parser: Parser = .{ + .grapheme_data = grapheme_data, + }; + + // initialize the read buffer + var buf: [1024]u8 = undefined; + var read_start: usize = 0; + // read loop + while (!self.should_quit) { + const n = try self.tty.read(buf[read_start..]); + var seq_start: usize = 0; + while (seq_start < n) { + const result = try parser.parse(buf[seq_start..n], paste_allocator); + if (result.n == 0) { + // copy the read to the beginning. We don't use memcpy because + // this could be overlapping, and it's also rare + const initial_start = seq_start; + while (seq_start < n) : (seq_start += 1) { + buf[seq_start - initial_start] = buf[seq_start]; } - self.postEvent(.{ .key_press = mut_key }); + read_start = seq_start - initial_start + 1; + continue; } - }, - .key_release => |*key| { - if (@hasField(Event, "key_release")) { - // HACK: yuck. there has to be a better way - var mut_key = key; - if (key.text) |text| { - mut_key.text = cache.put(text); - } - self.postEvent(.{ .key_release = mut_key }); + read_start = 0; + seq_start += result.n; + + const event = result.event orelse continue; + switch (event) { + .key_press => |key| { + if (@hasField(Event, "key_press")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + self.postEvent(.{ .key_press = mut_key }); + } + }, + .key_release => |*key| { + if (@hasField(Event, "key_release")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + self.postEvent(.{ .key_release = mut_key }); + } + }, + .mouse => |mouse| { + if (@hasField(Event, "mouse")) { + self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); + } + }, + .focus_in => { + if (@hasField(Event, "focus_in")) { + self.postEvent(.focus_in); + } + }, + .focus_out => { + if (@hasField(Event, "focus_out")) { + self.postEvent(.focus_out); + } + }, + .paste_start => { + if (@hasField(Event, "paste_start")) { + self.postEvent(.paste_start); + } + }, + .paste_end => { + if (@hasField(Event, "paste_end")) { + self.postEvent(.paste_end); + } + }, + .paste => |text| { + if (@hasField(Event, "paste")) { + self.postEvent(.{ .paste = text }); + } else { + if (paste_allocator) |_| + paste_allocator.?.free(text); + } + }, + .color_report => |report| { + if (@hasField(Event, "color_report")) { + self.postEvent(.{ .color_report = report }); + } + }, + .color_scheme => |scheme| { + if (@hasField(Event, "color_scheme")) { + self.postEvent(.{ .color_scheme = scheme }); + } + }, + .cap_kitty_keyboard => { + log.info("kitty keyboard capability detected", .{}); + self.vaxis.caps.kitty_keyboard = true; + }, + .cap_kitty_graphics => { + if (!self.vaxis.caps.kitty_graphics) { + log.info("kitty graphics capability detected", .{}); + self.vaxis.caps.kitty_graphics = true; + } + }, + .cap_rgb => { + log.info("rgb capability detected", .{}); + self.vaxis.caps.rgb = true; + }, + .cap_unicode => { + log.info("unicode capability detected", .{}); + self.vaxis.caps.unicode = .unicode; + self.vaxis.screen.width_method = .unicode; + }, + .cap_sgr_pixels => { + log.info("pixel mouse capability detected", .{}); + self.vaxis.caps.sgr_pixels = true; + }, + .cap_color_scheme_updates => { + log.info("color_scheme_updates capability detected", .{}); + self.vaxis.caps.color_scheme_updates = true; + }, + .cap_da1 => { + std.Thread.Futex.wake(&self.vaxis.query_futex, 10); + }, + .winsize => unreachable, // handled elsewhere for posix } - }, - .mouse => |mouse| { - if (@hasField(Event, "mouse")) { - self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); - } - }, - .focus_in => { - if (@hasField(Event, "focus_in")) { - self.postEvent(.focus_in); - } - }, - .focus_out => { - if (@hasField(Event, "focus_out")) { - self.postEvent(.focus_out); - } - }, - .paste_start => { - if (@hasField(Event, "paste_start")) { - self.postEvent(.paste_start); - } - }, - .paste_end => { - if (@hasField(Event, "paste_end")) { - self.postEvent(.paste_end); - } - }, - .paste => |text| { - if (@hasField(Event, "paste")) { - self.postEvent(.{ .paste = text }); - } else { - if (paste_allocator) |_| - paste_allocator.?.free(text); - } - }, - .color_report => |report| { - if (@hasField(Event, "color_report")) { - self.postEvent(.{ .color_report = report }); - } - }, - .color_scheme => |scheme| { - if (@hasField(Event, "color_scheme")) { - self.postEvent(.{ .color_scheme = scheme }); - } - }, - .cap_kitty_keyboard => { - log.info("kitty keyboard capability detected", .{}); - self.vaxis.caps.kitty_keyboard = true; - }, - .cap_kitty_graphics => { - if (!self.vaxis.caps.kitty_graphics) { - log.info("kitty graphics capability detected", .{}); - self.vaxis.caps.kitty_graphics = true; - } - }, - .cap_rgb => { - log.info("rgb capability detected", .{}); - self.vaxis.caps.rgb = true; - }, - .cap_unicode => { - log.info("unicode capability detected", .{}); - self.vaxis.caps.unicode = .unicode; - self.vaxis.screen.width_method = .unicode; - }, - .cap_sgr_pixels => { - log.info("pixel mouse capability detected", .{}); - self.vaxis.caps.sgr_pixels = true; - }, - .cap_color_scheme_updates => { - log.info("color_scheme_updates capability detected", .{}); - self.vaxis.caps.color_scheme_updates = true; - }, - .cap_da1 => { - std.Thread.Futex.wake(&self.vaxis.query_futex, 10); - }, + } } - } + }, } } }; diff --git a/src/Screen.zig b/src/Screen.zig index 362c477..eb5489d 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -4,7 +4,7 @@ const assert = std.debug.assert; const Cell = @import("Cell.zig"); const Shape = @import("Mouse.zig").Shape; const Image = @import("Image.zig"); -const Winsize = @import("tty.zig").Winsize; +const Winsize = @import("main.zig").Winsize; const Unicode = @import("Unicode.zig"); const Method = @import("gwidth.zig").Method; diff --git a/src/Vaxis.zig b/src/Vaxis.zig index d0a5ae1..52da6e0 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const atomic = std.atomic; const base64Encoder = std.base64.standard.Encoder; const zigimg = @import("zigimg"); @@ -17,7 +18,7 @@ const Hyperlink = Cell.Hyperlink; const KittyFlags = Key.KittyFlags; const Shape = Mouse.Shape; const Style = Cell.Style; -const Winsize = @import("tty.zig").Winsize; +const Winsize = @import("main.zig").Winsize; const ctlseqs = @import("ctlseqs.zig"); const gwidth = @import("gwidth.zig"); @@ -253,29 +254,37 @@ pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void { /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if /// you are using Loop.run() pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void { - // Apply any environment variables - if (std.posix.getenv("ASCIINEMA_REC")) |_| - self.sgr = .legacy; - if (std.posix.getenv("TERMUX_VERSION")) |_| - self.sgr = .legacy; - if (std.posix.getenv("VHS_RECORD")) |_| { - self.caps.unicode = .wcwidth; - self.caps.kitty_keyboard = false; - self.sgr = .legacy; - } - if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_| - self.sgr = .legacy; - if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_| - self.caps.unicode = .wcwidth; - if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_| - self.caps.unicode = .unicode; + switch (builtin.os.tag) { + .windows => { + // No feature detection on windows. We just hard enable some knowns for ConPTY + self.sgr = .legacy; + }, + else => { + // Apply any environment variables + if (std.posix.getenv("ASCIINEMA_REC")) |_| + self.sgr = .legacy; + if (std.posix.getenv("TERMUX_VERSION")) |_| + self.sgr = .legacy; + if (std.posix.getenv("VHS_RECORD")) |_| { + self.caps.unicode = .wcwidth; + self.caps.kitty_keyboard = false; + self.sgr = .legacy; + } + if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_| + self.sgr = .legacy; + if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_| + self.caps.unicode = .wcwidth; + if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_| + self.caps.unicode = .unicode; - // enable detected features - if (self.caps.kitty_keyboard) { - try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags); - } - if (self.caps.unicode == .unicode) { - try tty.writeAll(ctlseqs.unicode_set); + // enable detected features + if (self.caps.kitty_keyboard) { + try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags); + } + if (self.caps.unicode == .unicode) { + try tty.writeAll(ctlseqs.unicode_set); + } + }, } } diff --git a/src/event.zig b/src/event.zig index 417d7d9..efa62f2 100644 --- a/src/event.zig +++ b/src/event.zig @@ -1,6 +1,7 @@ pub const Key = @import("Key.zig"); pub const Mouse = @import("Mouse.zig"); pub const Color = @import("Cell.zig").Color; +pub const Winsize = @import("main.zig").Winsize; /// The events that Vaxis emits internally pub const Event = union(enum) { @@ -14,6 +15,7 @@ pub const Event = union(enum) { paste: []const u8, // osc 52 paste, caller must free color_report: Color.Report, // osc 4, 10, 11, 12 response color_scheme: Color.Scheme, + winsize: Winsize, // these are delivered as discovered terminal capabilities cap_kitty_keyboard, diff --git a/src/main.zig b/src/main.zig index 51ccb4b..4f80eb3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,14 +19,24 @@ pub const Screen = @import("Screen.zig"); pub const AllocatingScreen = @import("InternalScreen.zig"); pub const Parser = @import("Parser.zig"); pub const Window = @import("Window.zig"); -pub const tty = @import("tty.zig"); -pub const Tty = tty.Tty; -pub const Winsize = tty.Winsize; - pub const widgets = @import("widgets.zig"); pub const gwidth = @import("gwidth.zig"); pub const ctlseqs = @import("ctlseqs.zig"); +/// The target TTY implementation +pub const Tty = switch (builtin.os.tag) { + .windows => @import("windows/Tty.zig"), + else => @import("posix/Tty.zig"), +}; + +/// The size of the terminal screen +pub const Winsize = struct { + rows: usize, + cols: usize, + x_pixel: usize, + y_pixel: usize, +}; + /// Initialize a Vaxis application. pub fn init(alloc: std.mem.Allocator, opts: Vaxis.Options) !Vaxis { return Vaxis.init(alloc, opts); diff --git a/src/posix/Tty.zig b/src/posix/Tty.zig new file mode 100644 index 0000000..57a2b2c --- /dev/null +++ b/src/posix/Tty.zig @@ -0,0 +1,177 @@ +//! TTY implementation conforming to posix standards +const Posix = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); + +const posix = std.posix; +const Winsize = @import("../main.zig").Winsize; + +/// the original state of the terminal, prior to calling makeRaw +termios: posix.termios, + +/// The file descriptor of the tty +fd: posix.fd_t, + +pub const SignalHandler = struct { + context: *anyopaque, + callback: *const fn (context: *anyopaque) void, +}; + +/// global signal handlers +var handlers: [8]SignalHandler = undefined; +var handler_mutex: std.Thread.Mutex = .{}; +var handler_idx: usize = 0; + +/// 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 nothing +pub var global_tty: ?Posix = null; + +/// 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() !Posix { + // Open our tty + const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); + + // Set the termios of the tty + const termios = try makeRaw(fd); + + var act = posix.Sigaction{ + .handler = .{ .handler = Posix.handleWinch }, + .mask = switch (builtin.os.tag) { + .macos => 0, + .linux => posix.empty_sigset, + else => @compileError("os not supported"), + }, + .flags = 0, + }; + try posix.sigaction(posix.SIG.WINCH, &act, null); + + const self: Posix = .{ + .fd = fd, + .termios = termios, + }; + + global_tty = self; + + return self; +} + +/// release resources associated with the Tty return it to its original state +pub fn deinit(self: Posix) void { + posix.tcsetattr(self.fd, .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 + posix.close(self.fd); +} + +/// Write bytes to the tty +pub fn write(self: *const Posix, bytes: []const u8) !usize { + return posix.write(self.fd, bytes); +} + +pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { + const self: *const Posix = @ptrCast(@alignCast(ptr)); + return posix.write(self.fd, bytes); +} + +pub fn anyWriter(self: *const Posix) std.io.AnyWriter { + return .{ + .context = self, + .writeFn = Posix.opaqueWrite, + }; +} + +pub fn read(self: *const Posix, buf: []u8) !usize { + return posix.read(self.fd, buf); +} + +pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { + const self: *const Posix = @ptrCast(@alignCast(ptr)); + return posix.read(self.fd, buf); +} + +pub fn anyReader(self: *const Posix) std.io.AnyReader { + return .{ + .context = self, + .readFn = Posix.opaqueRead, + }; +} + +/// Install a signal handler for winsize. A maximum of 8 handlers may be +/// installed +pub fn notifyWinsize(handler: SignalHandler) !void { + handler_mutex.lock(); + defer handler_mutex.unlock(); + if (handler_idx == handlers.len) return error.OutOfMemory; + handlers[handler_idx] = handler; + handler_idx += 1; +} + +fn handleWinch(_: c_int) callconv(.C) void { + handler_mutex.lock(); + defer handler_mutex.unlock(); + 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[@intFromEnum(posix.V.MIN)] = 1; + raw.cc[@intFromEnum(posix.V.TIME)] = 0; + try posix.tcsetattr(fd, .FLUSH, raw); + return state; +} + +/// Get the window size from the kernel +pub fn getWinsize(fd: posix.fd_t) !Winsize { + var winsize = posix.winsize{ + .ws_row = 0, + .ws_col = 0, + .ws_xpixel = 0, + .ws_ypixel = 0, + }; + + const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); + if (posix.errno(err) == .SUCCESS) + return Winsize{ + .rows = winsize.ws_row, + .cols = winsize.ws_col, + .x_pixel = winsize.ws_xpixel, + .y_pixel = winsize.ws_ypixel, + }; + return error.IoctlError; +} + +pub fn bufferedWriter(self: *const Posix) std.io.BufferedWriter(4096, std.io.AnyWriter) { + return std.io.bufferedWriter(self.anyWriter()); +} diff --git a/src/tty.zig b/src/tty.zig deleted file mode 100644 index f22b029..0000000 --- a/src/tty.zig +++ /dev/null @@ -1,190 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const posix = std.posix; - -const log = std.log.scoped(.tty); - -pub const Tty = switch (builtin.os.tag) { - .windows => @compileError("windows not supported, try wsl"), - else => PosixTty, -}; - -/// The size of the terminal screen -pub const Winsize = struct { - rows: usize, - cols: usize, - x_pixel: usize, - y_pixel: usize, -}; - -/// TTY implementation conforming to posix standards -pub const PosixTty = struct { - /// the original state of the terminal, prior to calling makeRaw - termios: posix.termios, - - /// The file descriptor of the tty - fd: posix.fd_t, - - pub const SignalHandler = struct { - context: *anyopaque, - callback: *const fn (context: *anyopaque) void, - }; - - /// global signal handlers - var handlers: [8]SignalHandler = undefined; - var handler_mutex: std.Thread.Mutex = .{}; - var handler_idx: usize = 0; - - /// 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 nothing - pub var global_tty: ?PosixTty = null; - - /// 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() !PosixTty { - // Open our tty - const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); - - // Set the termios of the tty - const termios = try makeRaw(fd); - - var act = posix.Sigaction{ - .handler = .{ .handler = PosixTty.handleWinch }, - .mask = switch (builtin.os.tag) { - .macos => 0, - .linux => posix.empty_sigset, - else => @compileError("os not supported"), - }, - .flags = 0, - }; - try posix.sigaction(posix.SIG.WINCH, &act, null); - - const self: PosixTty = .{ - .fd = fd, - .termios = termios, - }; - - global_tty = self; - - return self; - } - - /// release resources associated with the Tty return it to its original state - pub fn deinit(self: PosixTty) void { - posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| { - log.err("couldn't restore terminal: {}", .{err}); - }; - if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos - posix.close(self.fd); - } - - /// Write bytes to the tty - pub fn write(self: *const PosixTty, bytes: []const u8) !usize { - return posix.write(self.fd, bytes); - } - - pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { - const self: *const PosixTty = @ptrCast(@alignCast(ptr)); - return posix.write(self.fd, bytes); - } - - pub fn anyWriter(self: *const PosixTty) std.io.AnyWriter { - return .{ - .context = self, - .writeFn = PosixTty.opaqueWrite, - }; - } - - pub fn read(self: *const PosixTty, buf: []u8) !usize { - return posix.read(self.fd, buf); - } - - pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { - const self: *const PosixTty = @ptrCast(@alignCast(ptr)); - return posix.read(self.fd, buf); - } - - pub fn anyReader(self: *const PosixTty) std.io.AnyReader { - return .{ - .context = self, - .readFn = PosixTty.opaqueRead, - }; - } - - /// Install a signal handler for winsize. A maximum of 8 handlers may be - /// installed - pub fn notifyWinsize(handler: SignalHandler) !void { - handler_mutex.lock(); - defer handler_mutex.unlock(); - if (handler_idx == handlers.len) return error.OutOfMemory; - handlers[handler_idx] = handler; - handler_idx += 1; - } - - fn handleWinch(_: c_int) callconv(.C) void { - handler_mutex.lock(); - defer handler_mutex.unlock(); - 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[@intFromEnum(posix.V.MIN)] = 1; - raw.cc[@intFromEnum(posix.V.TIME)] = 0; - try posix.tcsetattr(fd, .FLUSH, raw); - return state; - } - - /// Get the window size from the kernel - pub fn getWinsize(fd: posix.fd_t) !Winsize { - var winsize = posix.winsize{ - .ws_row = 0, - .ws_col = 0, - .ws_xpixel = 0, - .ws_ypixel = 0, - }; - - const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); - if (posix.errno(err) == .SUCCESS) - return Winsize{ - .rows = winsize.ws_row, - .cols = winsize.ws_col, - .x_pixel = winsize.ws_xpixel, - .y_pixel = winsize.ws_ypixel, - }; - return error.IoctlError; - } - - pub fn bufferedWriter(self: *const PosixTty) std.io.BufferedWriter(4096, std.io.AnyWriter) { - return std.io.bufferedWriter(self.anyWriter()); - } -}; diff --git a/src/windows/Tty.zig b/src/windows/Tty.zig new file mode 100644 index 0000000..9c956d2 --- /dev/null +++ b/src/windows/Tty.zig @@ -0,0 +1,389 @@ +//! A Windows TTY implementation, using virtual terminal process output and +//! native windows input +const Tty = @This(); + +const std = @import("std"); +const Event = @import("../event.zig").Event; +const Key = @import("../Key.zig"); +const windows = std.os.windows; + +stdin: windows.HANDLE, +stdout: windows.HANDLE, + +initial_codepage: c_uint, +initial_input_mode: u32, +initial_output_mode: u32, + +// a buffer to write key text into +buf: [4]u8 = undefined, + +pub var global_tty: ?Tty = null; + +const utf8_codepage: c_uint = 65001; + +const InputMode = struct { + const enable_window_input: u32 = 0x0008; // resize events + const enable_mouse_input: u32 = 0x0010; + + pub fn rawMode() u32 { + return enable_window_input | enable_mouse_input; + } +}; + +const OutputMode = struct { + const enable_processed_output: u32 = 0x0001; // handle control sequences + const enable_virtual_terminal_processing: u32 = 0x0004; // handle ANSI sequences + const disable_newline_auto_return: u32 = 0x0008; // disable inserting a new line when we write at the last column + const enable_lvb_grid_worldwide: u32 = 0x0010; // enables reverse video and underline + + fn rawMode() u32 { + return enable_processed_output | + enable_virtual_terminal_processing | + disable_newline_auto_return | + enable_lvb_grid_worldwide; + } +}; + +pub fn init() !Tty { + const stdin = try windows.GetStdHandle(windows.STD_INPUT_HANDLE); + const stdout = try windows.GetStdHandle(windows.STD_OUTPUT_HANDLE); + + // get initial modes + var initial_input_mode: windows.DWORD = undefined; + var initial_output_mode: windows.DWORD = undefined; + const initial_output_codepage = windows.kernel32.GetConsoleOutputCP(); + { + if (windows.kernel32.GetConsoleMode(stdin, &initial_input_mode) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); + } + if (windows.kernel32.GetConsoleMode(stdout, &initial_output_mode) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); + } + } + + // set new modes + { + if (SetConsoleMode(stdin, InputMode.rawMode()) == 0) + return windows.unexpectedError(windows.kernel32.GetLastError()); + + if (SetConsoleMode(stdout, OutputMode.rawMode()) == 0) + return windows.unexpectedError(windows.kernel32.GetLastError()); + + if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0) + return windows.unexpectedError(windows.kernel32.GetLastError()); + } + + const self: Tty = .{ + .stdin = stdin, + .stdout = stdout, + .initial_codepage = initial_output_codepage, + .initial_input_mode = initial_input_mode, + .initial_output_mode = initial_output_mode, + }; + + // save a copy of this tty as the global_tty for panic handling + global_tty = self; + + return self; +} + +pub fn deinit(self: Tty) void { + _ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage); + _ = SetConsoleMode(self.stdin, self.initial_input_mode); + _ = SetConsoleMode(self.stdout, self.initial_output_mode); + windows.CloseHandle(self.stdin); + windows.CloseHandle(self.stdout); +} + +pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { + const self: *const Tty = @ptrCast(@alignCast(ptr)); + return windows.WriteFile(self.stdout, bytes, null); +} + +pub fn anyWriter(self: *const Tty) std.io.AnyWriter { + return .{ + .context = self, + .writeFn = Tty.opaqueWrite, + }; +} + +pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWriter) { + return std.io.bufferedWriter(self.anyWriter()); +} + +pub fn nextEvent(self: *Tty) !Event { + // We use a loop so we can ignore certain events + var ansi_buf: [128]u8 = undefined; + var ansi_idx: usize = 0; + var escape_st: bool = false; + while (true) { + var event_count: u32 = 0; + var input_record: INPUT_RECORD = undefined; + if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0) + return windows.unexpectedError(windows.kernel32.GetLastError()); + + switch (input_record.EventType) { + 0x0001 => { // Key event + const event = input_record.Event.KeyEvent; + + const base_layout: u21 = switch (event.wVirtualKeyCode) { + 0x00 => { // delivered when we get an escape sequence + ansi_buf[ansi_idx] = event.uChar.AsciiChar; + ansi_idx += 1; + if (ansi_idx <= 2) { + continue; + } + switch (ansi_buf[1]) { + '[' => { // CSI, read until 0x40 to 0xFF + switch (event.uChar.AsciiChar) { + 0x40...0xFF => { + return .cap_da1; + }, + else => continue, + } + }, + ']' => { // OSC, read until ESC \ or BEL + switch (event.uChar.AsciiChar) { + 0x07 => { + return .cap_da1; + }, + 0x1B => { + escape_st = true; + continue; + }, + '\\' => { + if (escape_st) { + return .cap_da1; + } + continue; + }, + else => continue, + } + }, + else => continue, + } + }, + 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.f8, + 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, + 0xa1 => Key.right_shift, + 0xa2 => Key.left_control, + 0xa3 => Key.right_control, + 0xa4 => 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 => '\\', + 0xdd => ']', + 0xde => '\'', + else => continue, + }; + + 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(cp, &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) { + 0 => return .{ .key_release = key }, + else => return .{ .key_press = key }, + } + }, + 0x0002 => { // TODO: ConPTY doesn't pass through MOUSE_EVENT input records yet. + // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str + }, + 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: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) { + return windows.unexpectedError(windows.kernel32.GetLastError()); + } + const window_rect = console_info.srWindow; + const width = window_rect.Right - window_rect.Left; + const height = window_rect.Bottom - window_rect.Top; + return .{ + .winsize = .{ + .cols = @intCast(width), + .rows = @intCast(height), + .x_pixel = 0, + .y_pixel = 0, + }, + }; + }, + 0x0010 => { // Focus events + switch (input_record.Event.FocusEvent.bSetFocus) { + 0 => return .focus_out, + else => return .focus_in, + } + }, + else => {}, + } + } +} + +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; + + return .{ + .shift = mods & shift > 0, + .alt = mods & alt > 0, + .ctrl = mods & ctrl > 0, + .caps_lock = mods & caps > 0, + .num_lock = mods & num_lock > 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; + +pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL; +// TODO: remove this in zig 0.13.0 +pub extern "kernel32" fn SetConsoleMode(in_hConsoleHandle: windows.HANDLE, in_dwMode: windows.DWORD) callconv(windows.WINAPI) windows.BOOL; diff --git a/src/xev.zig b/src/xev.zig index b03c89c..828623e 100644 --- a/src/xev.zig +++ b/src/xev.zig @@ -1,8 +1,8 @@ const std = @import("std"); const xev = @import("xev"); -const Tty = @import("tty.zig").Tty; -const Winsize = @import("tty.zig").Winsize; +const Tty = @import("main.zig").Tty; +const Winsize = @import("main.zig").Winsize; const Vaxis = @import("Vaxis.zig"); const Parser = @import("Parser.zig"); const Key = @import("Key.zig"); @@ -161,6 +161,7 @@ pub fn TtyWatcher(comptime Userdata: type) type { .paste => |paste| .{ .paste = paste }, .color_report => |report| .{ .color_report = report }, .color_scheme => |scheme| .{ .color_scheme = scheme }, + .winsize => |ws| .{ .winsize = ws }, // capability events which we handle below .cap_kitty_keyboard, @@ -192,6 +193,7 @@ pub fn TtyWatcher(comptime Userdata: type) type { .paste, .color_report, .color_scheme, + .winsize, => unreachable, // handled above .cap_kitty_keyboard => {