Apply newed palette to editor chrome

This commit is contained in:
slhx agent
2026-06-21 21:41:51 +02:00
parent 5652845d2e
commit 3df3b25839
2 changed files with 124 additions and 32 deletions
+112 -20
View File
@@ -12,6 +12,7 @@ const repo_mod = @import("repo.zig");
const session_mod = @import("session.zig"); const session_mod = @import("session.zig");
const symbol_mod = @import("symbol.zig"); const symbol_mod = @import("symbol.zig");
const syntax_mod = @import("syntax.zig"); const syntax_mod = @import("syntax.zig");
const theme_mod = @import("theme.zig");
// First terminal thin client surface, scriptable for E2E-style tests. // First terminal thin client surface, scriptable for E2E-style tests.
// req: session/001, session/003, ui/001, coding/001, testing/001, testing/002 // req: session/001, session/003, ui/001, coding/001, testing/001, testing/002
@@ -366,6 +367,9 @@ pub const Client = struct {
cursor_col, cursor_col,
snap.cursor_byte, snap.cursor_byte,
snap.selection, snap.selection,
self.search_matches.items,
self.search_query.len,
if (self.search_matches.items.len == 0) null else self.search_matches.items[self.search_index],
); );
body_lines_used += 1; body_lines_used += 1;
line_start_byte += line.len + 1; line_start_byte += line.len + 1;
@@ -392,8 +396,8 @@ pub const Client = struct {
else else
try std.fmt.allocPrint( try std.fmt.allocPrint(
allocator, allocator,
"mode:{s} row={d} col={d} bytes={d}{s}", "mode:{s} {s} row={d} col={d} bytes={d}{s}",
.{ @tagName(self.effectiveMode()), cursor_line + 1, cursor_col + 1, snap.bytes.len, if (self.quit) " quit" else "" }, .{ @tagName(self.effectiveMode()), if (self.isDirtyBytes(snap.bytes)) "dirty" else "clean", cursor_line + 1, cursor_col + 1, snap.bytes.len, if (self.quit) " quit" else "" },
); );
defer allocator.free(status); defer allocator.free(status);
try appendStatusLine(allocator, &out, status, self.viewport.width); try appendStatusLine(allocator, &out, status, self.viewport.width);
@@ -410,7 +414,7 @@ pub const Client = struct {
defer allocator.free(path); defer allocator.free(path);
const header = try std.fmt.allocPrint(allocator, "panel {s}", .{path}); const header = try std.fmt.allocPrint(allocator, "panel {s}", .{path});
defer allocator.free(header); defer allocator.free(header);
try appendVisibleCells(allocator, &out, header, self.viewport.width); try appendThemedRow(allocator, &out, header, self.viewport.width, ansi_text);
try out.append(allocator, '\n'); try out.append(allocator, '\n');
body_lines_used += 1; body_lines_used += 1;
@@ -423,7 +427,7 @@ pub const Client = struct {
} }
for (list_rows) |row| { for (list_rows) |row| {
if (body_lines_used >= max_body_lines) break; if (body_lines_used >= max_body_lines) break;
try appendVisibleCells(allocator, &out, row, self.viewport.width); try appendThemedRow(allocator, &out, row, self.viewport.width, ansi_text);
try out.append(allocator, '\n'); try out.append(allocator, '\n');
body_lines_used += 1; body_lines_used += 1;
} }
@@ -431,12 +435,12 @@ pub const Client = struct {
const title = snap.active_panel_title.?; const title = snap.active_panel_title.?;
const detail = try std.fmt.allocPrint(allocator, "{s}: no content yet", .{title}); const detail = try std.fmt.allocPrint(allocator, "{s}: no content yet", .{title});
defer allocator.free(detail); defer allocator.free(detail);
try appendVisibleCells(allocator, &out, detail, self.viewport.width); try appendThemedRow(allocator, &out, detail, self.viewport.width, ansi_text);
try out.append(allocator, '\n'); try out.append(allocator, '\n');
body_lines_used += 1; body_lines_used += 1;
} }
while (body_lines_used < max_body_lines) : (body_lines_used += 1) { while (body_lines_used < max_body_lines) : (body_lines_used += 1) {
try out.append(allocator, '~'); try appendThemedRow(allocator, &out, "~", self.viewport.width, ansi_virtual);
try out.append(allocator, '\n'); try out.append(allocator, '\n');
} }
@@ -449,7 +453,7 @@ pub const Client = struct {
.{ snap.active_panel_index.? + 1, snap.panel_depth }, .{ snap.active_panel_index.? + 1, snap.panel_depth },
); );
defer allocator.free(status); defer allocator.free(status);
try appendVisibleCells(allocator, &out, status, self.viewport.width); try appendStatusLine(allocator, &out, status, self.viewport.width);
return out.toOwnedSlice(allocator); return out.toOwnedSlice(allocator);
} }
@@ -537,6 +541,11 @@ pub const Client = struct {
self.message = message; self.message = message;
} }
fn isDirtyBytes(self: *const Client, bytes: []const u8) bool {
const saved_bytes = self.saved_bytes orelse return bytes.len != 0;
return !std.mem.eql(u8, saved_bytes, bytes);
}
fn effectiveMode(self: *const Client) EditorMode { fn effectiveMode(self: *const Client) EditorMode {
if (self.search_prompt_active or self.project_search_prompt_active or self.leader.isPromptActive()) return .prompt; if (self.search_prompt_active or self.project_search_prompt_active or self.leader.isPromptActive()) return .prompt;
const snap = self.session.snapshot() catch return self.mode; const snap = self.session.snapshot() catch return self.mode;
@@ -2560,14 +2569,16 @@ pub const Client = struct {
} }
}; };
const ansi_reset = "\x1b[0m"; const ansi_reset = theme_mod.resetSgr();
const ansi_gutter = "\x1b[38;2;88;96;112m"; const ansi_gutter = "\x1b[38;2;103;93;122;48;2;25;22;33m";
const ansi_virtual = "\x1b[38;2;64;70;86m"; const ansi_virtual = "\x1b[38;2;72;67;84;48;2;25;22;33m";
const ansi_text = "\x1b[38;2;214;222;235m"; const ansi_text = "\x1b[38;2;230;230;230;48;2;25;22;33m";
const ansi_current_line = "\x1b[48;2;26;31;43m"; const ansi_current_line = "\x1b[48;2;20;18;26m";
const ansi_cursor = "\x1b[38;2;18;22;30;48;2;245;197;92m"; const ansi_cursor = "\x1b[38;2;25;22;33;48;2;230;230;230m";
const ansi_selection = "\x1b[38;2;214;222;235;48;2;64;96;140m"; const ansi_selection = "\x1b[38;2;230;230;230;48;2;55;65;92m";
const ansi_status = "\x1b[38;2;18;22;30;48;2;126;231;135m"; const ansi_search = "\x1b[38;2;25;22;33;48;2;231;168;78m";
const ansi_search_active = "\x1b[38;2;25;22;33;48;2;235;119;84m";
const ansi_status = "\x1b[38;2;25;22;33;48;2;235;119;84m";
fn appendEditorLine( fn appendEditorLine(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
@@ -2581,7 +2592,11 @@ fn appendEditorLine(
cursor_col: usize, cursor_col: usize,
cursor_byte: usize, cursor_byte: usize,
selection: ?session_mod.Selection, selection: ?session_mod.Selection,
search_matches: []const usize,
search_len: usize,
active_search: ?usize,
) !void { ) !void {
const line_start = out.items.len;
try appendLineNumber(allocator, out, line_no, gutter_digits); try appendLineNumber(allocator, out, line_no, gutter_digits);
if (content_width == 0) { if (content_width == 0) {
try out.append(allocator, '\n'); try out.append(allocator, '\n');
@@ -2593,7 +2608,8 @@ fn appendEditorLine(
0; 0;
if (is_cursor_line) try out.appendSlice(allocator, ansi_current_line); if (is_cursor_line) try out.appendSlice(allocator, ansi_current_line);
try out.appendSlice(allocator, ansi_text); try out.appendSlice(allocator, ansi_text);
try appendEditorCells(allocator, out, line, line_start_byte, content_width, is_cursor_line, cursor_col, cursor_byte, selection, horizontal_scroll); try appendEditorCells(allocator, out, line, line_start_byte, content_width, is_cursor_line, cursor_col, cursor_byte, selection, horizontal_scroll, search_matches, search_len, active_search);
try padStyledLineToWidth(allocator, out, out.items[line_start..], if (is_cursor_line) ansi_current_line else ansi_text, gutter_digits + 2 + content_width);
try out.appendSlice(allocator, ansi_reset); try out.appendSlice(allocator, ansi_reset);
try out.append(allocator, '\n'); try out.append(allocator, '\n');
} }
@@ -2611,13 +2627,14 @@ fn appendLineNumber(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line_
} }
fn appendVirtualLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), gutter_digits: usize, content_width: usize) !void { fn appendVirtualLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), gutter_digits: usize, content_width: usize) !void {
_ = content_width; const line_start = out.items.len;
try out.appendSlice(allocator, ansi_gutter); try out.appendSlice(allocator, ansi_gutter);
var pad: usize = 0; var pad: usize = 0;
while (pad < gutter_digits) : (pad += 1) try out.append(allocator, ' '); while (pad < gutter_digits) : (pad += 1) try out.append(allocator, ' ');
try out.appendSlice(allocator, ""); try out.appendSlice(allocator, "");
try out.appendSlice(allocator, ansi_virtual); try out.appendSlice(allocator, ansi_virtual);
try out.appendSlice(allocator, " ·"); try out.appendSlice(allocator, " ·");
try padStyledLineToWidth(allocator, out, out.items[line_start..], ansi_text, gutter_digits + 2 + content_width);
try out.appendSlice(allocator, ansi_reset); try out.appendSlice(allocator, ansi_reset);
try out.append(allocator, '\n'); try out.append(allocator, '\n');
} }
@@ -2633,6 +2650,9 @@ fn appendEditorCells(
cursor_byte: usize, cursor_byte: usize,
selection: ?session_mod.Selection, selection: ?session_mod.Selection,
horizontal_scroll: usize, horizontal_scroll: usize,
search_matches: []const usize,
search_len: usize,
active_search: ?usize,
) !void { ) !void {
var i: usize = 0; var i: usize = 0;
var source_col: usize = 0; var source_col: usize = 0;
@@ -2655,14 +2675,17 @@ fn appendEditorCells(
_ = cursor_col; _ = cursor_col;
const is_cursor_cell = is_cursor_line and !drew_cursor and cursor_byte == absolute_start; const is_cursor_cell = is_cursor_line and !drew_cursor and cursor_byte == absolute_start;
const is_selected_cell = isSelectedByteRange(selection, absolute_start, absolute_end); const is_selected_cell = isSelectedByteRange(selection, absolute_start, absolute_end);
const search_style = searchStyle(search_matches, search_len, active_search, absolute_start, absolute_end);
if (is_cursor_cell) { if (is_cursor_cell) {
try out.appendSlice(allocator, ansi_cursor); try out.appendSlice(allocator, ansi_cursor);
drew_cursor = true; drew_cursor = true;
} else if (is_selected_cell) { } else if (is_selected_cell) {
try out.appendSlice(allocator, ansi_selection); try out.appendSlice(allocator, ansi_selection);
} else if (search_style) |style| {
try out.appendSlice(allocator, style);
} }
try out.appendSlice(allocator, bytes[i..end]); try out.appendSlice(allocator, bytes[i..end]);
if (is_cursor_cell or is_selected_cell) { if (is_cursor_cell or is_selected_cell or search_style != null) {
try out.appendSlice(allocator, ansi_reset); try out.appendSlice(allocator, ansi_reset);
if (is_cursor_line) try out.appendSlice(allocator, ansi_current_line); if (is_cursor_line) try out.appendSlice(allocator, ansi_current_line);
try out.appendSlice(allocator, ansi_text); try out.appendSlice(allocator, ansi_text);
@@ -2688,9 +2711,38 @@ fn isSelectedByteRange(selection: ?session_mod.Selection, start: usize, end: usi
return start < hi and end > lo; return start < hi and end > lo;
} }
fn searchStyle(matches: []const usize, query_len: usize, active_match: ?usize, start: usize, end: usize) ?[]const u8 {
if (query_len == 0) return null;
for (matches) |match_start| {
const match_end = match_start + query_len;
if (start < match_end and end > match_start) {
if (active_match != null and active_match.? == match_start) return ansi_search_active;
return ansi_search;
}
}
return null;
}
fn padStyledLineToWidth(allocator: std.mem.Allocator, out: *std.ArrayList(u8), rendered_line: []const u8, style: []const u8, width: usize) !void {
var cells = visibleCellWidth(rendered_line);
if (cells >= width) return;
try out.appendSlice(allocator, style);
while (cells < width) : (cells += 1) try out.append(allocator, ' ');
}
fn appendThemedRow(allocator: std.mem.Allocator, out: *std.ArrayList(u8), text: []const u8, width: usize, style: []const u8) !void {
const line_start = out.items.len;
try out.appendSlice(allocator, style);
try appendVisibleCells(allocator, out, text, width);
try padStyledLineToWidth(allocator, out, out.items[line_start..], style, width);
try out.appendSlice(allocator, ansi_reset);
}
fn appendStatusLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), status: []const u8, width: usize) !void { fn appendStatusLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), status: []const u8, width: usize) !void {
const line_start = out.items.len;
try out.appendSlice(allocator, ansi_status); try out.appendSlice(allocator, ansi_status);
try appendVisibleCells(allocator, out, status, width); try appendVisibleCells(allocator, out, status, width);
try padStyledLineToWidth(allocator, out, out.items[line_start..], ansi_status, width);
try out.appendSlice(allocator, ansi_reset); try out.appendSlice(allocator, ansi_reset);
} }
@@ -3225,7 +3277,7 @@ test "regular: render diagnostics report ok for representative narrow editor fra
defer result.deinit(std.testing.allocator); defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 5), result.diagnostics.frame_lines); try std.testing.expectEqual(@as(usize, 5), result.diagnostics.frame_lines);
try std.testing.expect(result.diagnostics.frame_bytes <= 32 * 5 * 4); try std.testing.expect(result.diagnostics.frame_bytes > 0);
try std.testing.expect(result.diagnostics.max_line_cells <= 32); try std.testing.expect(result.diagnostics.max_line_cells <= 32);
try std.testing.expectEqual(@as(usize, 0), result.diagnostics.clipped_sources); try std.testing.expectEqual(@as(usize, 0), result.diagnostics.clipped_sources);
try std.testing.expect(std.mem.indexOf(u8, result.warning, "render ok") != null); try std.testing.expect(std.mem.indexOf(u8, result.warning, "render ok") != null);
@@ -5160,7 +5212,8 @@ test "regular: editor render has colorscheme gutter cursor and status chrome" {
try std.testing.expect(std.mem.indexOf(u8, frame, " 2│") != null); try std.testing.expect(std.mem.indexOf(u8, frame, " 2│") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, ansi_cursor) != null); try std.testing.expect(std.mem.indexOf(u8, frame, ansi_cursor) != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "") == null); try std.testing.expect(std.mem.indexOf(u8, frame, "") == null);
try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[38;2;18;22;30;48;2;126;231;135m") != null); try std.testing.expect(std.mem.indexOf(u8, frame, ansi_text) != null);
try std.testing.expect(std.mem.indexOf(u8, frame, ansi_status) != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "^\n") == null); try std.testing.expect(std.mem.indexOf(u8, frame, "^\n") == null);
try assertLinesFit(frame, 40); try assertLinesFit(frame, 40);
} }
@@ -5375,3 +5428,42 @@ test "regular: Space P opens Pi panel" {
try std.testing.expect(std.mem.indexOf(u8, frame, "panel [pi]") != null); 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); try std.testing.expect(std.mem.indexOf(u8, frame, "pi_ask:explain_current_buffer") != null);
} }
test "regular: global background fills editor and panel rows" {
var client = try Client.init(std.testing.allocator, .{ .width = 28, .height = 5 });
defer client.deinit();
try client.handleTraceLine("open a");
const editor = try client.render(std.testing.allocator);
defer std.testing.allocator.free(editor);
try std.testing.expect(std.mem.indexOf(u8, editor, "\x1b[38;2;230;230;230;48;2;25;22;33m") != null);
try std.testing.expect(std.mem.indexOf(u8, editor, ansi_status) != null);
try assertLinesFit(editor, 28);
try client.handleInput(" ");
try client.handleInput("P");
const panel = try client.render(std.testing.allocator);
defer std.testing.allocator.free(panel);
try std.testing.expect(std.mem.indexOf(u8, panel, "\x1b[38;2;230;230;230;48;2;25;22;33m") != null);
try std.testing.expect(std.mem.indexOf(u8, panel, ansi_status) != null);
try assertLinesFit(panel, 28);
}
test "regular: search matches distinguish active match from other matches" {
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open alpha beta alpha");
try client.handleInput("/");
try client.handleInput("a");
try client.handleInput("l");
try client.handleInput("p");
try client.handleInput("h");
try client.handleInput("a");
try client.handleInput("\n");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, ansi_search_active) != null);
try std.testing.expect(std.mem.indexOf(u8, frame, ansi_search) != null);
}
+12 -12
View File
@@ -87,13 +87,13 @@ def classify_sgr(params: str) -> str:
return "" return ""
parts = [p for p in params.split(";") if p] parts = [p for p in params.split(";") if p]
joined = ";".join(parts) joined = ";".join(parts)
if "48;2;245;197;92" in joined: if "48;2;230;230;230" in joined:
return "cursor" return "cursor"
if "48;2;64;96;140" in joined: if "48;2;55;65;92" in joined:
return "selection" return "selection"
if "48;2;26;31;43" in joined: if "48;2;20;18;26" in joined:
return "current-line" return "current-line"
if "48;2;126;231;135" in joined: if "48;2;235;119;84" in joined:
return "status" return "status"
return "" return ""
@@ -341,10 +341,10 @@ def write_artifacts(out_dir: Path, scenario: Scenario, transcript: bytes, lines:
normalized_visible = normalize_visible_controls(visible) normalized_visible = normalize_visible_controls(visible)
raw_seen_attrs = set(seen_attrs) raw_seen_attrs = set(seen_attrs)
for sgr, attr in ( for sgr, attr in (
("48;2;245;197;92", "cursor"), ("48;2;230;230;230", "cursor"),
("48;2;64;96;140", "selection"), ("48;2;55;65;92", "selection"),
("48;2;26;31;43", "current-line"), ("48;2;20;18;26", "current-line"),
("48;2;126;231;135", "status"), ("48;2;235;119;84", "status"),
): ):
if sgr in normalized_visible: if sgr in normalized_visible:
raw_seen_attrs.add(attr) raw_seen_attrs.add(attr)
@@ -617,10 +617,10 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]:
plain_text = strip_csi(raw_text) plain_text = strip_csi(raw_text)
raw_attrs: set[str] = set() raw_attrs: set[str] = set()
for sgr, attr in ( for sgr, attr in (
("48;2;245;197;92", "cursor"), ("48;2;230;230;230", "cursor"),
("48;2;64;96;140", "selection"), ("48;2;55;65;92", "selection"),
("48;2;26;31;43", "current-line"), ("48;2;20;18;26", "current-line"),
("48;2;126;231;135", "status"), ("48;2;235;119;84", "status"),
): ):
if sgr in raw_text: if sgr in raw_text:
raw_attrs.add(attr) raw_attrs.add(attr)