diff --git a/src/tui.zig b/src/tui.zig index 33265b2..243b536 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -417,7 +417,7 @@ pub const Client = struct { defer allocator.free(path); const header = try std.fmt.allocPrint(allocator, "panel {s}", .{path}); defer allocator.free(header); - try appendThemedRow(allocator, &out, header, self.viewport.width, ansi_text); + try appendThemedRow(allocator, &out, header, self.viewport.width, ansi_panel_header); try out.append(allocator, '\n'); body_lines_used += 1; @@ -2589,6 +2589,8 @@ const ansi_syntax_number = "\x1b[38;2;231;168;78;48;2;25;22;33m"; const ansi_diag_error = "\x1b[38;2;238;84;88;48;2;25;22;33m"; const ansi_diag_warning = "\x1b[38;2;231;168;78;48;2;25;22;33m"; const ansi_diag_info = "\x1b[38;2;121;155;224;48;2;25;22;33m"; +const ansi_panel_header = "\x1b[38;2;25;22;33;48;2;116;196;171m"; +const ansi_panel_section = "\x1b[38;2;235;119;84;48;2;25;22;33m"; const ansi_diag_hint = "\x1b[38;2;116;196;171;48;2;25;22;33m"; const ansi_success = "\x1b[38;2;116;196;171;48;2;25;22;33m"; const ansi_stale = "\x1b[38;2;103;93;122;48;2;25;22;33m"; @@ -2786,6 +2788,7 @@ fn panelRowStyle(row: []const u8) []const u8 { if (startsAny(row, &.{ "info:", "diagnostic:info", "stdout:", "job:status:running", "terminal:status:running" })) return ansi_diag_info; if (startsAny(row, &.{ "hint:", "diagnostic:hint" })) return ansi_diag_hint; if (startsAny(row, &.{ "provider:", "lsp-provider:", "language-provider:" })) return ansi_provider; + if (startsAny(row, &.{ "lsp:hover:", "lsp:signature:", "lsp:param:", "pi_ask:", "pi_edit:", "pi_review:" })) return ansi_panel_section; if (startsAny(row, &.{ "missing:", "conflict:", "stale:", "job_status:idle", "terminal_status:idle" })) return ansi_stale; return ansi_text; } @@ -5609,3 +5612,51 @@ test "regular: appended panel rows carry semantic color identity" { try appendPanelRow(std.testing.allocator, &out, "provider:zig:zls:available", 72); try std.testing.expect(std.mem.indexOf(u8, out.items, ansi_provider) != null); } + +test "regular: panel hierarchy uses header and section styles" { + var client = try Client.init(std.testing.allocator, .{ .width = 56, .height = 8 }); + defer client.deinit(); + + try client.handleInput(" "); + try client.handleInput("P"); + const frame = try client.render(std.testing.allocator); + defer std.testing.allocator.free(frame); + + try std.testing.expect(std.mem.indexOf(u8, frame, ansi_panel_header) != null); + try std.testing.expectEqualStrings(ansi_panel_section, panelRowStyle("pi_ask:explain_current_buffer")); + try std.testing.expect(std.mem.indexOf(u8, frame, "panel [pi]") != null); + try std.testing.expect(std.mem.indexOf(u8, frame, "pi_ask:explain_current_buffer") != null); +} + +test "regular: hover and signature rows have readable panel hierarchy styles" { + var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 8 }); + defer client.deinit(); + + const hover_json = "{\"result\":{\"contents\":{\"kind\":\"markdown\",\"value\":\"```zig\\nfn foo() void\\n```\\nsummary\"}}}"; + var hover_line: std.ArrayList(u8) = .empty; + defer hover_line.deinit(std.testing.allocator); + try hover_line.appendSlice(std.testing.allocator, "lsp_hover "); + try hover_line.appendSlice(std.testing.allocator, hover_json); + try client.handleTraceLine(hover_line.items); + { + const frame = try client.render(std.testing.allocator); + defer std.testing.allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, ansi_panel_header) != null); + try std.testing.expect(std.mem.indexOf(u8, frame, "panel [lsp-hover]") != null); + } + + var signature_client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 8 }); + defer signature_client.deinit(); + const signature_rows = [_][]const u8{ + "lsp:signature:foo(a:i32,b:bool)", + "lsp:param:1:b_second", + }; + try signature_client.openListRows("lsp-signature", signature_rows[0..], .none); + { + const frame = try signature_client.render(std.testing.allocator); + defer std.testing.allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, ansi_panel_header) != null); + try std.testing.expect(std.mem.indexOf(u8, frame, "panel [lsp-signature]") != null); + try std.testing.expectEqualStrings(ansi_panel_section, panelRowStyle("lsp:signature:foo(a:i32,b:bool)")); + } +} diff --git a/tools/terminal_e2e.py b/tools/terminal_e2e.py index 7c3c772..18beb62 100755 --- a/tools/terminal_e2e.py +++ b/tools/terminal_e2e.py @@ -111,6 +111,8 @@ def classify_sgr(params: str) -> str: return "current-line" if "48;2;235;119;84" in joined: return "status" + if "48;2;116;196;171" in joined: + return "panel-header" return "" @@ -555,7 +557,7 @@ SCENARIOS = [ Scenario("multi-file-coding-loop", 72, 16, "repo-multifile", "attached-keyboard", "multi-file", "truecolor", "file", setup_multifile, (b"A", b"?", b"\x1b", b" ", b"w", b" ", b"o", b"r", b"e", b"p", b"o", b"/", b"a", b"l", b"p", b"h", b"a", b".", b"z", b"i", b"g", b"\r", b"A", b"!", b"\x1b", b" ", b"w", b" ", b"q"), None, ("alpha.zig", "beta", "☻", "mode:normal"), max_key_events=30, expect_files=(("alpha.zig", "alpha!\n"), ("beta.zig", "beta?"))), Scenario("lsp-assisted-coding", 60, 12, "lsp-fixture", "attached-keyboard", "lsp", "truecolor", "file", setup_lsp_assist, (b" ", b"l", b"h", b" ", b"l", b"s", b" ", b"d", b"q", b" ", b"d", b"n", b" ", b"l", b"f", b" ", b"w", b" ", b"q"), "ZLS", ("[zls] add(lhs, rhs)", "[zls] add(lhs: i32, rhs: i32) active=rhs", "diag:fresh:zls", "format:zls:applied", "ZLS", "mode:normal"), max_key_events=19, prelude=LSP_ASSIST_PRELUDE), Scenario("project-search-panel", 60, 12, "repo-search", "attached-keyboard", "project-search", "truecolor", "file", setup_project_search, (b" ", b"s", b"p", b"f", b"i", b"n", b"d", b"m", b"e", b"\r", b"q", b" ", b"q"), "main\n", ("panel [search]", "target.zig", "findme", "mode:normal"), max_key_events=13), - Scenario("file-and-pi-panels", 60, 12, "repo-multifile", "attached-keyboard", "panels", "truecolor", "file", setup_multifile, (b" ", b"f", b"q", b" ", b"P", b"q", b" ", b"q"), "beta\n", ("panel [files]", "alpha.zig", "panel [pi]", "pi_ask:explain_current_buffer", "mode:normal"), max_key_events=8), + Scenario("file-and-pi-panels", 60, 12, "repo-multifile", "attached-keyboard", "panels", "truecolor", "file", setup_multifile, (b" ", b"f", b"q", b" ", b"P", b"q", b" ", b"q"), "beta\n", ("panel [files]", "alpha.zig", "panel [pi]", "pi_ask:explain_current_buffer", "mode:normal"), required_attrs=("panel-header", "current-line"), max_key_events=8), Scenario("job-cancel-output-panel", 60, 12, "job", "attached-keyboard", "job-output", "truecolor", "file", setup_empty, (b" ", b"t", b"x", b"q", b" ", b"q"), "", ("panel [job-output]", "job:profile:build", "job:status:cancelled:user", "mode:normal"), max_key_events=6), Scenario("dirty-discard-shift-q", 52, 12, "empty", "ios-default-qwertz-space-path", "dirty-discard", "truecolor", "file", setup_empty, tuple(bytes([b]) for b in b" ps Q"), "", ("1│", "☻")), Scenario("directory-panel-narrow", 52, 12, "directory", "ios-default-qwertz-space-path", "panel", "mono", "directory", setup_directory, PANEL_QUIT, None, ("file", "one.zig"), required_attrs=()), @@ -649,6 +651,7 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]: ("48;2;55;65;92", "selection"), ("48;2;20;18;26", "current-line"), ("48;2;235;119;84", "status"), + ("48;2;116;196;171", "panel-header"), ("38;2;235;119;84", "syntax-keyword"), ("38;2;121;155;224", "syntax-builtin"), ("38;2;116;196;171", "syntax-string"),