diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 13:33:54 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 1d19d0ec24b924d1d2ccf2cab25199c6e6865fd7 (patch) | |
| tree | 27ba58d9d0cd8eed3adf808b35242aed7ac28fa3 | |
| parent | 2bdde8c5ec1049ea0f575c6f9165ed4cef00ea50 (diff) | |
| download | pardes-1d19d0ec24b924d1d2ccf2cab25199c6e6865fd7.tar.gz pardes-1d19d0ec24b924d1d2ccf2cab25199c6e6865fd7.zip | |
Tint pane tag filenames with an optional theme color
| -rw-r--r-- | docs/config.md | 9 | ||||
| -rw-r--r-- | docs/themes.md | 5 | ||||
| -rw-r--r-- | src/pardes.zig | 39 | ||||
| -rw-r--r-- | src/themes/atelier.zig | 1 | ||||
| -rw-r--r-- | src/themes/clay.zig | 1 | ||||
| -rw-r--r-- | src/themes/daybreak.zig | 1 | ||||
| -rw-r--r-- | src/themes/dusk.zig | 1 | ||||
| -rw-r--r-- | src/themes/forge.zig | 1 | ||||
| -rw-r--r-- | src/themes/forge_black.zig | 1 | ||||
| -rw-r--r-- | src/themes/forge_soft.zig | 1 | ||||
| -rw-r--r-- | src/themes/harvest.zig | 1 | ||||
| -rw-r--r-- | src/themes/ink.zig | 1 | ||||
| -rw-r--r-- | src/themes/lagoon.zig | 1 | ||||
| -rw-r--r-- | src/themes/orchard.zig | 1 | ||||
| -rw-r--r-- | src/themes/orchard_black.zig | 1 | ||||
| -rw-r--r-- | src/themes/paper.zig | 1 | ||||
| -rw-r--r-- | src/themes/solarium.zig | 1 | ||||
| -rw-r--r-- | src/themes/spectrum.zig | 1 | ||||
| -rw-r--r-- | test/panes.zig | 117 |
19 files changed, 183 insertions, 2 deletions
diff --git a/docs/config.md b/docs/config.md index e8c25ceb..899d4cc4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -254,8 +254,8 @@ format, and it is `pardes.Theme` serialised by `std.zon.stringify`: the theme name, thirteen required RGB roles (`sel_bg`, `sel_fg`, `tag_bg`, `tag_fg`, `box`, `box_dim`, `kw`, `str`, `num`, `comment`, `lineno`, `scroll_track`, `scroll_thumb`), nullable `bg`/`fg`, and either a 16-color RGB `palette` or -`null`. Nine optional nullable RGB roles extend this format: -`tag_active_bg`, `tag_active_fg`, `border`, `search_bg`, `search_fg`, +`null`. Optional nullable RGB roles extend this format: +`tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `border`, `lineno_active`, `search_bg`, `search_fg`, `diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, and `diagnostic_hint`. Missing new roles use backward-compatible defaults, so previously exported files remain valid. [Theme customization](themes.md) @@ -263,6 +263,11 @@ describes each role and its fallback. RGB values are three-byte arrays, and hex literals are accepted. The original fields remain required; there is no inheritance or partial override layer. +`tag_name_fg` gives the filename at the end of a pane's path a separate +foreground on both normal and active tags. When omitted or `null`, it uses +the corresponding tag foreground. Directory text and tag commands retain +their regular colors; selecting filename text uses the selection colors. + `Filter` in a terminal's tag projects that pane's ANSI colors through the active theme, and does it in two stages. The default foreground and background roles are mapped FIRST, because ghostty-vt generates the whole 256-color diff --git a/docs/themes.md b/docs/themes.md index 7d71d0dd..d2cad95b 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -37,6 +37,10 @@ tints. Active pane and column tags use a restrained tone-on-tone shift, not a light/dark inversion: dark palettes keep dark tags, and light palettes keep light tags. A slight text lift and the focus marker retain the cue without turning the entire command strip into a highlight. +The filename at the end of a pane's path uses a subtle accent tint, mixed +from the tag text and each palette's accent. The directory and commands keep +their usual foreground. Filename text meets the same contrast threshold on +both normal and active tag backgrounds. Comments are colorful first-class text, while line numbers stay quiet; the current line number gets a small color emphasis and bold weight, never a bright tag-colored block. These are appearance changes only; @@ -98,6 +102,7 @@ so existing exported themes remain valid. | Optional role | Purpose | When absent | | --- | --- | --- | | `tag_active_bg`, `tag_active_fg` | Focused pane tag surface and text | `tag_bg`, `tag_fg` | +| `tag_name_fg` | Filename at the end of the pane's path, on normal and active tags | The corresponding normal or active tag foreground | | `border` | Quiet separators | `scroll_track` | | `lineno_active` | Restrained current line number foreground | `lineno` | | `search_bg`, `search_fg` | Search matches, independent of selection | `sel_bg`, `sel_fg` | diff --git a/src/pardes.zig b/src/pardes.zig index 537b212a..3c0205d2 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2894,6 +2894,8 @@ pub const Theme = struct { // editor palettes keep working; ChromeTheme resolves their legacy defaults. tag_active_bg: ?[3]u8 = null, tag_active_fg: ?[3]u8 = null, + /// Quiet filename/output-name accent; directories and commands keep tag ink. + tag_name_fg: ?[3]u8 = null, border: ?[3]u8 = null, search_bg: ?[3]u8 = null, search_fg: ?[3]u8 = null, @@ -2987,6 +2989,9 @@ test "native themes keep text and UI roles readable on their own surfaces" { try std.testing.expect(themeContrast(th.comment, th.bg.?) >= themeContrast(th.lineno, th.bg.?) + 2); try std.testing.expect(themeContrast(th.tag_fg, th.tag_bg) >= minimum); try std.testing.expect(themeContrast(chrome.tag_active_fg, chrome.tag_active_bg) >= minimum); + try std.testing.expect(th.tag_name_fg != null); + try std.testing.expect(themeContrast(chrome.tag_name_fg, th.tag_bg) >= minimum); + try std.testing.expect(themeContrast(chrome.tag_active_name_fg, chrome.tag_active_bg) >= minimum); // Focus stays on the same tonal surface, rather than inverting a // whole command strip. Text remains readable in both states. try std.testing.expect(themeContrast(chrome.tag_active_bg, th.tag_bg) <= 1.6); @@ -3071,9 +3076,12 @@ test "legacy ThemeFile documents inherit Pardes UI roles without new fields" { const parsed = try std.zon.parse.fromSliceAlloc(Theme, std.testing.allocator, source, null, .{}); defer std.zon.parse.free(std.testing.allocator, parsed); try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_active_bg); + try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_name_fg); try std.testing.expectEqual(@as(?[3]u8, null), parsed.search_bg); const chrome = ChromeTheme.fromTheme(&parsed); try std.testing.expectEqual(parsed.tag_bg, chrome.tag_active_bg); + try std.testing.expectEqual(parsed.tag_fg, chrome.tag_name_fg); + try std.testing.expectEqual(chrome.tag_active_fg, chrome.tag_active_name_fg); try std.testing.expectEqual(parsed.sel_bg, chrome.search_bg); try std.testing.expectEqual(parsed.fg.?, chrome.diagnostic_error); } @@ -3134,6 +3142,8 @@ pub const ChromeTheme = struct { tag_fg: [3]u8, tag_active_bg: [3]u8, tag_active_fg: [3]u8, + tag_name_fg: [3]u8, + tag_active_name_fg: [3]u8, border: [3]u8, search_bg: [3]u8, search_fg: [3]u8, @@ -3153,6 +3163,8 @@ pub const ChromeTheme = struct { .tag_fg = th.tag_fg, .tag_active_bg = th.tag_active_bg orelse th.tag_bg, .tag_active_fg = th.tag_active_fg orelse th.tag_fg, + .tag_name_fg = th.tag_name_fg orelse th.tag_fg, + .tag_active_name_fg = th.tag_name_fg orelse (th.tag_active_fg orelse th.tag_fg), .border = th.border orelse th.scroll_track, .search_bg = th.search_bg orelse th.sel_bg, .search_fg = th.search_fg orelse th.sel_fg, @@ -7090,6 +7102,22 @@ pub const Pardes = struct { return out; } + fn tagNameRange(pane: *const Pane, prefix_len: usize) ?struct { lo: usize, hi: usize } { + var start: usize = 0; + const name = if (pane.file) |file| + if (pane.tag_name_len) |len| pane.tag_name[0..len] else file.path + else if (pane.image) |state| blk: { + // Image tags put renderer controls before the path. + start = prefix_len -| state.path.len; + break :blk state.path; + } else if (comptime pdf_enabled) blk: { + break :blk if (pane.pdf) |state| state.path else return null; + } else return null; + const basename = if (std.mem.lastIndexOfScalar(u8, name, '/')) |slash| slash + 1 else 0; + if (basename == name.len) return null; + return .{ .lo = start + basename, .hi = start + name.len }; + } + pub fn seedTail(_: *Pardes, pane: *Pane) void { if (pane.tag_init) return; const tail = curTail(pane); @@ -13094,6 +13122,17 @@ pub const Pardes = struct { tag.len; const tag_start = @min(tag_cut, panes.File.rawAtDisplay(tag, pane.tag_scroll)); _ = s.print(tx, tag_y, tw, tag[tag_start..tag_cut], tag_style); + if (tagNameRange(pane, tag.len - curTail(pane).len)) |name| { + const lo = @max(name.lo, tag_start); + const hi = @min(name.hi, tag_cut); + if (lo < hi) { + var col = panes.File.rawDisplayCol(tag, lo) -| pane.tag_scroll; + const end = panes.File.rawDisplayCol(tag, hi) -| pane.tag_scroll; + const name_fg = if (active and p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg; + while (col < end and col < tw) : (col += 1) + s.at(tx + @as(u16, @intCast(col)), tag_y).style.fg = .{ .rgb = name_fg }; + } + } if (p.look_hover_preview) |preview| { if (preview.pane == id and preview.serial == pane.serial) if (preview.sel) |sel| if (@min(sel.r0, sel.r1) < BOX_H) { const preview_bg = mix(page_rgb, mix(page_rgb, th.sel_bg)); diff --git a/src/themes/atelier.zig b/src/themes/atelier.zig index 8b45623b..ba1ca0cf 100644 --- a/src/themes/atelier.zig +++ b/src/themes/atelier.zig @@ -7,6 +7,7 @@ pub const theme = .{ .fg = .{ 0x29, 0x3f, 0x35 }, .tag_bg = .{ 0xe4, 0xf0, 0xed }, .tag_fg = .{ 0x3d, 0x60, 0x55 }, + .tag_name_fg = .{ 0x3d, 0x66, 0x5a }, .tag_active_bg = .{ 0xd3, 0xe7, 0xdf }, .tag_active_fg = .{ 0x35, 0x58, 0x4d }, .box = .{ 0x3e, 0x7a, 0x68 }, diff --git a/src/themes/clay.zig b/src/themes/clay.zig index bf9e7027..80a689d8 100644 --- a/src/themes/clay.zig +++ b/src/themes/clay.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xf3, 0xe5, 0xbc }, .tag_bg = .{ 0x34, 0x3f, 0x36 }, .tag_fg = .{ 0xd7, 0xd6, 0xb6 }, + .tag_name_fg = .{ 0xc5, 0xd0, 0xa8 }, .tag_active_bg = .{ 0x42, 0x4f, 0x40 }, .tag_active_fg = .{ 0xe3, 0xe2, 0xc2 }, .box = .{ 0x8e, 0xc0, 0x7c }, diff --git a/src/themes/daybreak.zig b/src/themes/daybreak.zig index 28c0ac0a..07fc981b 100644 --- a/src/themes/daybreak.zig +++ b/src/themes/daybreak.zig @@ -7,6 +7,7 @@ pub const theme = .{ .fg = .{ 0x14, 0x2d, 0x40 }, .tag_bg = .{ 0xed, 0xf2, 0xf7 }, .tag_fg = .{ 0x29, 0x46, 0x5e }, + .tag_name_fg = .{ 0x20, 0x4c, 0x6b }, .tag_active_bg = .{ 0xdf, 0xea, 0xf3 }, .tag_active_fg = .{ 0x21, 0x3e, 0x56 }, .box = .{ 0x07, 0x5f, 0x91 }, diff --git a/src/themes/dusk.zig b/src/themes/dusk.zig index e4ba012b..f3a7bcb6 100644 --- a/src/themes/dusk.zig +++ b/src/themes/dusk.zig @@ -7,6 +7,7 @@ pub const theme = .{ .fg = .{ 0xc3, 0xb6, 0xc1 }, .tag_bg = .{ 0x2c, 0x26, 0x30 }, .tag_fg = .{ 0xc6, 0xb6, 0xc5 }, + .tag_name_fg = .{ 0xce, 0xb5, 0xba }, .tag_active_bg = .{ 0x3b, 0x2f, 0x3b }, .tag_active_fg = .{ 0xd2, 0xc2, 0xd1 }, .box = .{ 0xe4, 0xb3, 0x9b }, diff --git a/src/themes/forge.zig b/src/themes/forge.zig index ee23640d..0d8c58d0 100644 --- a/src/themes/forge.zig +++ b/src/themes/forge.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xe5, 0xe5, 0xe5 }, .tag_bg = .{ 0x22, 0x25, 0x2b }, .tag_fg = .{ 0xc5, 0xc9, 0xd2 }, + .tag_name_fg = .{ 0xbd, 0xc8, 0xd9 }, .tag_active_bg = .{ 0x2d, 0x34, 0x40 }, .tag_active_fg = .{ 0xd1, 0xd5, 0xde }, .box = .{ 0xa5, 0xc4, 0xee }, diff --git a/src/themes/forge_black.zig b/src/themes/forge_black.zig index d9605e97..16c9700a 100644 --- a/src/themes/forge_black.zig +++ b/src/themes/forge_black.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xf6, 0xf6, 0xf6 }, .tag_bg = .{ 0x22, 0x25, 0x2b }, .tag_fg = .{ 0xc5, 0xc9, 0xd2 }, + .tag_name_fg = .{ 0xbd, 0xc8, 0xd9 }, .tag_active_bg = .{ 0x2d, 0x34, 0x40 }, .tag_active_fg = .{ 0xd1, 0xd5, 0xde }, .box = .{ 0xa5, 0xc4, 0xee }, diff --git a/src/themes/forge_soft.zig b/src/themes/forge_soft.zig index 0d882fbb..97ed5b23 100644 --- a/src/themes/forge_soft.zig +++ b/src/themes/forge_soft.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xbf, 0xc3, 0xcc }, .tag_bg = .{ 0x2a, 0x2d, 0x34 }, .tag_fg = .{ 0xc5, 0xc9, 0xd2 }, + .tag_name_fg = .{ 0xbd, 0xc8, 0xd9 }, .tag_active_bg = .{ 0x37, 0x3e, 0x49 }, .tag_active_fg = .{ 0xd1, 0xd5, 0xde }, .box = .{ 0xa5, 0xc4, 0xee }, diff --git a/src/themes/harvest.zig b/src/themes/harvest.zig index 0536ffa8..d22e2a52 100644 --- a/src/themes/harvest.zig +++ b/src/themes/harvest.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xf3, 0xf2, 0xcc }, .tag_bg = .{ 0x34, 0x3b, 0x2e }, .tag_fg = .{ 0xd5, 0xd8, 0xb8 }, + .tag_name_fg = .{ 0xd4, 0xd0, 0xad }, .tag_active_bg = .{ 0x42, 0x4d, 0x38 }, .tag_active_fg = .{ 0xe1, 0xe4, 0xc4 }, .box = .{ 0xcf, 0xba, 0x8b }, diff --git a/src/themes/ink.zig b/src/themes/ink.zig index d34f8fae..71dbd328 100644 --- a/src/themes/ink.zig +++ b/src/themes/ink.zig @@ -7,6 +7,7 @@ pub const theme = .{ .fg = .{ 0xf3, 0xf7, 0xfc }, .tag_bg = .{ 0x15, 0x21, 0x2c }, .tag_fg = .{ 0xd5, 0xe4, 0xf1 }, + .tag_name_fg = .{ 0xc1, 0xe1, 0xf4 }, .tag_active_bg = .{ 0x22, 0x33, 0x42 }, .tag_active_fg = .{ 0xe1, 0xee, 0xf9 }, .box = .{ 0x86, 0xd8, 0xff }, diff --git a/src/themes/lagoon.zig b/src/themes/lagoon.zig index cc444ff1..33411f88 100644 --- a/src/themes/lagoon.zig +++ b/src/themes/lagoon.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xba, 0xc9, 0xce }, .tag_bg = .{ 0x29, 0x44, 0x3f }, .tag_fg = .{ 0xbb, 0xd7, 0xcb }, + .tag_name_fg = .{ 0xae, 0xd3, 0xc5 }, .tag_active_bg = .{ 0x34, 0x57, 0x4d }, .tag_active_fg = .{ 0xc7, 0xe3, 0xd7 }, .box = .{ 0x89, 0xc7, 0xb4 }, diff --git a/src/themes/orchard.zig b/src/themes/orchard.zig index 17ac801a..2ed5c14d 100644 --- a/src/themes/orchard.zig +++ b/src/themes/orchard.zig @@ -7,6 +7,7 @@ pub const theme = .{ .fg = .{ 0xe5, 0xee, 0xe5 }, .tag_bg = .{ 0x1d, 0x28, 0x24 }, .tag_fg = .{ 0xb2, 0xc3, 0xb8 }, + .tag_name_fg = .{ 0xb1, 0xc5, 0xae }, .tag_active_bg = .{ 0x26, 0x36, 0x2d }, .tag_active_fg = .{ 0xbe, 0xcf, 0xc4 }, .box = .{ 0xad, 0xcc, 0x91 }, diff --git a/src/themes/orchard_black.zig b/src/themes/orchard_black.zig index bf53c66a..e3b2ef27 100644 --- a/src/themes/orchard_black.zig +++ b/src/themes/orchard_black.zig @@ -7,6 +7,7 @@ pub const theme = .{ .fg = .{ 0xf2, 0xf8, 0xef }, .tag_bg = .{ 0x1d, 0x28, 0x24 }, .tag_fg = .{ 0xb2, 0xc3, 0xb8 }, + .tag_name_fg = .{ 0xb1, 0xc5, 0xae }, .tag_active_bg = .{ 0x26, 0x36, 0x2d }, .tag_active_fg = .{ 0xbe, 0xcf, 0xc4 }, .box = .{ 0xad, 0xcc, 0x91 }, diff --git a/src/themes/paper.zig b/src/themes/paper.zig index 7bfb891d..bcb3a513 100644 --- a/src/themes/paper.zig +++ b/src/themes/paper.zig @@ -7,6 +7,7 @@ pub const theme = .{ .fg = .{ 0x30, 0x3b, 0x3b }, .tag_bg = .{ 0xe8, 0xea, 0xe3 }, .tag_fg = .{ 0x52, 0x62, 0x5a }, + .tag_name_fg = .{ 0x4c, 0x64, 0x59 }, .tag_active_bg = .{ 0xdc, 0xe5, 0xdc }, .tag_active_fg = .{ 0x4a, 0x5a, 0x52 }, .box = .{ 0x3c, 0x6b, 0x56 }, diff --git a/src/themes/solarium.zig b/src/themes/solarium.zig index 84866830..9a7e7fab 100644 --- a/src/themes/solarium.zig +++ b/src/themes/solarium.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xa7, 0xb7, 0xae }, .tag_bg = .{ 0x12, 0x3f, 0x43 }, .tag_fg = .{ 0xc7, 0xd4, 0xbd }, + .tag_name_fg = .{ 0xca, 0xce, 0xaa }, .tag_active_bg = .{ 0x21, 0x4e, 0x50 }, .tag_active_fg = .{ 0xd3, 0xe0, 0xc9 }, .box = .{ 0xd5, 0xbc, 0x72 }, diff --git a/src/themes/spectrum.zig b/src/themes/spectrum.zig index 8c43fada..70645771 100644 --- a/src/themes/spectrum.zig +++ b/src/themes/spectrum.zig @@ -8,6 +8,7 @@ pub const theme = .{ .fg = .{ 0xfc, 0xfc, 0xfa }, .tag_bg = .{ 0x3b, 0x38, 0x30 }, .tag_fg = .{ 0xdd, 0xd8, 0xbb }, + .tag_name_fg = .{ 0xe6, 0xd8, 0xa6 }, .tag_active_bg = .{ 0x4d, 0x47, 0x36 }, .tag_active_fg = .{ 0xe9, 0xe4, 0xc7 }, .box = .{ 0xff, 0xd8, 0x66 }, diff --git a/test/panes.zig b/test/panes.zig index 97dc3927..a7e59923 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -518,6 +518,122 @@ const JumpSelectionTests = struct { } }; +const TagNameTintTests = struct { + const name_fg = [3]u8{ 231, 137, 43 }; + const tag_fg = [3]u8{ 111, 122, 133 }; + + fn configure(p: *Pardes) void { + var theme = p.theme().*; + theme.tag_name_fg = name_fg; + theme.tag_fg = tag_fg; + p.custom_theme = theme; + p.chrome_animation.snap(pardes.ChromeTheme.fromTheme(p.theme())); + p.settings.focus_tint = false; + p.presentation.enabled = false; + } + + fn check(p: *Pardes, id: usize, name: []const u8) !usize { + const pane = p.panes[id].?; + var arena = std.heap.ArenaAllocator.init(p.gpa); + defer arena.deinit(); + const surface = try p.render(arena.allocator()); + const text = try p.tagText(arena.allocator(), pane); + const base = std.mem.lastIndexOfScalar(u8, name, '/') orelse return error.MissingDirectory; + const rect = p.rects[id]; + const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; + const left = rect.x + config.GUTTER; + var visible_name: usize = 0; + var at: usize = 0; + while (at < text.len) : (at = modal.nextGrapheme(text, at)) { + const start: i32 = @as(i32, @intCast(panes.File.rawDisplayCol(text, at))) - pane.tag_scroll; + const end = start + @as(i32, @intCast(panes.File.displayWidth(text[at..modal.nextGrapheme(text, at)]))); + var col = @max(0, start); + while (col < end and col < rect.w - config.GUTTER) : (col += 1) { + const in_name = at > base and at < name.len; + const fg = if (in_name) name_fg else tag_fg; + try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg); + if (in_name) visible_name += 1; + } + } + return visible_name; + } + + test "tag filename tint excludes directories dirty marker and output commands" { + for ([_]bool{ false, true }) |bottom| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 24 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + p.gpa.free(pane.file.?.path); + pane.file.?.path = try p.gpa.dupe(u8, "/project/็file.zig"); + pane.file.?.revision += 1; + configure(p); + defer p.custom_theme = null; + p.settings.tag_bottom = bottom; + try std.testing.expect((try check(p, 0, pane.file.?.path)) > 0); + try panes.Output.fillResults(p, 0, "/project", .search, "tint", try p.gpa.dupe(u8, "no matches\n"), null); + const id = pane.search_pane.?; + p.update(.tick); + try std.testing.expect((try check(p, id, p.panes[id].?.file.?.path)) > 0); + } + } + + test "tag filename tint follows scrolled Unicode staged names and yields to selection" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 38, .rows = 14 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + const old = pane.file.?.path.len; + configure(p); + defer p.custom_theme = null; + p.update(.{ .key = .{ .cp = ':' } }); + p.update(.{ .key = .{ .cp = 'i' } }); + pane.tag_anchor = 0; + pane.tag_col = @intCast(old - 1); + pane.tag_sel = true; + const name = "/a/very/long/directory/that/extends/past/the/tag/็e\u{301}name-with-a-long-tail.zig"; + p.update(.{ .paste = name }); + try std.testing.expectEqualStrings(name, pane.tag_name[0..pane.tag_name_len.?]); + pane.tag_col = @intCast(std.mem.indexOf(u8, name, "็").?); + pane.tag_sel = false; + try std.testing.expect((try check(p, 0, name)) > 0); + try std.testing.expect(pane.tag_scroll > 0); + pane.tag_anchor = pane.tag_col; + pane.tag_sel = true; + pane.tag_scroll += 4; // bring both cells of the selected wide glyph into view + var arena = std.heap.ArenaAllocator.init(p.gpa); + defer arena.deinit(); + const surface = try p.render(arena.allocator()); + const x = p.rects[0].x + config.GUTTER + @as(u16, @intCast(panes.File.rawDisplayCol(name, pane.tag_col) - pane.tag_scroll)); + const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y; + try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x, y).style.fg); + try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x + 1, y).style.fg); + } + + test "tag filename tint legacy themes retain distinct active foreground" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 70, .rows = 14 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + p.gpa.free(pane.file.?.path); + pane.file.?.path = try p.gpa.dupe(u8, "/dir/file.zig"); + configure(p); + defer p.custom_theme = null; + p.custom_theme.?.tag_name_fg = null; + const active_fg = [3]u8{ 175, 183, 194 }; + p.custom_theme.?.tag_active_fg = active_fg; + p.chrome_animation.snap(pardes.ChromeTheme.fromTheme(p.theme())); + var arena = std.heap.ArenaAllocator.init(p.gpa); + defer arena.deinit(); + for ([_]bool{ false, true }) |active| { + p.settings.focus_tint = active; + const surface = try p.render(arena.allocator()); + const x = p.rects[0].x + config.GUTTER; + const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y; + const fg = if (active) active_fg else tag_fg; + try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 1, y).style.fg); + try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 5, y).style.fg); + } + } +}; + test "pane filename edits stage commit and cancel without changing body or command tail" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 70, .rows = 12 }); defer p.deinit(); @@ -3259,6 +3375,7 @@ const TerminalTests = struct { }; test { + _ = TagNameTintTests; _ = TreeContextTests; _ = TtySelectionTests; _ = ClickCursorTests; |
