loop: fully clean up tty when stopping event loop

Previously, stopping the read thread would not fully clean up the tty.
This could result in issues such as #9 where the terminal state was
borked upon return to shell or if temporarily exiting the TUI.

Fixes: #9
This commit is contained in:
Tim Culverhouse 2024-04-30 16:42:10 -05:00
parent 2f83b0d6ca
commit 50242b984b
4 changed files with 83 additions and 66 deletions

View file

@ -82,6 +82,12 @@ pub fn main() !void {
vx.queueRefresh(); vx.queueRefresh();
} else if (key.matches('n', .{ .ctrl = true })) { } else if (key.matches('n', .{ .ctrl = true })) {
try vx.notify("vaxis", "hello from vaxis"); try vx.notify("vaxis", "hello from vaxis");
loop.stop();
var child = std.process.Child.init(&.{"nvim"}, alloc);
_ = try child.spawnAndWait();
try loop.run();
try vx.enterAltScreen();
vx.queueRefresh();
} else if (key.matches(vaxis.Key.enter, .{})) { } else if (key.matches(vaxis.Key.enter, .{})) {
text_input.clearAndFree(); text_input.clearAndFree();
} else { } else {

View file

@ -30,11 +30,17 @@ pub fn Loop(comptime T: type) type {
/// stops reading from the tty and returns it to it's initial state /// stops reading from the tty and returns it to it's initial state
pub fn stop(self: *Self) void { pub fn stop(self: *Self) void {
if (self.vaxis.tty) |*tty| tty.stop(); if (self.vaxis.tty) |*tty| {
// stop the read loop, then join the thread
tty.stop();
if (self.thread) |thread| { if (self.thread) |thread| {
thread.join(); thread.join();
self.thread = null; self.thread = null;
} }
// once thread is closed we can deinit the tty
tty.deinit();
self.vaxis.tty = null;
}
} }
/// returns the next available event, blocking until one is available /// returns the next available event, blocking until one is available

View file

@ -25,6 +25,15 @@ should_quit: bool = false,
buffered_writer: BufferedWriter, buffered_writer: BufferedWriter,
state: struct {
/// if we are in the alt screen
alt_screen: bool = false,
/// if we have entered kitty keyboard
kitty_keyboard: bool = false,
bracketed_paste: bool = false,
mouse: bool = false,
} = .{},
/// initializes a Tty instance by opening /dev/tty and "making it raw" /// initializes a Tty instance by opening /dev/tty and "making it raw"
pub fn init() !Tty { pub fn init() !Tty {
// Open our tty // Open our tty
@ -42,6 +51,21 @@ pub fn init() !Tty {
/// release resources associated with the Tty return it to its original state /// release resources associated with the Tty return it to its original state
pub fn deinit(self: *Tty) void { pub fn deinit(self: *Tty) void {
if (self.state.kitty_keyboard) {
_ = self.write(ctlseqs.csi_u_pop) catch {};
}
if (self.state.mouse) {
_ = self.write(ctlseqs.mouse_reset) catch {};
}
if (self.state.bracketed_paste) {
_ = self.write(ctlseqs.bp_reset) catch {};
}
if (self.state.alt_screen) {
_ = self.write(ctlseqs.rmcup) catch {};
}
// always show the cursor on exit
_ = self.write(ctlseqs.show_cursor) catch {};
self.flush() catch {};
posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| { posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| {
log.err("couldn't restore terminal: {}", .{err}); log.err("couldn't restore terminal: {}", .{err});
}; };

View file

@ -42,15 +42,6 @@ screen: Screen,
/// the next render /// the next render
screen_last: InternalScreen = undefined, screen_last: InternalScreen = undefined,
state: struct {
/// if we are in the alt screen
alt_screen: bool = false,
/// if we have entered kitty keyboard
kitty_keyboard: bool = false,
bracketed_paste: bool = false,
mouse: bool = false,
} = .{},
caps: Capabilities = .{}, caps: Capabilities = .{},
/// if we should redraw the entire screen on the next render /// if we should redraw the entire screen on the next render
@ -86,23 +77,7 @@ pub fn init(alloc: std.mem.Allocator, _: Options) !Vaxis {
/// optional so applications can choose to not free resources when the /// optional so applications can choose to not free resources when the
/// application will be exiting anyways /// application will be exiting anyways
pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void { pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void {
if (self.tty) |_| { if (self.tty) |*tty| {
var tty = &self.tty.?;
if (self.state.kitty_keyboard) {
_ = tty.write(ctlseqs.csi_u_pop) catch {};
}
if (self.state.mouse) {
_ = tty.write(ctlseqs.mouse_reset) catch {};
}
if (self.state.bracketed_paste) {
_ = tty.write(ctlseqs.bp_reset) catch {};
}
if (self.state.alt_screen) {
_ = tty.write(ctlseqs.rmcup) catch {};
}
// always show the cursor on exit
_ = tty.write(ctlseqs.show_cursor) catch {};
tty.flush() catch {};
tty.deinit(); tty.deinit();
} }
if (alloc) |a| { if (alloc) |a| {
@ -147,20 +122,22 @@ pub fn window(self: *Vaxis) Window {
/// enter the alternate screen. The alternate screen will automatically /// enter the alternate screen. The alternate screen will automatically
/// be exited if calling deinit while in the alt screen /// be exited if calling deinit while in the alt screen
pub fn enterAltScreen(self: *Vaxis) !void { pub fn enterAltScreen(self: *Vaxis) !void {
if (self.state.alt_screen) return; if (self.tty) |*tty| {
var tty = self.tty orelse return; if (tty.state.alt_screen) return;
_ = try tty.write(ctlseqs.smcup); _ = try tty.write(ctlseqs.smcup);
try tty.flush(); try tty.flush();
self.state.alt_screen = true; tty.state.alt_screen = true;
}
} }
/// exit the alternate screen /// exit the alternate screen
pub fn exitAltScreen(self: *Vaxis) !void { pub fn exitAltScreen(self: *Vaxis) !void {
if (!self.state.alt_screen) return; if (self.tty) |*tty| {
var tty = self.tty orelse return; if (!tty.state.alt_screen) return;
_ = try tty.write(ctlseqs.rmcup); _ = try tty.write(ctlseqs.rmcup);
try tty.flush(); try tty.flush();
self.state.alt_screen = false; tty.state.alt_screen = false;
}
} }
/// write queries to the terminal to determine capabilities. Individual /// write queries to the terminal to determine capabilities. Individual
@ -487,16 +464,18 @@ pub fn render(self: *Vaxis) !void {
} }
fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void { fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void {
self.state.kitty_keyboard = true; if (self.tty) |*tty| {
const flag_int: u5 = @bitCast(flags); const flag_int: u5 = @bitCast(flags);
try std.fmt.format( try std.fmt.format(
self.tty.?.buffered_writer.writer(), tty.buffered_writer.writer(),
ctlseqs.csi_u_push, ctlseqs.csi_u_push,
.{ .{
flag_int, flag_int,
}, },
); );
try self.tty.?.flush(); try tty.flush();
tty.state.kitty_keyboard = true;
}
} }
/// send a system notification /// send a system notification
@ -533,14 +512,15 @@ pub fn setTitle(self: *Vaxis, title: []const u8) !void {
// beginning and end of a detected paste. All keystrokes between these // beginning and end of a detected paste. All keystrokes between these
// events were pasted // events were pasted
pub fn setBracketedPaste(self: *Vaxis, enable: bool) !void { pub fn setBracketedPaste(self: *Vaxis, enable: bool) !void {
if (self.tty == null) return; if (self.tty) |*tty| {
self.state.bracketed_paste = enable;
const seq = if (enable) const seq = if (enable)
ctlseqs.bp_set ctlseqs.bp_set
else else
ctlseqs.bp_reset; ctlseqs.bp_reset;
_ = try self.tty.?.write(seq); _ = try tty.write(seq);
try self.tty.?.flush(); try tty.flush();
tty.state.bracketed_paste = enable;
}
} }
/// set the mouse shape /// set the mouse shape
@ -550,8 +530,8 @@ pub fn setMouseShape(self: *Vaxis, shape: Shape) void {
/// turn mouse reporting on or off /// turn mouse reporting on or off
pub fn setMouseMode(self: *Vaxis, enable: bool) !void { pub fn setMouseMode(self: *Vaxis, enable: bool) !void {
var tty = self.tty orelse return; if (self.tty) |*tty| {
self.state.mouse = enable; tty.state.mouse = enable;
if (enable) { if (enable) {
_ = try tty.write(ctlseqs.mouse_set); _ = try tty.write(ctlseqs.mouse_set);
try tty.flush(); try tty.flush();
@ -560,6 +540,7 @@ pub fn setMouseMode(self: *Vaxis, enable: bool) !void {
try tty.flush(); try tty.flush();
} }
} }
}
pub fn loadImage( pub fn loadImage(
self: *Vaxis, self: *Vaxis,