core: close read thread via DSR

Request a device status report to trigger a read. Wait for the thread to
join in the main loop to ensure that the read thread has fully closed.
This should prevent the need for polling and a quit_fd, and a separate
mechanism on macos.
This commit is contained in:
Tim Culverhouse 2024-03-19 13:26:08 -05:00
parent b0944234c6
commit a505d67276
5 changed files with 16 additions and 340 deletions

View file

@ -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,
});

View file

@ -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);
}

View file

@ -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) {

View file

@ -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";

View file

@ -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;
}
}
}