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:
parent
b0944234c6
commit
a505d67276
5 changed files with 16 additions and 340 deletions
|
@ -48,7 +48,7 @@ pub fn build(b: *std.Build) void {
|
||||||
const tests_step = b.step("test", "Run tests");
|
const tests_step = b.step("test", "Run tests");
|
||||||
|
|
||||||
const tests = b.addTest(.{
|
const tests = b.addTest(.{
|
||||||
.root_source_file = .{ .path = "src/Tty-macos.zig" },
|
.root_source_file = .{ .path = "src/main.zig" },
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
28
src/Tty.zig
28
src/Tty.zig
|
@ -4,6 +4,7 @@ const os = std.os;
|
||||||
const Vaxis = @import("vaxis.zig").Vaxis;
|
const Vaxis = @import("vaxis.zig").Vaxis;
|
||||||
const Parser = @import("Parser.zig");
|
const Parser = @import("Parser.zig");
|
||||||
const GraphemeCache = @import("GraphemeCache.zig");
|
const GraphemeCache = @import("GraphemeCache.zig");
|
||||||
|
const ctlseqs = @import("ctlseqs.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.tty);
|
const log = std.log.scoped(.tty);
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ termios: os.termios,
|
||||||
/// The file descriptor we are using for I/O
|
/// The file descriptor we are using for I/O
|
||||||
fd: os.fd_t,
|
fd: os.fd_t,
|
||||||
|
|
||||||
|
should_quit: bool = false,
|
||||||
/// the write end of a pipe to signal the tty should exit its run loop
|
/// the write end of a pipe to signal the tty should exit its run loop
|
||||||
quit_fd: ?os.fd_t = null,
|
quit_fd: ?os.fd_t = null,
|
||||||
|
|
||||||
|
@ -49,9 +51,8 @@ pub fn deinit(self: *Tty) void {
|
||||||
|
|
||||||
/// stops the run loop
|
/// stops the run loop
|
||||||
pub fn stop(self: *Tty) void {
|
pub fn stop(self: *Tty) void {
|
||||||
if (self.quit_fd) |fd| {
|
self.should_quit = true;
|
||||||
_ = std.os.write(fd, "q") catch {};
|
_ = std.os.write(self.fd, ctlseqs.device_status_report) catch {};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// read input from the tty
|
/// read input from the tty
|
||||||
|
@ -60,10 +61,6 @@ pub fn run(
|
||||||
comptime Event: type,
|
comptime Event: type,
|
||||||
vx: *Vaxis(Event),
|
vx: *Vaxis(Event),
|
||||||
) !void {
|
) !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
|
// get our initial winsize
|
||||||
const winsize = try getWinsize(self.fd);
|
const winsize = try getWinsize(self.fd);
|
||||||
|
@ -71,9 +68,6 @@ pub fn run(
|
||||||
vx.postEvent(.{ .winsize = winsize });
|
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
|
// Build a winch handler. We need build this struct to get an anonymous
|
||||||
// function which can post the winsize event
|
// function which can post the winsize event
|
||||||
// TODO: more signals, move this outside of this function?
|
// TODO: more signals, move this outside of this function?
|
||||||
|
@ -113,24 +107,12 @@ pub fn run(
|
||||||
// initialize a grapheme cache
|
// initialize a grapheme cache
|
||||||
var cache: GraphemeCache = .{};
|
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 = .{};
|
var parser: Parser = .{};
|
||||||
|
|
||||||
// initialize the read buffer
|
// initialize the read buffer
|
||||||
var buf: [1024]u8 = undefined;
|
var buf: [1024]u8 = undefined;
|
||||||
// read loop
|
// read loop
|
||||||
while (true) {
|
while (!self.should_quit) {
|
||||||
_ = try std.os.poll(&pollfds, -1);
|
|
||||||
if (pollfds[1].revents & std.os.POLL.IN != 0) {
|
|
||||||
log.debug("quitting read thread", .{});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = try os.read(self.fd, &buf);
|
const n = try os.read(self.fd, &buf);
|
||||||
var start: usize = 0;
|
var start: usize = 0;
|
||||||
while (start < n) {
|
while (start < n) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Queries
|
// Queries
|
||||||
pub const primary_device_attrs = "\x1b[c";
|
pub const primary_device_attrs = "\x1b[c";
|
||||||
pub const tertiary_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 xtversion = "\x1b[>0q";
|
||||||
pub const decrqm_focus = "\x1b[?1004$p";
|
pub const decrqm_focus = "\x1b[?1004$p";
|
||||||
pub const decrqm_sync = "\x1b[?2026$p";
|
pub const decrqm_sync = "\x1b[?2026$p";
|
||||||
|
|
|
@ -5,7 +5,7 @@ const base64 = std.base64.standard.Encoder;
|
||||||
|
|
||||||
const Queue = @import("queue.zig").Queue;
|
const Queue = @import("queue.zig").Queue;
|
||||||
const ctlseqs = @import("ctlseqs.zig");
|
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 Winsize = Tty.Winsize;
|
||||||
const Key = @import("Key.zig");
|
const Key = @import("Key.zig");
|
||||||
const Screen = @import("Screen.zig");
|
const Screen = @import("Screen.zig");
|
||||||
|
@ -51,6 +51,8 @@ pub fn Vaxis(comptime T: type) type {
|
||||||
|
|
||||||
tty: ?Tty,
|
tty: ?Tty,
|
||||||
|
|
||||||
|
read_thread: ?std.Thread,
|
||||||
|
|
||||||
/// the screen we write to
|
/// the screen we write to
|
||||||
screen: Screen,
|
screen: Screen,
|
||||||
/// The last screen we drew. We keep this so we can efficiently update on
|
/// 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 = .{},
|
||||||
.screen_last = .{},
|
.screen_last = .{},
|
||||||
.render_timer = try std.time.Timer.start(),
|
.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 {
|
pub fn startReadThread(self: *Self) !void {
|
||||||
self.tty = try Tty.init();
|
self.tty = try Tty.init();
|
||||||
// run our tty read loop in it's own thread
|
// run our tty read loop in it's own thread
|
||||||
_ = try std.Thread.spawn(.{}, Tty.run, .{ &self.tty.?, T, self });
|
self.read_thread = try std.Thread.spawn(.{}, Tty.run, .{ &self.tty.?, T, self });
|
||||||
// try read_thread.setName("tty");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// stops reading from the tty
|
/// stops reading from the tty
|
||||||
|
@ -142,6 +144,10 @@ pub fn Vaxis(comptime T: type) type {
|
||||||
if (self.tty) |_| {
|
if (self.tty) |_| {
|
||||||
var tty = &self.tty.?;
|
var tty = &self.tty.?;
|
||||||
tty.stop();
|
tty.stop();
|
||||||
|
if (self.read_thread) |thread| {
|
||||||
|
thread.join();
|
||||||
|
self.read_thread = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue