From 316b65a0f779329b7469ee3315f8a65031c0761d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 25 Feb 2026 14:57:48 +0100 Subject: [PATCH 1/6] refactor: add support for dotnet test output file links --- src/file_link.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/file_link.zig b/src/file_link.zig index bde12b11..7f9409c2 100644 --- a/src/file_link.zig +++ b/src/file_link.zig @@ -43,6 +43,11 @@ pub fn parse(link: []const u8) error{InvalidFileLink}!Dest { file.path = link; break :blk null; }; + } else if (line_.len > 5 and std.mem.eql(u8, "line ", line_[0..5])) { + file.line = std.fmt.parseInt(usize, line_[5..], 10) catch blk: { + file.path = link; + break :blk null; + }; } else { file.line = std.fmt.parseInt(usize, line_, 10) catch blk: { file.path = link; From 61a509cf2fef82fb164a426c8a3e4668d7f549f7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 25 Feb 2026 16:31:29 +0100 Subject: [PATCH 2/6] refactor(terminal): persist terminal state across terminal view/show operations --- src/tui/terminal_view.zig | 147 +++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 64 deletions(-) diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index 87a39667..d9d123b3 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -25,24 +25,16 @@ const Terminal = vaxis.widgets.Terminal; allocator: Allocator, plane: Plane, -vt: Terminal, -env: std.process.EnvMap, -write_buf: [4096]u8, -pty_pid: ?tp.pid = null, focused: bool = false, -cwd: std.ArrayListUnmanaged(u8) = .empty, -title: std.ArrayListUnmanaged(u8) = .empty, input_mode: Mode, hover: bool = false, +vt: *Vt, pub fn create(allocator: Allocator, parent: Plane) !Widget { return create_with_args(allocator, parent, .{}); } pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Context) !Widget { - const self = try allocator.create(Self); - errdefer allocator.destroy(self); - const container = try WidgetList.createHStyled( allocator, parent, @@ -77,8 +69,6 @@ pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Contex } else { try argv_list.append(allocator, env.get("SHELL") orelse "bash"); } - const argv: []const []const u8 = argv_list.items; - const home = env.get("HOME") orelse "/tmp"; // Use the current plane dimensions for the initial pty size. The plane // starts at 0×0 before the first resize, so use a sensible fallback @@ -86,43 +76,23 @@ pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Contex const cols: u16 = @intCast(@max(80, plane.dim_x())); const rows: u16 = @intCast(@max(24, plane.dim_y())); - // write_buf must outlive the Terminal because the pty writer holds a - // pointer into it. It lives inside Self so the lifetimes match. - self.write_buf = undefined; - const vt = try Terminal.init( - allocator, - argv, - &env, - .{ - .winsize = .{ .rows = rows, .cols = cols, .x_pixel = 0, .y_pixel = 0 }, - .scrollback_size = 0, - .initial_working_directory = blk: { - const project = tp.env.get().str("project"); - break :blk if (project.len > 0) project else home; - }, - }, - &self.write_buf, - ); + if (global_vt == null) try Vt.init(allocator, argv_list.items, env, rows, cols); + + const self = try allocator.create(Self); + errdefer allocator.destroy(self); self.* = .{ .allocator = allocator, .plane = plane, - .vt = vt, - .env = env, - .write_buf = undefined, // managed via self.vt's pty_writer pointer - .pty_pid = null, .input_mode = try keybind.mode("terminal", allocator, .{ .insert_command = "do_nothing" }), + .vt = &global_vt.?, }; - try self.vt.spawn(); - try tui.message_filters().add(MessageFilter.bind(self, receive_filter)); container.ctx = self; try container.add(Widget.to(self)); - self.pty_pid = try pty.spawn(allocator, &self.vt); - return container.widget(); } @@ -173,7 +143,7 @@ pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { .mods = @bitCast(modifiers), .text = if (text.len > 0) text else null, }; - self.vt.update(.{ .key_press = key }) catch |e| + self.vt.vt.update(.{ .key_press = key }) catch |e| std.log.err("terminal_view: input failed: {}", .{e}); tui.need_render(@src()); return true; @@ -195,22 +165,17 @@ pub fn unfocus(self: *Self) void { pub fn deinit(self: *Self, allocator: Allocator) void { if (self.focused) tui.release_keyboard_focus(Widget.to(self)); - self.cwd.deinit(self.allocator); - self.title.deinit(self.allocator); - if (self.pty_pid) |pid| { - pid.send(.{ "pty_actor", "quit" }) catch {}; - pid.deinit(); - self.pty_pid = null; - } - self.vt.deinit(); - self.env.deinit(); + // if (state) |*p| { + // p.deinit(self.allocator); + // state = null; + // } self.plane.deinit(); allocator.destroy(self); } pub fn render(self: *Self, _: *const Widget.Theme) bool { // Drain the vt event queue. - while (self.vt.tryEvent()) |event| { + while (self.vt.vt.tryEvent()) |event| { switch (event) { .exited => |code| { self.show_exit_message(code); @@ -218,18 +183,18 @@ pub fn render(self: *Self, _: *const Widget.Theme) bool { }, .redraw, .bell => {}, .pwd_change => |path| { - self.cwd.clearRetainingCapacity(); - self.cwd.appendSlice(self.allocator, path) catch {}; + self.vt.cwd.clearRetainingCapacity(); + self.vt.cwd.appendSlice(self.allocator, path) catch {}; }, .title_change => |t| { - self.title.clearRetainingCapacity(); - self.title.appendSlice(self.allocator, t) catch {}; + self.vt.title.clearRetainingCapacity(); + self.vt.title.appendSlice(self.allocator, t) catch {}; }, } } // Blit the terminal's front screen into our vaxis.Window. - self.vt.draw(self.allocator, self.plane.window, self.focused) catch |e| { + self.vt.vt.draw(self.allocator, self.plane.window, self.focused) catch |e| { std.log.err("terminal_view: draw failed: {}", .{e}); }; @@ -248,23 +213,13 @@ fn show_exit_message(self: *Self, code: u8) void { w.writeAll("]\x1b[0m\r\n") catch {}; var parser: pty.Parser = .{ .buf = .init(self.allocator) }; defer parser.buf.deinit(); - _ = self.vt.processOutput(&parser, msg.written()) catch {}; + _ = self.vt.vt.processOutput(&parser, msg.written()) catch {}; } pub fn handle_resize(self: *Self, pos: Widget.Box) void { self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; - - const cols: u16 = @intCast(@max(1, pos.w)); - const rows: u16 = @intCast(@max(1, pos.h)); - self.vt.resize(.{ - .rows = rows, - .cols = cols, - .x_pixel = 0, - .y_pixel = 0, - }) catch |e| { - std.log.err("terminal_view: resize failed: {}", .{e}); - }; + self.vt.resize(pos); } fn receive_filter(_: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool { @@ -275,6 +230,70 @@ fn receive_filter(_: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bo return false; } +const Vt = struct { + vt: Terminal, + env: std.process.EnvMap, + write_buf: [4096]u8, + pty_pid: ?tp.pid = null, + cwd: std.ArrayListUnmanaged(u8) = .empty, + title: std.ArrayListUnmanaged(u8) = .empty, + + fn init(allocator: std.mem.Allocator, argv: []const []const u8, env: std.process.EnvMap, rows: u16, cols: u16) !void { + const home = env.get("HOME") orelse "/tmp"; + + global_vt = .{ + .vt = undefined, + .env = env, + .write_buf = undefined, // managed via self.vt's pty_writer pointer + .pty_pid = null, + }; + const self = &global_vt.?; + self.vt = try Terminal.init( + allocator, + argv, + &env, + .{ + .winsize = .{ .rows = rows, .cols = cols, .x_pixel = 0, .y_pixel = 0 }, + .scrollback_size = 0, + .initial_working_directory = blk: { + const project = tp.env.get().str("project"); + break :blk if (project.len > 0) project else home; + }, + }, + &self.write_buf, + ); + + try self.vt.spawn(); + self.pty_pid = try pty.spawn(allocator, &self.vt); + } + + fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + self.cwd.deinit(allocator); + self.title.deinit(allocator); + if (self.pty_pid) |pid| { + pid.send(.{ "pty_actor", "quit" }) catch {}; + pid.deinit(); + self.pty_pid = null; + } + self.vt.deinit(); + self.env.deinit(); + } + + pub fn resize(self: *@This(), pos: Widget.Box) void { + const cols: u16 = @intCast(@max(1, pos.w)); + const rows: u16 = @intCast(@max(1, pos.h)); + self.vt.resize(.{ + .rows = rows, + .cols = cols, + .x_pixel = 0, + .y_pixel = 0, + }) catch |e| { + std.log.err("terminal: resize failed: {}", .{e}); + }; + } +}; +var global_vt: ?Vt = null; + const pty = struct { const Parser = Terminal.Parser; From 69b0885f4b27db30d217c44d05c7fe53afb93528 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 25 Feb 2026 19:12:49 +0100 Subject: [PATCH 3/6] fix(terminal): properly catch child EOF And be much more explicit about error handling. --- build.zig.zon | 4 +-- src/tui/terminal_view.zig | 70 +++++++++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 5ea83ccd..09d3c13a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -30,8 +30,8 @@ .hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#03fe1b123afdaf879829da507f54e42780d4ec65", - .hash = "vaxis-0.5.1-BWNV_EJOCQCOD5tzfVqOgDUSYfvBLrokxE0fr9MSb05q", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#8fba2b2b14174f73a957b3b4b0e1c30f25a78577", + .hash = "vaxis-0.5.1-BWNV_HJQCQAVq-jmKeSh0Ur4DLbkc3DUP-amXI3k22TB", }, .zeit = .{ .url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88", diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index d9d123b3..bbc27467 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -271,7 +271,7 @@ const Vt = struct { self.cwd.deinit(allocator); self.title.deinit(allocator); if (self.pty_pid) |pid| { - pid.send(.{ "pty_actor", "quit" }) catch {}; + pid.send(.{"quit"}) catch {}; pid.deinit(); self.pty_pid = null; } @@ -340,16 +340,20 @@ const pty = struct { errdefer self.deinit(); if (try m.match(.{ "fd", "pty", "read_ready" })) { - try self.read_and_process(); - return; - } - - if (try m.match(.{ "pty_actor", "quit" })) { + self.read_and_process() catch |e| return switch (e) { + error.Terminated => tp.exit_normal(), + error.InputOutput => tp.exit_normal(), + error.SendFailed => tp.exit_normal(), + error.Unexpected => tp.exit_normal(), + }; + } else if (try m.match(.{"quit"})) { return tp.exit_normal(); + } else { + return tp.unexpected(m); } } - fn read_and_process(self: *@This()) tp.result { + fn read_and_process(self: *@This()) error{ Terminated, InputOutput, SendFailed, Unexpected }!void { var buf: [4096]u8 = undefined; while (true) { @@ -359,27 +363,57 @@ const pty = struct { const code = self.vt.cmd.wait(); self.vt.event_queue.push(.{ .exited = code }); self.parent.send(.{ "terminal_view", "output" }) catch {}; - return tp.exit_normal(); + return error.InputOutput; + }, + error.SystemResources, + error.IsDir, + error.OperationAborted, + error.BrokenPipe, + error.ConnectionResetByPeer, + error.ConnectionTimedOut, + error.NotOpenForReading, + error.SocketNotConnected, + error.Canceled, + error.AccessDenied, + error.ProcessNotFound, + error.LockViolation, + error.Unexpected, + => { + std.log.err("terminal_view: read unexpected: {}", .{e}); + return error.Unexpected; }, - else => return tp.exit_error(e, @errorReturnTrace()), }; if (n == 0) { const code = self.vt.cmd.wait(); self.vt.event_queue.push(.{ .exited = code }); self.parent.send(.{ "terminal_view", "output" }) catch {}; - return tp.exit_normal(); + return error.Terminated; } - const exited = self.vt.processOutput(&self.parser, buf[0..n]) catch |e| - return tp.exit_error(e, @errorReturnTrace()); - if (exited) { - self.parent.send(.{ "terminal_view", "output" }) catch {}; - return tp.exit_normal(); + defer self.parent.send(.{ "terminal_view", "output" }) catch {}; + + switch (self.vt.processOutput(&self.parser, buf[0..n]) catch |e| switch (e) { + error.WriteFailed, + error.ReadFailed, + error.OutOfMemory, + error.Utf8InvalidStartByte, + => { + std.log.err("terminal_view: processOutput unexpected: {}", .{e}); + return error.Unexpected; + }, + }) { + .exited => { + return error.Terminated; + }, + .running => {}, } - // Notify parent that new output is available. - self.parent.send(.{ "terminal_view", "output" }) catch {}; } - self.fd.wait_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); + self.fd.wait_read() catch |e| switch (e) { + error.ThespianFileDescriptorWaitReadFailed => { + std.log.err("terminal_view: wait_read unexpected: {}", .{e}); + return error.Unexpected; + }, + }; } }; From 3e265dade5d9b03d1d78d736ac85408a8ac2f43b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 25 Feb 2026 20:33:14 +0100 Subject: [PATCH 4/6] feat(terminal): add scrollback support --- build.zig.zon | 4 ++-- src/tui/terminal_view.zig | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 09d3c13a..83fc7493 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -30,8 +30,8 @@ .hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#8fba2b2b14174f73a957b3b4b0e1c30f25a78577", - .hash = "vaxis-0.5.1-BWNV_HJQCQAVq-jmKeSh0Ur4DLbkc3DUP-amXI3k22TB", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#8561409c5cd0e1799118d188764ac3d6edae6445", + .hash = "vaxis-0.5.1-BWNV_MlsCQBt3YUcMDQoQIhq4I8rS2ElBX9rRjpktJBQ", }, .zeit = .{ .url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88", diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index bbc27467..ba658876 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -113,6 +113,15 @@ pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { .same, .notfound => {}, }; + if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON4), tp.more })) { + if (self.vt.vt.scroll(3)) tui.need_render(@src()); + return true; + } + if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON5), tp.more })) { + if (self.vt.vt.scroll(-3)) tui.need_render(@src()); + return true; + } + if (!(try m.match(.{ "I", tp.more }) // or // try m.match(.{ "B", tp.more }) or @@ -143,6 +152,7 @@ pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { .mods = @bitCast(modifiers), .text = if (text.len > 0) text else null, }; + self.vt.vt.scrollToBottom(); self.vt.vt.update(.{ .key_press = key }) catch |e| std.log.err("terminal_view: input failed: {}", .{e}); tui.need_render(@src()); From f88f7794105b513756313ab1adafbec201613d97 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 25 Feb 2026 20:33:41 +0100 Subject: [PATCH 5/6] refactor(terminal): add scrollback size configuration option --- src/config.zig | 1 + src/tui/terminal_view.zig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config.zig b/src/config.zig index 194a60d7..c4572b00 100644 --- a/src/config.zig +++ b/src/config.zig @@ -12,6 +12,7 @@ gutter_width_minimum: usize = 4, gutter_width_maximum: usize = 8, enable_terminal_cursor: bool = true, enable_terminal_color_scheme: bool = false, +terminal_scrollback_size: u16 = 500, enable_sgr_pixel_mode_support: bool = true, enable_modal_dim: bool = true, highlight_current_line: bool = true, diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index ba658876..1aa8eb73 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -264,7 +264,7 @@ const Vt = struct { &env, .{ .winsize = .{ .rows = rows, .cols = cols, .x_pixel = 0, .y_pixel = 0 }, - .scrollback_size = 0, + .scrollback_size = tui.config().terminal_scrollback_size, .initial_working_directory = blk: { const project = tp.env.get().str("project"); break :blk if (project.len > 0) project else home; From 41576388920ec2bb4ce6310caa30f0a91a49e42b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 25 Feb 2026 20:34:13 +0100 Subject: [PATCH 6/6] refactor(terminal): add force quit keybinding to terminal mode --- src/keybind/builtin/flow.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 37993966..818a41ae 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -600,7 +600,8 @@ ["alt+shift+p", "open_command_palette"], ["alt+x", "open_command_palette"], ["alt+!", "run_task"], - ["alt+f9", "panel_next_widget_style"] + ["alt+f9", "panel_next_widget_style"], + ["ctrl+shift+q", "quit_without_saving"] ] } }