From ffa51e17c7b60aa8f3a0ff95b76ac238b88acddd Mon Sep 17 00:00:00 2001 From: slhx agent Date: Sun, 21 Jun 2026 02:36:41 +0200 Subject: [PATCH] Render active panels on narrow terminal --- src/tui.zig | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/tui.zig b/src/tui.zig index f6a1ba2..f092e78 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -93,6 +93,10 @@ pub const Client = struct { if (std.mem.startsWith(u8, line, "key ")) return self.handleTraceKey(line[4..]); if (std.mem.startsWith(u8, line, "type ")) return self.handleInput(line[5..]); + if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line); + if (std.mem.eql(u8, line, "panel_close")) return self.applyProtocol("command panel_close"); + if (std.mem.eql(u8, line, "panel_next")) return self.applyProtocol("command panel_next"); + if (std.mem.eql(u8, line, "panel_prev")) return self.applyProtocol("command panel_prev"); if (std.mem.startsWith(u8, line, "open ")) return self.applyProtocol(line); if (std.mem.startsWith(u8, line, "insert ")) return self.applyProtocolCommand(line); if (std.mem.eql(u8, line, "left")) return self.applyProtocol("command move_left"); @@ -108,6 +112,11 @@ pub const Client = struct { pub fn render(self: *const Client, allocator: std.mem.Allocator) ![]u8 { try self.viewport.validate(); const snap = try self.session.snapshot(); + if (snap.active_panel_title != null) return self.renderPanel(allocator, snap); + return self.renderEditor(allocator, snap); + } + + fn renderEditor(self: *const Client, allocator: std.mem.Allocator, snap: session_mod.Snapshot) ![]u8 { var out = std.ArrayList(u8).empty; errdefer out.deinit(allocator); @@ -151,6 +160,43 @@ pub const Client = struct { return out.toOwnedSlice(allocator); } + fn renderPanel(self: *const Client, allocator: std.mem.Allocator, snap: session_mod.Snapshot) ![]u8 { + var out = std.ArrayList(u8).empty; + errdefer out.deinit(allocator); + const max_body_lines = self.viewport.height - 1; + var body_lines_used: usize = 0; + + const path = try self.session.panelPathAlloc(allocator); + defer allocator.free(path); + const header = try std.fmt.allocPrint(allocator, "panel {s}", .{path}); + defer allocator.free(header); + try appendVisibleCells(allocator, &out, header, self.viewport.width); + try out.append(allocator, '\n'); + body_lines_used += 1; + + if (body_lines_used < max_body_lines) { + const title = snap.active_panel_title.?; + const detail = try std.fmt.allocPrint(allocator, "{s}: no content yet", .{title}); + defer allocator.free(detail); + try appendVisibleCells(allocator, &out, detail, self.viewport.width); + try out.append(allocator, '\n'); + body_lines_used += 1; + } + while (body_lines_used < max_body_lines) : (body_lines_used += 1) { + try out.append(allocator, '~'); + try out.append(allocator, '\n'); + } + + const status = try std.fmt.allocPrint( + allocator, + "panel {d}/{d} x close", + .{ snap.active_panel_index.? + 1, snap.panel_depth }, + ); + defer allocator.free(status); + try appendVisibleCells(allocator, &out, status, self.viewport.width); + return out.toOwnedSlice(allocator); + } + pub fn handleInput(self: *Client, raw: []const u8) !void { if (self.quit) return Error.ClientQuit; const event = input.normalize(raw); @@ -500,3 +546,56 @@ test "adversarial: unknown symbol key does not mutate the buffer" { defer std.testing.allocator.free(frame); try std.testing.expect(std.mem.indexOf(u8, frame, "unknown symbol key") != null); } + +test "regular: narrow terminal renders active panel instead of editor and returns cleanly" { + var client = try Client.init(std.testing.allocator, .{ .width = 20, .height = 4 }); + defer client.deinit(); + + try client.handleTraceLine("open editor-text"); + try client.handleTraceLine("panel_open files"); + const frame = try client.render(std.testing.allocator); + defer std.testing.allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, "panel [files]") != null); + try std.testing.expect(std.mem.indexOf(u8, frame, "editor-text") == null); + try assertLinesFit(frame, 20); + + try client.handleTraceLine("panel_close"); + const editor_frame = try client.render(std.testing.allocator); + defer std.testing.allocator.free(editor_frame); + try std.testing.expect(std.mem.indexOf(u8, editor_frame, "editor-text") != null); + try std.testing.expect(std.mem.indexOf(u8, editor_frame, "panel [files]") == null); + try assertLinesFit(editor_frame, 20); +} + +test "regular: nested panel breadcrumbs render on narrow terminal without overflow" { + var client = try Client.init(std.testing.allocator, .{ .width = 18, .height = 4 }); + defer client.deinit(); + + try client.handleTraceLine("open abc"); + try client.handleTraceLine("panel_open files"); + try client.handleTraceLine("panel_open diagnostics"); + const frame = try client.render(std.testing.allocator); + defer std.testing.allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, "panel files>") != null); + try std.testing.expect(std.mem.indexOf(u8, frame, "[diagnostics]") == null); + try assertLinesFit(frame, 18); + + try client.handleTraceLine("panel_prev"); + const previous_frame = try client.render(std.testing.allocator); + defer std.testing.allocator.free(previous_frame); + try std.testing.expect(std.mem.indexOf(u8, previous_frame, "panel [files]") != null); + try assertLinesFit(previous_frame, 18); +} + +test "adversarial: invalid panel title and empty close recover without changing editor render" { + var client = try Client.init(std.testing.allocator, .{ .width = 16, .height = 4 }); + defer client.deinit(); + + try client.handleTraceLine("open abc"); + try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("panel_open bad title")); + try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("panel_close")); + const frame = try client.render(std.testing.allocator); + defer std.testing.allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, "abc") != null); + try assertLinesFit(frame, 16); +}