diff --git a/build.zig b/build.zig index c26bb8f..43acaf6 100644 --- a/build.zig +++ b/build.zig @@ -48,7 +48,7 @@ pub fn build(b: *std.Build) void { const tests_step = b.step("test", "Run tests"); const tests = b.addTest(.{ - .root_source_file = .{ .path = "src/Tty-macos.zig" }, + .root_source_file = .{ .path = "src/main.zig" }, .target = target, .optimize = optimize, }); diff --git a/src/Tty-macos.zig b/src/Tty-macos.zig deleted file mode 100644 index c9ea8f0..0000000 --- a/src/Tty-macos.zig +++ /dev/null @@ -1,313 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const os = std.os; -const Vaxis = @import("vaxis.zig").Vaxis; -const Parser = @import("Parser.zig"); -const GraphemeCache = @import("GraphemeCache.zig"); -const select = @import("select.zig").select; - -const log = std.log.scoped(.tty); - -const Tty = @This(); - -const Writer = std.io.Writer(os.fd_t, os.WriteError, os.write); -const BufferedWriter = std.io.BufferedWriter(4096, Writer); - -/// the original state of the terminal, prior to calling makeRaw -termios: os.termios, - -/// the file descriptor we are using for I/O -fd: std.fs.File, - -/// the write end of a pipe to signal the tty should exit its run loop -quit_fd: ?os.fd_t = null, - -buffered_writer: BufferedWriter, - -/// initializes a Tty instance by opening /dev/tty and "making it raw" -pub fn init() !Tty { - // Open our tty - const fd = try std.fs.cwd().openFile("/dev/tty", .{ - .mode = .read_write, - .allow_ctty = true, -}); - - // Set the termios of the tty - const termios = try makeRaw(fd.handle); - - return Tty{ - .fd = fd, - .termios = termios, - .buffered_writer = std.io.bufferedWriter(Writer{ .context = fd.handle }), - }; -} - -/// release resources associated with the Tty and return it to its original state -pub fn deinit(self: *Tty) void { - os.tcsetattr(self.fd.handle, .FLUSH, self.termios) catch |err| { - log.err("couldn't restore terminal: {}", .{err}); - }; - self.fd.close(); -} - -/// stops the run loop -pub fn stop(self: *Tty) void { - if (self.quit_fd) |fd| { - _ = std.os.write(fd, "q") catch {}; - } -} - -/// read input from the tty -pub fn run( - self: *Tty, - comptime Event: type, - vx: *Vaxis(Event), -) !void { - // create a pipe so we can signal to exit the run loop - const read_end, const write_end = try os.pipe(); - defer os.close(read_end); - defer os.close(write_end); - - // get our initial winsize - const winsize = try getWinsize(self.fd.handle); - if (@hasField(Event, "winsize")) { - vx.postEvent(.{ .winsize = winsize }); - } - - self.quit_fd = write_end; - - // Build a winch handler. We need build this struct to get an anonymous - // function which can post the winsize event - // TODO: more signals, move this outside of this function? - const WinchHandler = struct { - const Self = @This(); - - var vx_winch: *Vaxis(Event) = undefined; - var fd: os.fd_t = undefined; - - fn init(vx_arg: *Vaxis(Event), fd_arg: os.fd_t) !void { - vx_winch = vx_arg; - fd = fd_arg; - var act = os.Sigaction{ - .handler = .{ .handler = Self.handleWinch }, - .mask = switch (builtin.os.tag) { - .macos => 0, - .linux => std.os.empty_sigset, - else => @compileError("os not supported"), - }, - .flags = 0, - }; - - try os.sigaction(os.SIG.WINCH, &act, null); - } - - fn handleWinch(_: c_int) callconv(.C) void { - const ws = getWinsize(fd) catch { - return; - }; - if (@hasField(Event, "winsize")) { - vx_winch.postEvent(.{ .winsize = ws }); - } - } - }; - try WinchHandler.init(vx, self.fd.handle); - - // initialize a grapheme cache - var cache: GraphemeCache = .{}; - - var parser: Parser = .{}; - - // 2kb ought to be more than enough? given that we reset after each call? - var io_buf: [2 * 1024]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&io_buf); - - // set up fds for selecting - var selector = try select(fba.allocator(), enum { tty, quit }, .{ - .tty = self.fd, - .quit = .{ .handle = read_end }, - }); - - // read loop - while (true) { - fba.reset(); - try selector.select(); - - if (selector.fifo(.quit).readableLength() > 0) { - log.debug("quitting read thread", .{}); - return; - } - - const tty = selector.fifo(.tty); - const n = tty.readableLength(); - var start: usize = 0; - defer tty.discard(n); - while (start < n) { - const result = try parser.parse(tty.readableSlice(start)); - start += result.n; - // TODO: if we get 0 byte read, copy the remaining bytes to the - // beginning of the buffer and read mmore? this should only happen - // if we are in the middle of a grapheme at and filled our - // buffer. Probably can happen on large pastes so needs to be - // implemented but low priority - - 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); - } - vx.postEvent(.{ .key_press = mut_key }); - } - }, - .mouse => |mouse| { - if (@hasField(Event, "mouse")) { - vx.postEvent(.{ .mouse = mouse }); - } - }, - .focus_in => { - if (@hasField(Event, "focus_in")) { - vx.postEvent(.focus_in); - } - }, - .focus_out => { - if (@hasField(Event, "focus_out")) { - vx.postEvent(.focus_out); - } - }, - .paste_start => { - if (@hasField(Event, "paste_start")) { - vx.postEvent(.paste_start); - } - }, - .paste_end => { - if (@hasField(Event, "paste_end")) { - vx.postEvent(.paste_end); - } - }, - .cap_kitty_keyboard => { - log.info("kitty keyboard capability detected", .{}); - vx.caps.kitty_keyboard = true; - }, - .cap_kitty_graphics => { - if (!vx.caps.kitty_graphics) { - log.info("kitty graphics capability detected", .{}); - vx.caps.kitty_graphics = true; - } - }, - .cap_rgb => { - log.info("rgb capability detected", .{}); - vx.caps.rgb = true; - }, - .cap_unicode => { - log.info("unicode capability detected", .{}); - vx.caps.unicode = true; - vx.screen.unicode = true; - }, - .cap_da1 => { - std.Thread.Futex.wake(&vx.query_futex, 10); - }, - } - } - } -} - -/// write to the tty. These writes are buffered and require calling flush to -/// flush writes to the tty -pub fn write(self: *Tty, bytes: []const u8) !usize { - return self.buffered_writer.write(bytes); -} - -/// flushes the write buffer to the tty -pub fn flush(self: *Tty) !void { - try self.buffered_writer.flush(); -} - -/// makeRaw enters the raw state for the terminal. -pub fn makeRaw(fd: os.fd_t) !os.termios { - const state = try os.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.iflag.IUTF8 = true; - - 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(std.posix.V.MIN)] = 1; - raw.cc[@intFromEnum(std.posix.V.TIME)] = 0; - try os.tcsetattr(fd, .FLUSH, raw); - - return state; -} - -/// The size of the terminal screen -pub const Winsize = @import("Tty.zig").Winsize; - -fn getWinsize(fd: os.fd_t) !Winsize { - var winsize = os.winsize{ - .ws_row = 0, - .ws_col = 0, - .ws_xpixel = 0, - .ws_ypixel = 0, - }; - - const TIOCGWINSZ = 1074295912; - const err = os.system.ioctl(fd, @as(c_int, TIOCGWINSZ), @intFromPtr(&winsize)); - const e = os.errno(err); - if (e == .SUCCESS) - return Winsize{ - .rows = winsize.ws_row, - .cols = winsize.ws_col, - .x_pixel = winsize.ws_xpixel, - .y_pixel = winsize.ws_ypixel, - }; - return error.IoctlError; -} - -test "run" { - if (true) return error.SkipZigTest; - const TestEvent = union(enum) { - winsize: Winsize, - key_press: @import("Key.zig"), - }; - - var vx = try Vaxis(TestEvent).init(.{}); - defer vx.deinit(null); - var tty = try init(); - defer tty.deinit(); - - const inner = struct { - fn f(t: *Tty) void { - std.time.sleep(std.time.ns_per_s); - t.stop(); - } - }; - - const pid = try std.Thread.spawn(.{}, inner.f, .{&tty}); - defer pid.join(); - - try tty.run(TestEvent, &vx); -} - -test "get winsize" { - const tty = try init(); - _ = try getWinsize(tty.fd); -} diff --git a/src/Tty.zig b/src/Tty.zig index 2c8fa94..53db387 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -4,6 +4,7 @@ const os = std.os; const Vaxis = @import("vaxis.zig").Vaxis; const Parser = @import("Parser.zig"); const GraphemeCache = @import("GraphemeCache.zig"); +const ctlseqs = @import("ctlseqs.zig"); const log = std.log.scoped(.tty); @@ -19,6 +20,7 @@ termios: os.termios, /// The file descriptor we are using for I/O fd: os.fd_t, +should_quit: bool = false, /// the write end of a pipe to signal the tty should exit its run loop quit_fd: ?os.fd_t = null, @@ -49,9 +51,8 @@ pub fn deinit(self: *Tty) void { /// stops the run loop pub fn stop(self: *Tty) void { - if (self.quit_fd) |fd| { - _ = std.os.write(fd, "q") catch {}; - } + self.should_quit = true; + _ = std.os.write(self.fd, ctlseqs.device_status_report) catch {}; } /// read input from the tty @@ -60,10 +61,6 @@ pub fn run( comptime Event: type, vx: *Vaxis(Event), ) !void { - // create a pipe so we can signal to exit the run loop - const pipe = try os.pipe(); - defer os.close(pipe[0]); - defer os.close(pipe[1]); // get our initial winsize const winsize = try getWinsize(self.fd); @@ -71,9 +68,6 @@ pub fn run( vx.postEvent(.{ .winsize = winsize }); } - // assign the write end of the pipe to our quit_fd - self.quit_fd = pipe[1]; - // Build a winch handler. We need build this struct to get an anonymous // function which can post the winsize event // TODO: more signals, move this outside of this function? @@ -113,24 +107,12 @@ pub fn run( // initialize a grapheme cache var cache: GraphemeCache = .{}; - // Set up fds for polling - var pollfds: [2]std.os.pollfd = .{ - .{ .fd = self.fd, .events = std.os.POLL.IN, .revents = undefined }, - .{ .fd = pipe[0], .events = std.os.POLL.IN, .revents = undefined }, - }; - var parser: Parser = .{}; // initialize the read buffer var buf: [1024]u8 = undefined; // read loop - while (true) { - _ = try std.os.poll(&pollfds, -1); - if (pollfds[1].revents & std.os.POLL.IN != 0) { - log.debug("quitting read thread", .{}); - return; - } - + while (!self.should_quit) { const n = try os.read(self.fd, &buf); var start: usize = 0; while (start < n) { diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig index 02d2949..7e4f3d8 100644 --- a/src/ctlseqs.zig +++ b/src/ctlseqs.zig @@ -1,6 +1,7 @@ // Queries pub const primary_device_attrs = "\x1b[c"; pub const tertiary_device_attrs = "\x1b[=c"; +pub const device_status_report = "\x1b[5n"; pub const xtversion = "\x1b[>0q"; pub const decrqm_focus = "\x1b[?1004$p"; pub const decrqm_sync = "\x1b[?2026$p"; diff --git a/src/vaxis.zig b/src/vaxis.zig index 0384b5a..0d593aa 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -5,7 +5,7 @@ const base64 = std.base64.standard.Encoder; const Queue = @import("queue.zig").Queue; const ctlseqs = @import("ctlseqs.zig"); -const Tty = if (builtin.os.tag.isDarwin()) @import("Tty-macos.zig") else @import("Tty.zig"); +const Tty = @import("Tty.zig"); const Winsize = Tty.Winsize; const Key = @import("Key.zig"); const Screen = @import("Screen.zig"); @@ -51,6 +51,8 @@ pub fn Vaxis(comptime T: type) type { tty: ?Tty, + read_thread: ?std.Thread, + /// the screen we write to screen: Screen, /// The last screen we drew. We keep this so we can efficiently update on @@ -91,6 +93,7 @@ pub fn Vaxis(comptime T: type) type { .screen = .{}, .screen_last = .{}, .render_timer = try std.time.Timer.start(), + .read_thread = null, }; } @@ -133,8 +136,7 @@ pub fn Vaxis(comptime T: type) type { pub fn startReadThread(self: *Self) !void { self.tty = try Tty.init(); // run our tty read loop in it's own thread - _ = try std.Thread.spawn(.{}, Tty.run, .{ &self.tty.?, T, self }); - // try read_thread.setName("tty"); + self.read_thread = try std.Thread.spawn(.{}, Tty.run, .{ &self.tty.?, T, self }); } /// stops reading from the tty @@ -142,6 +144,10 @@ pub fn Vaxis(comptime T: type) type { if (self.tty) |_| { var tty = &self.tty.?; tty.stop(); + if (self.read_thread) |thread| { + thread.join(); + self.read_thread = null; + } } }