Add generic list panel primitive

This commit is contained in:
slhx agent
2026-06-21 02:45:16 +02:00
parent ffa51e17c7
commit c0a4d1c9bb
6 changed files with 498 additions and 23 deletions
+67 -1
View File
@@ -94,6 +94,12 @@ 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.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
if (std.mem.eql(u8, line, "list_down")) return self.applyProtocol("command list_down");
if (std.mem.eql(u8, line, "list_up")) return self.applyProtocol("command list_up");
if (std.mem.eql(u8, line, "list_select")) return self.applyProtocol("command list_select");
if (std.mem.eql(u8, line, "list_cancel")) return self.applyProtocol("command list_cancel");
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");
@@ -174,7 +180,20 @@ pub const Client = struct {
try out.append(allocator, '\n');
body_lines_used += 1;
if (body_lines_used < max_body_lines) {
const remaining_rows = max_body_lines - body_lines_used;
const rows = self.session.activeListRowsAlloc(allocator, remaining_rows) catch null;
if (rows) |list_rows| {
defer {
for (list_rows) |row| allocator.free(row);
allocator.free(list_rows);
}
for (list_rows) |row| {
if (body_lines_used >= max_body_lines) break;
try appendVisibleCells(allocator, &out, row, self.viewport.width);
try out.append(allocator, '\n');
body_lines_used += 1;
}
} else 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);
@@ -599,3 +618,50 @@ test "adversarial: invalid panel title and empty close recover without changing
try std.testing.expect(std.mem.indexOf(u8, frame, "abc") != null);
try assertLinesFit(frame, 16);
}
test "regular: list panel filters moves selects and renders rows" {
var client = try Client.init(std.testing.allocator, .{ .width = 32, .height = 5 });
defer client.deinit();
try client.handleTraceLine("list_open files src/main.zig|src/panel.zig|README.md");
try client.handleTraceLine("list_filter src");
try client.handleTraceLine("list_down");
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, "> src/panel.zig") != null);
try assertLinesFit(frame, 32);
try client.handleTraceLine("list_select");
const summary = try client.session.panelSummaryAlloc(std.testing.allocator);
defer std.testing.allocator.free(summary);
try std.testing.expectEqualStrings("list:filter=src,cursor=1,visible=2,selected=src/panel.zig", summary);
}
test "regular: list cancel returns to previous editor surface" {
var client = try Client.init(std.testing.allocator, .{ .width = 24, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("list_open files a.zig|b.zig");
try client.handleTraceLine("list_cancel");
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 std.testing.expect(std.mem.indexOf(u8, frame, "panel") == null);
}
test "adversarial: list no-match state and invalid actions recover" {
var client = try Client.init(std.testing.allocator, .{ .width = 24, .height = 4 });
defer client.deinit();
try client.handleTraceLine("list_open files a.zig|b.zig");
try client.handleTraceLine("list_filter none");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "no matches") != null);
try assertLinesFit(frame, 24);
try client.handleTraceLine("list_cancel");
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("list_down"));
}