summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 10:18:23 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitc8bcda00ad54e4f81cc83ffe601d49703f4a7135 (patch)
tree521282843c76f88711c4063b3eb809c3d1857927
parent826ec1fac3c012e71ab34207328faaac5b5827b7 (diff)
downloadpardes-c8bcda00ad54e4f81cc83ffe601d49703f4a7135.tar.gz
pardes-c8bcda00ad54e4f81cc83ffe601d49703f4a7135.zip
ThemeSel in sections: acme and lapis with the current themes, helix and dark as legacy, a section per ported family
The picker's headings come from colors.theme_groups instead of one split at native_theme_count, so acme (current, matched to plan9port acme's source) no longer sits under a legacy heading. Ring order follows the sections: the fifteen pardes palettes, acme, lapis, the faithful ports a family at a time, then helix and dark, then the imports. An imported theme whose name a port takes gets _helix, as zed's carry _zed.
-rw-r--r--docs/config.md12
-rw-r--r--docs/themes.md32
-rw-r--r--src/Output.zig5
-rw-r--r--src/colors.zig83
-rw-r--r--test/output.zig32
5 files changed, 143 insertions, 21 deletions
diff --git a/docs/config.md b/docs/config.md
index 59883b59..0c11b206 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -109,11 +109,15 @@ project gains or loses a file. A name that is not in the ring is ignored.
The fifteen [native Pardes themes](themes.md) lead the ring: `orchard` (the
default), `dusk`, `ink`, `paper`, `daybreak`, `atelier`, `forge`, `lagoon`,
`solarium`, `spectrum`, `harvest`, `clay`, `forge_black`, `forge_soft`, and
-`orchard_black`. `ThemeSel` lists them first under Pardes themes, followed by
-a separate legacy/imported section. They add coordinated
+`orchard_black`, then `acme` and `lapis`. `ThemeSel` lists them first under
+Pardes themes. They add coordinated
focus, search, diagnostic and terminal colors; `ink` and `daybreak` are high
-contrast dark and light options. The original `helix`, `dark` and `acme`
-themes and all imported names remain available.
+contrast dark and light options. After them come the
+[faithful ports](themes.md#faithful-ports) of well-known themes, a section per
+family, then the legacy `helix` and `dark`, then everything imported. Where a
+port takes an imported theme's name (`dracula`), the imported one gains
+`_helix` (`dracula_helix`), as zed's carry `_zed`. `NextColor` walks the same
+ring in the same order.
`FocusTint` toggles the focused pane and column tag tints; it is on by default.
Workspace, column and pane command text can be edited directly; see
diff --git a/docs/themes.md b/docs/themes.md
index 38596819..9c8e91a5 100644
--- a/docs/themes.md
+++ b/docs/themes.md
@@ -6,9 +6,10 @@ terminals together. `orchard` is the initial theme.
Execute `Theme <name>` anywhere, or open `ThemeSel` with `SPC t t` and select
a theme's command. Put the same command in your [startup configuration](config.md)
to keep a preference. `NextColor` cycles through the native themes first,
-then the retained `helix`, `dark` and `acme` themes and imported palettes.
-`ThemeSel` groups the new palettes under `Pardes themes`, followed by
-`Legacy and imported themes`; its navigation skips the section headings.
+then `acme` and `lapis`, the [faithful ports](#faithful-ports), the legacy
+`helix` and `dark`, and the imported palettes. `ThemeSel` shows the same
+order in sections: `Pardes themes`, one per ported family, `Legacy themes`
+and `Imported themes (helix, zed)`; its navigation skips the section headings.
See the [Agave visual review](ui-review.md) for the original six-palette gallery.
| Theme | Character | Page | Accent |
@@ -58,7 +59,8 @@ Their local references are `vendor/themes/dark_plus.toml`,
`material_oceanic.toml` (and its `material_deep_ocean.toml` parent),
`solarized_dark.toml`, `monokai_pro.toml`, `autumn.toml`, and the Gruvbox Dark
entry in `gruvbox.json`. The original imported themes remain selectable under
-their existing names; the adaptations have distinct Pardes names and do not
+their existing names (one a [faithful port](#faithful-ports) now holds gains
+`_helix`, as in `dracula_helix`); the adaptations have distinct Pardes names and do not
claim upstream affiliation. Existing vendor provenance and licenses remain
with those sources.
@@ -81,6 +83,28 @@ no more than 1.6:1, keeping focus changes quiet. ANSI black is exempt because ap
also use it as a background. These are palette checks, not a guarantee about
arbitrary terminal escape sequences, reversed colors or shader effects.
+## Faithful ports
+
+These are well-known themes taken as close to their originals as pardes can
+draw them: every colour the original has (page, text, selection, syntax, line
+numbers, diagnostics, terminal ANSI) is its own, read out of the original's
+files, and each theme file cites the file and line of every value. The loose
+adaptations above (`forge`, `solarium`, `spectrum`, ...) stay as they are.
+
+pardes has one colour each for keywords, strings, numbers and comments, so a
+theme that splits a kind (VS Code's control-flow keywords) keeps its main
+one. A pane's tag is the original's window bar (vim's `StatusLine` and
+`StatusLineNC`, emacs's `mode-line` and `mode-line-inactive`) or, in a tabbed
+editor, its tab. What the original lacks (grips, a scroll column) comes from
+its own palette. Where a chrome colour misses one of pardes's floors (rules
+1.5:1 off the page, the focused tag 1.5:1 off the unfocused one on a dark
+page and 1.25:1 on a light one, tag text 4.5:1), only that chrome colour
+moves, just far enough, and the file's header says so. Text, syntax and ANSI
+are never adjusted.
+
+| Family | Themes | Source |
+| --- | --- | --- |
+
## Make a theme your own
Run `DumpThemes` to export complete `.zon` files below the configuration
diff --git a/src/Output.zig b/src/Output.zig
index b32a40a2..553dcc95 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -522,10 +522,9 @@ pub fn openJumps(p: *Pardes, id: usize) !void {
pub fn openThemes(p: *Pardes, id: usize) !void {
var out: std.Io.Writer.Allocating = .init(p.gpa);
errdefer out.deinit();
- try out.writer.writeAll("# Pardes themes\n");
for (pardes.themes, 0..) |t, i| {
- if (i == pardes.native_theme_count)
- try out.writer.writeAll("\n# Legacy and imported themes\n");
+ if (pardes.colors.groupAt(i)) |title|
+ try out.writer.print("{s}# {s}\n", .{ if (i == 0) "" else "\n", title });
try out.writer.print(comptime config.Runtime.findAction(.theme).?.word ++ " {s}\n", .{t.name});
}
const content = try out.toOwnedSlice();
diff --git a/src/colors.zig b/src/colors.zig
index 8319a112..743a74d7 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -105,6 +105,25 @@ test "every native theme's separators stand off its page, and its focus tint off
}
}
+test "every faithful port meets pardes's chrome floors in its own colours, with nothing left to derive" {
+ const first = comptime indexOfTheme("lapis") + 1;
+ const last = comptime indexOfTheme("helix");
+ for (themes[first..last]) |*th| {
+ const chrome = ChromeTheme.fromTheme(th);
+ const page = th.bg.?;
+ // Written out already adjusted: the run-time lift and push leave
+ // the file's own values, so what the file says is what is drawn.
+ try std.testing.expectEqual(th.border.?, chrome.border);
+ try std.testing.expectEqual(th.tag_active_bg.?, chrome.tag_active_bg);
+ try std.testing.expect(contrast(th.border.?, page) >= separator_floor);
+ try std.testing.expect(contrast(th.tag_active_bg.?, th.tag_bg) >= focusTintFloor(th));
+ try std.testing.expect(contrast(th.tag_fg, th.tag_bg) >= 4.5);
+ try std.testing.expect(contrast(th.tag_active_fg.?, th.tag_active_bg.?) >= 4.5);
+ try std.testing.expect(contrast(th.box_dim, th.tag_bg) >= 1.5);
+ try std.testing.expect(th.palette != null);
+ }
+}
+
test "a derived separator or tint keeps the theme's own when it already stands off" {
const forge = &themes[for (themes[0..native_count], 0..) |t, i| {
if (std.mem.eql(u8, t.name, "forge")) break i;
@@ -303,10 +322,13 @@ const curated = struct {
pub const forge_black = @import("themes/forge_black.zig");
pub const forge_soft = @import("themes/forge_soft.zig");
pub const orchard_black = @import("themes/orchard_black.zig");
- pub const helix = @import("themes/helix.zig");
- pub const dark = @import("themes/dark.zig");
pub const acme = @import("themes/acme.zig");
pub const lapis = @import("themes/lapis.zig");
+ // Faithful ports of well-known themes, a family at a time (src/themes/
+ // <name>.zig says where each value came from): `theme_groups`.
+ // The legacy pair last.
+ pub const helix = @import("themes/helix.zig");
+ pub const dark = @import("themes/dark.zig");
};
fn fold(comptime C: type) [@typeInfo(C).@"struct".decls.len]Theme {
@@ -338,10 +360,65 @@ fn fold(comptime C: type) [@typeInfo(C).@"struct".decls.len]Theme {
}
}
-pub const themes = fold(curated) ++ fold(@import("generated_themes"));
+/// The imported themes (tools/gen_themes.zig), less any name one of ours
+/// takes: a faithful port (`dracula`) keeps the plain name, and helix's
+/// looser copy of it is still there as `dracula_helix` (the zed ones already
+/// carry `_zed`).
+fn foldImports(comptime ours: anytype, comptime C: type) [@typeInfo(C).@"struct".decls.len]Theme {
+ comptime {
+ @setEvalBranchQuota(400000);
+ var out = fold(C);
+ for (&out) |*g| for (ours) |c| if (std.mem.eql(u8, c.name, g.name)) {
+ g.name = g.name ++ "_helix";
+ };
+ return out;
+ }
+}
+
+pub const themes = fold(curated) ++ foldImports(fold(curated), @import("generated_themes"));
/// The first `native_count` of `themes` are pardes's own; the rest imported.
pub const native_count = @typeInfo(curated).@"struct".decls.len;
+/// The picker's sections (`ThemeSel`), in ring order, each running from
+/// the theme it names to the next section's; `NextColor` and `Theme` still
+/// see one flat ring. The current pardes themes, then the faithful ports a
+/// family at a time, then the legacy pair (`helix`, captured from an old
+/// helix setup and the boot default before orchard; `dark`, the host
+/// terminal's own ground), then everything imported.
+pub const ThemeGroup = struct { title: []const u8, first: []const u8 };
+const theme_groups = [_]ThemeGroup{
+ .{ .title = "Pardes themes", .first = "orchard" },
+ .{ .title = "Legacy themes", .first = "helix" },
+};
+
+fn indexOfTheme(comptime name: []const u8) usize {
+ return comptime blk: {
+ @setEvalBranchQuota(100000);
+ for (themes[0..native_count], 0..) |t, i| if (std.mem.eql(u8, t.name, name)) break :blk i;
+ @compileError("theme_groups names " ++ name ++ ", which is not one of ours");
+ };
+}
+
+/// The heading of the picker section that starts at ring index `index`.
+pub fn groupAt(index: usize) ?[]const u8 {
+ inline for (theme_groups) |g| if (index == comptime indexOfTheme(g.first)) return g.title;
+ if (index == native_count) return "Imported themes (helix, zed)";
+ return null;
+}
+
+test "the picker's sections run in ring order and cover it" {
+ var last: usize = 0;
+ inline for (theme_groups, 0..) |g, n| {
+ const at = comptime indexOfTheme(g.first);
+ if (n == 0) try std.testing.expectEqual(@as(usize, 0), at) else try std.testing.expect(at > last);
+ last = at;
+ }
+ try std.testing.expectEqualStrings("Pardes themes", groupAt(0).?);
+ // acme is a current theme, not a legacy one.
+ try std.testing.expect(indexOfTheme("acme") < indexOfTheme("helix"));
+ try std.testing.expect(groupAt(native_count) != null);
+}
+
/// Original and classic-inspired Pardes palettes precede retained imports.
pub const native_theme_count = 15;
diff --git a/test/output.zig b/test/output.zig
index 478f1b4a..d9d41e28 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -952,7 +952,7 @@ test "N exactly reverses n across output panes and the ring seam" {
try std.testing.expect(saw_first and saw_second);
}
-test "ThemeSel groups native themes first and skips headings in both directions" {
+test "ThemeSel lists the ring in sections and skips headings in both directions" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 40 });
defer p.deinit();
const source = try p.setTestFile("notes\n");
@@ -966,17 +966,35 @@ test "ThemeSel groups native themes first and skips headings in both directions"
try std.testing.expectEqual(@as(i32, 0), list.body.cur_row);
try std.testing.expectEqual(@as(usize, 0), list.file.?.scroll);
try std.testing.expectEqualStrings("# Pardes themes", modal.lineSlice(content, 0));
- try std.testing.expectEqualStrings("# Legacy and imported themes", modal.lineSlice(content, pardes.native_theme_count + 2));
- try std.testing.expectEqual(@as(usize, pardes.themes.len + 3), std.mem.count(u8, content, "\n"));
+ // Every theme in ring order, each section's heading after a blank line
+ // where colors.groupAt says one starts.
+ var row: usize = 1;
+ var sections: usize = 1;
+ var first_break: ?usize = null;
for (pardes.themes, 0..) |theme, i| {
- const row = i + 1 + @as(usize, if (i >= pardes.native_theme_count) 2 else 0);
+ if (i > 0) if (pardes.colors.groupAt(i)) |title| {
+ try std.testing.expectEqualStrings("", modal.lineSlice(content, row));
+ try std.testing.expect(std.mem.startsWith(u8, modal.lineSlice(content, row + 1), "# "));
+ try std.testing.expectEqualStrings(title, modal.lineSlice(content, row + 1)["# ".len..]);
+ row += 2;
+ sections += 1;
+ if (first_break == null) first_break = i;
+ };
const line = modal.lineSlice(content, row);
try std.testing.expect(std.mem.startsWith(u8, line, "Theme "));
try std.testing.expectEqualStrings(theme.name, line["Theme ".len..]);
+ row += 1;
}
+ try std.testing.expectEqual(pardes.themes.len + 1 + 2 * (sections - 1), std.mem.count(u8, content, "\n"));
+ // acme is a current theme: it is listed before any break.
+ const acme = for (pardes.themes, 0..) |t, i| {
+ if (std.mem.eql(u8, t.name, "acme")) break i;
+ } else return error.MissingAcme;
+ const brk = first_break orelse return error.NoSections;
+ try std.testing.expect(acme < brk);
// Opening an imported theme's selector must still begin with the natives.
// Walk across the section break without selecting or executing a heading.
- for (pardes.themes[0 .. pardes.native_theme_count + 1]) |theme| {
+ for (pardes.themes[0 .. brk + 1]) |theme| {
p.update(.{ .key = .{ .cp = 'n' } });
try std.testing.expectEqual(tid, p.active);
const selected = selectedOutputText(list) orelse return error.MissingThemeSelection;
@@ -985,9 +1003,9 @@ test "ThemeSel groups native themes first and skips headings in both directions"
}
p.update(.{ .key = .{ .cp = 'N' } });
const previous = selectedOutputText(list) orelse return error.MissingThemeSelection;
- try std.testing.expectEqualStrings(pardes.themes[pardes.native_theme_count - 1].name, previous["Theme ".len..]);
+ try std.testing.expectEqualStrings(pardes.themes[brk - 1].name, previous["Theme ".len..]);
p.update(.{ .key = .{ .cp = Key.tab } });
- try std.testing.expectEqual(pardes.native_theme_count - 1, p.settings.theme);
+ try std.testing.expectEqual(brk - 1, p.settings.theme);
}
test "n/N selects command outputs by whole row" {