diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-31 09:59:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:08 -0300 |
| commit | 5cf16eab0a4beec196e51bdcee731b899a2af07c (patch) | |
| tree | fc2e7afaab7796b3f32773e29d26b5969727475c /tools/gen_themes.zig | |
| parent | 560ae0a63f7b5d354bad3ba13dd8ab26b5f0a870 (diff) | |
| download | pardes-5cf16eab0a4beec196e51bdcee731b899a2af07c.tar.gz pardes-5cf16eab0a4beec196e51bdcee731b899a2af07c.zip | |
every helix and zed theme on the machine, not a curated nine
228 in the ring: 3 pardes ships plus all 214 helix runtime themes and all 11
zed variants across its three families. No exclusions.
inherits is what this cost. 76 themes need it, so the reader is two passes now,
and the merge is WHOLE-KEY rather than field-wise because helix merges the
theme body at depth 1 — the three *_transparent themes clear their parent's
background with an empty table, and a field-wise merge would leave it painted.
I had that wrong until I read helix-view/src/theme.rs.
At 214 files the generator meets themes that are SPARSE rather than broken, and
the old fatal-on-anything-missing would have rejected them. Every fallback is
helix's own rule: no background at all means wear the terminal's, so bg and fg
go null (13 themes, correctly); the twelve RGB fields that cannot be null fall
back to ghostty's default palette entries 7 and 0, which is exactly what a
pardes terminal already paints an unstyled ANSI index as. fatal is kept for
what genuinely cannot be read.
Three real bugs surfaced only at scale: #ccc shorthand doubles the nibble
(helix's rule, 58 colours), a quoted inline-table key silently dropped a
selection colour, and four themes name a cursor background EQUAL to the page
because they are about to reverse that cell — taking it painted an invisible
move box. 0 degenerate themes out of 225 now: no fg==bg, no syntax colour on
its own background, no invisible box.
Collisions get an unconditional rule rather than a clever one: a zed variant is
always <name>_zed, because both projects ship gruvbox, ayu and one, and a
conditional tag would move a name when the other source changes. A comptime
assert holds it.
Cost: no-op build 0.257 -> 0.276s, a pardes.zig change 46.8 -> 47.5s, binary
+0.98%. The generator does 217 sources in 54ms.
NextColor stays and is no longer a way to REACH a theme — but the browse got
better, not worse: the generated half sorts by name, so its neighbours are that
theme's own family. theme.golden's fourth click moved from ayu_dark to acid,
which sorting 225 names does; themesel gained a tail capture proving row 228
renders.
Diffstat (limited to 'tools/gen_themes.zig')
| -rw-r--r-- | tools/gen_themes.zig | 278 |
1 files changed, 217 insertions, 61 deletions
diff --git a/tools/gen_themes.zig b/tools/gen_themes.zig index 50c32b8b..4fc7ab67 100644 --- a/tools/gen_themes.zig +++ b/tools/gen_themes.zig @@ -10,21 +10,40 @@ //! one input can be several themes out. //! //! Why vendored and not read out of ~/05-genizah: `zig build` must work on a -//! fresh clone with no network and no helix checkout. The files are small and -//! we consume a dozen fields out of each, so a curated handful beats a mirror -//! of all 214 helix ships — bulk buys nothing when pardes throws away 95% of -//! what a theme carries. +//! fresh clone with no network and no helix checkout. It is the WHOLE of what +//! both projects ship — helix's 214 runtime themes and zed's three families, +//! 1.5MB of text — because "which ones are worth having" is not a question a +//! build tool gets to answer on the user's behalf. +//! +//! NAMES. A helix theme is its file stem; a zed variant is its `name` slugged +//! with `_zed` on the end. Both projects ship gruvbox, ayu and one, so the +//! collision is systematic rather than accidental and qualifying every zed +//! variant is one rule instead of eight exceptions — it also keeps a name from +//! moving when the other source gains or loses a file. helix's own `acme.toml` +//! is vendored as `acme_helix.toml` for the same reason, since pardes ships an +//! acme of its own; the file is otherwise untouched. Two themes answering to +//! one name is a bug (`Theme <name>` would silently pick one), so src/pardes.zig +//! asserts the ring's names are distinct at comptime. //! //! Provenance: vendor/themes/*.toml are helix's own runtime themes (from //! helix-editor/helix runtime/themes, each file keeping its author's header); -//! *.json are zed's (zed-industries/zed assets/themes). Every one carries its -//! upstream licence beside it as `<name>.LICENSE`, and build.zig's directory -//! read skips anything that is not a .toml or a .json so they can sit there. +//! *.json are zed's (zed-industries/zed assets/themes). The licences sit beside +//! them as `<name>.LICENSE` — `helix.LICENSE` covering the runtime set, one per +//! zed family, plus the twenty-two helix keeps in `runtime/themes/licenses/` +//! for the themes it took from somewhere else (their names group by SOURCE, not +//! by file: `everforest.LICENSE` covers both everforest themes). build.zig's +//! directory read skips anything that is not a .toml or a .json, which is what +//! lets them all sit in the same flat directory as the themes they cover. //! -//! A theme that cannot be mapped is FATAL, naming the file and the key. The -//! vendored set is curated, so a missing key means the file is not what this -//! reader thinks it is — printing a warning and emitting a black-on-black theme -//! would put that mistake on screen instead of in the build log. +//! SPARSE vs BROKEN. At nine hand-picked files every missing key was a mistake, +//! so every missing key was fatal. At 225 that is wrong: helix themes legally +//! leave things unstyled, and helix's own rule for an unstyled scope is "the +//! page's colors", ending at "whatever the terminal is already wearing". So the +//! fallbacks below are helix's semantics, not a papering-over — see `helixTheme` +//! for the field-by-field list. What stays FATAL, naming the file and the key, +//! is input this reader cannot read at all: a colour that is neither #rrggbb nor +//! a palette entry nor one of the sixteen names, an `inherits` whose parent is +//! not vendored, a zed variant missing a key zed always writes. const std = @import("std"); const Rgb = [3]u8; @@ -58,15 +77,23 @@ pub fn main(init: std.process.Init) !void { if (args.len < 3) std.process.fatal("usage: gen-themes <out-dir> <theme-source>...", .{}); var themes: std.ArrayList(Theme) = .empty; + // TWO passes over the helix half, because `inherits = "other"` names a + // SIBLING SOURCE by stem: nothing can be mapped until every file is read, + // and the parent may be later in argv (or inherit something itself). + var tomls: std.StringHashMapUnmanaged(Toml) = .empty; + var stems: std.ArrayList([]const u8) = .empty; for (args[2..]) |src| { const text = try std.Io.Dir.cwd().readFileAlloc(io, src, arena, .limited(8 << 20)); - if (std.mem.endsWith(u8, src, ".toml")) - try readHelix(arena, src, text, &themes) - else if (std.mem.endsWith(u8, src, ".json")) + if (std.mem.endsWith(u8, src, ".toml")) { + const stem = std.fs.path.stem(src); + try tomls.put(arena, stem, parseToml(arena, src, text)); + try stems.append(arena, stem); + } else if (std.mem.endsWith(u8, src, ".json")) try readZed(arena, gpa, src, text, &themes) else std.process.fatal("{s}: not a helix .toml or a zed .json", .{src}); } + for (stems.items) |stem| try helixTheme(arena, stem, resolve(arena, &tomls, stem, 8), &themes); // sorted, so the ring's tail is the same on every machine whatever order // the directory happened to be read in std.mem.sort(Theme, themes.items, {}, byName); @@ -153,9 +180,18 @@ fn slug(arena: std.mem.Allocator, raw: []const u8) ![]const u8 { /// literal `#rrggbb`, and helix's bare form (`"ui.text" = "nord4"`) is an fg const Pair = struct { fg: ?[]const u8 = null, bg: ?[]const u8 = null }; -fn readHelix(arena: std.mem.Allocator, path: []const u8, text: []const u8, out: *std.ArrayList(Theme)) !void { +/// one .toml as written, before `inherits` is applied +const Toml = struct { + path: []const u8, + inherits: ?[]const u8, + keys: std.StringHashMapUnmanaged(Pair), + palette: std.StringHashMapUnmanaged([]const u8), +}; + +fn parseToml(arena: std.mem.Allocator, path: []const u8, text: []const u8) Toml { var palette: std.StringHashMapUnmanaged([]const u8) = .empty; var keys: std.StringHashMapUnmanaged(Pair) = .empty; + var inherits: ?[]const u8 = null; var in_palette = false; var lines = std.mem.splitScalar(u8, text, '\n'); while (lines.next()) |raw| { @@ -170,57 +206,146 @@ fn readHelix(arena: std.mem.Allocator, path: []const u8, text: []const u8, out: const val = std.mem.trim(u8, line[eq + 1 ..], " \t"); if (val.len == 0) continue; if (in_palette) { - try palette.put(arena, key, unquote(val)); + palette.put(arena, key, unquote(val)) catch @panic("OOM"); + } else if (std.mem.eql(u8, key, "inherits")) { + inherits = unquote(val); } else if (quoted(val)) { - try keys.put(arena, key, .{ .fg = unquote(val) }); + keys.put(arena, key, .{ .fg = unquote(val) }) catch @panic("OOM"); } else if (val[0] == '{') { - try keys.put(arena, key, table(val)); + keys.put(arena, key, table(val)) catch @panic("OOM"); } } - // `inherits = "other"` needs a second file we did not read, so the theme we - // would emit is only the half that overrides. Vendor the parent instead. - if (keys.contains("inherits")) std.process.fatal("{s}: inherits= is not supported; vendor a theme that stands alone", .{path}); + return .{ .path = path, .inherits = inherits, .keys = keys, .palette = palette }; +} + +/// `inherits = "other"` — a THIRD of helix's themes are a handful of overrides +/// on top of a sibling, so refusing them would cost 76 of the 214. +/// +/// The merge is WHOLE-KEY, not field-by-field: helix calls merge_toml_values at +/// depth ONE for the theme (helix-view theme.rs merge_themes), so the recursion +/// stops before it reaches inside an inline table and the child's value replaces +/// the parent's outright. That is load-bearing rather than pedantic — the three +/// inheriting `*_transparent` themes clear their parent's background by writing +/// `"ui.background" = {}`, which a field-wise merge would leave painted. The +/// [palette] is merged separately at depth two, i.e. entry by entry, which is +/// what lets a child's key name a colour only its parent defines. +/// +/// `depth` bounds the walk: a chain that long is a cycle, and a build tool +/// should say so rather than recurse until the stack runs out. No memoisation — +/// the deepest chain in helix's set is three and the widest parent has four +/// children, so the whole thing is a few hundred hash-map copies. +fn resolve(arena: std.mem.Allocator, tomls: *const std.StringHashMapUnmanaged(Toml), stem: []const u8, depth: usize) Helix { + const t = tomls.get(stem) orelse + std.process.fatal("inherits = \"{s}\" names a theme that is not vendored", .{stem}); + const parent = t.inherits orelse return .{ .path = t.path, .keys = t.keys, .palette = t.palette }; + if (depth == 0) std.process.fatal("{s}: inherits= chain is a cycle or deeper than eight", .{t.path}); + const base = resolve(arena, tomls, parent, depth - 1); + // CLONED, not mutated: the parent's own maps are the ones in `tomls`, and + // four themes inherit from github_dark alone + var keys = base.keys.clone(arena) catch @panic("OOM"); + var palette = base.palette.clone(arena) catch @panic("OOM"); + var kit = t.keys.iterator(); + while (kit.next()) |e| keys.put(arena, e.key_ptr.*, e.value_ptr.*) catch @panic("OOM"); + var pit = t.palette.iterator(); + while (pit.next()) |e| palette.put(arena, e.key_ptr.*, e.value_ptr.*) catch @panic("OOM"); + return .{ .path = t.path, .keys = keys, .palette = palette }; +} - const h: Helix = .{ .path = path, .keys = keys, .palette = palette }; - // helix's own rule for anything a theme left unset: the page's colors. - const fg = h.need("ui.text", .fg); - const bg = h.need("ui.background", .bg); +/// A resolved helix theme mapped onto pardes's fourteen fields. +/// +/// EVERY FALLBACK HERE IS HELIX'S OWN RULE, which is why none of them is fatal: +/// an unstyled scope wears `ui.text`, an unpainted cell wears `ui.background`, +/// and a theme that styles neither is saying "wear whatever the terminal is +/// already wearing" (the base16/term16/transparent themes exist precisely to do +/// that). pardes spells that last step as a null bg/fg, so those two pass +/// straight through — but the other twelve fields are RGB and cannot be null, so +/// they need a concrete stand-in for "the terminal's", and that is the default +/// palette's ink and page below. Field by field: +/// +/// bg ui.background.bg, else null = the host terminal's own cell +/// fg ui.text.fg, else null = ditto +/// box ui.cursor.primary.bg, else ui.cursor.bg, else keyword.fg, +/// else ink — skipping any of them that IS the page (see +/// firstNotPage) +/// box_dim, +/// scroll_thumb ui.selection.bg, else ui.selection.primary.bg, else +/// ui.selection.fg, same page-skipping, else page. No selection +/// colour anywhere means the author drew no selection either, +/// and a dim box the colour of the page is the honest rendering +/// of that — no theme in the vendored set actually gets there +/// tag_bg, +/// scroll_track ui.statusline.bg, else page +/// tag_fg ui.statusline.fg, else ink +/// kw/str/comment their scope's fg, else ink (helix: unstyled = ui.text) +/// num constant.numeric.fg, else constant.fg, else ink +/// lineno ui.linenr.fg, else ink +fn helixTheme(arena: std.mem.Allocator, stem: []const u8, h: Helix, out: *std.ArrayList(Theme)) !void { + const fg = h.get("ui.text", .fg); + const bg = h.get("ui.background", .bg); + const ink = fg orelse ansi_default[7]; + const page = bg orelse ansi_default[0]; // The move box is the accent that says "here". helix's is the primary // cursor's background — but a theme that draws its cursor by REVERSING // video (nord, monokai) never names one, so the fallback is the loudest // hue the theme did commit to. - const box = h.first(&.{ .{ "ui.cursor.primary", .bg }, .{ "ui.cursor", .bg }, .{ "keyword", .fg } }) orelse - std.process.fatal("{s}: no ui.cursor.primary/ui.cursor background and no keyword color", .{path}); - const sel = h.get("ui.selection", .bg) orelse bg; + const box = h.firstNotPage(page, &.{ .{ "ui.cursor.primary", .bg }, .{ "ui.cursor", .bg }, .{ "keyword", .fg } }) orelse ink; + // ...and the dim box is the selection, which the same themes draw the same + // reversed way, so it is read the same way: the primary selection is what + // helix highlights the range you are ON with, and a theme that names only a + // foreground for it (peachpuff, base16_terminal) is naming the colour the + // reverse will land in the background. + const sel = h.firstNotPage(page, &.{ .{ "ui.selection", .bg }, .{ "ui.selection.primary", .bg }, .{ "ui.selection", .fg } }) orelse page; try out.append(arena, .{ - .name = try slug(arena, std.fs.path.stem(path)), - // explicit rather than null: a helix theme dresses its whole page, and - // half-dressing it over whatever the host terminal defaults to is how - // you get grey-on-grey. `palette` stays null for the opposite reason — - // helix leaves the colors a PROGRAM emits alone, and so do we. + .name = try slug(arena, stem), + // `palette` stays null: helix leaves the colors a PROGRAM emits alone, + // and so do we. .bg = bg, .fg = fg, - .tag_bg = h.get("ui.statusline", .bg) orelse bg, - .tag_fg = h.get("ui.statusline", .fg) orelse fg, + .tag_bg = h.get("ui.statusline", .bg) orelse page, + .tag_fg = h.get("ui.statusline", .fg) orelse ink, .box = box, .box_dim = sel, - .kw = h.need("keyword", .fg), - .str = h.need("string", .fg), + .kw = h.get("keyword", .fg) orelse ink, + .str = h.get("string", .fg) orelse ink, // most themes only colour `constant`; the ones that split out numbers // are being more specific, so they win - .num = h.first(&.{ .{ "constant.numeric", .fg }, .{ "constant", .fg } }) orelse - std.process.fatal("{s}: no constant.numeric or constant color", .{path}), - .comment = h.need("comment", .fg), - .lineno = h.get("ui.linenr", .fg) orelse fg, + .num = h.first(&.{ .{ "constant.numeric", .fg }, .{ "constant", .fg } }) orelse ink, + .comment = h.get("comment", .fg) orelse ink, + .lineno = h.get("ui.linenr", .fg) orelse ink, // the scrollbar is chrome, so it takes the chrome colors: the tag bar's // own background for the track, the selection (one step brighter than // anything else in the theme's greys) for the thumb riding it - .scroll_track = h.get("ui.statusline", .bg) orelse bg, + .scroll_track = h.get("ui.statusline", .bg) orelse page, .scroll_thumb = sel, .palette = null, }); } +/// helix lets a theme name one of the terminal's sixteen colors instead of an +/// RGB (`fg = "light-gray"`), and four of them — base16_terminal, term16_dark, +/// term16_light, ttox — are written that way from top to bottom. pardes's Theme +/// is RGB, so a name has to become one, and these are ghostty's defaults: the +/// very colors a pardes terminal pane already paints an unstyled ANSI index as +/// (zig-pkg's ghostty, src/terminal/color.zig, `Name.default`). Copied and not +/// imported because the generator is a host tool that links nothing — sixteen +/// constants are not worth compiling a terminal emulator for the build machine. +/// Index 7 and index 0 are also the ink and page a theme gets when it styles +/// neither `ui.text` nor `ui.background`. +/// +/// The ORDER is helix's, which is not quite ANSI's: helix's `gray` is index 8 +/// and its `light-gray` is index 7 (helix-view/src/graphics.rs maps them onto +/// crossterm's DarkGrey and Grey respectively). +const ansi_names = [16][]const u8{ + "black", "red", "green", "yellow", "blue", "magenta", "cyan", "light-gray", + "gray", "light-red", "light-green", "light-yellow", "light-blue", "light-magenta", "light-cyan", "white", +}; +const ansi_default = [16]Rgb{ + .{ 0x1d, 0x1f, 0x21 }, .{ 0xcc, 0x66, 0x66 }, .{ 0xb5, 0xbd, 0x68 }, .{ 0xf0, 0xc6, 0x74 }, + .{ 0x81, 0xa2, 0xbe }, .{ 0xb2, 0x94, 0xbb }, .{ 0x8a, 0xbe, 0xb7 }, .{ 0xc5, 0xc8, 0xc6 }, + .{ 0x66, 0x66, 0x66 }, .{ 0xd5, 0x4e, 0x53 }, .{ 0xb9, 0xca, 0x4a }, .{ 0xe7, 0xc5, 0x47 }, + .{ 0x7a, 0xa6, 0xda }, .{ 0xc3, 0x97, 0xd8 }, .{ 0x70, 0xc0, 0xb1 }, .{ 0xea, 0xea, 0xea }, +}; + const Which = enum { fg, bg }; const Helix = struct { @@ -236,25 +361,43 @@ const Helix = struct { }); } - fn need(h: Helix, key: []const u8, which: Which) Rgb { - return h.get(key, which) orelse - std.process.fatal("{s}: missing {s}.{s}", .{ h.path, key, @tagName(which) }); - } - fn first(h: Helix, tries: []const struct { []const u8, Which }) ?Rgb { for (tries) |t| if (h.get(t[0], t[1])) |c| return c; return null; } - /// a literal `#rrggbb`, or a `[palette]` name that resolves to one. A - /// palette entry may itself be a name (helix lets one alias another), so - /// this walks — bounded, because a cycle is a broken theme and not a hang. - fn color(h: Helix, start: []const u8) Rgb { + /// `first`, skipping any candidate that comes out equal to the page. A + /// cursor or a selection painted in the page's own colour is a theme + /// drawing that highlight by REVERSING video (it names the cell it is about + /// to invert), not one naming an accent — take it and pardes paints an + /// invisible box on an invisible scrollbar thumb. + fn firstNotPage(h: Helix, page: Rgb, tries: []const struct { []const u8, Which }) ?Rgb { + for (tries) |t| if (h.get(t[0], t[1])) |c| if (!std.mem.eql(u8, &c, &page)) return c; + return null; + } + + /// a literal `#rrggbb`, a `[palette]` name that resolves to one, or one of + /// the sixteen terminal colours helix names. A palette entry may itself be a + /// name (helix lets one alias another), so this walks — bounded, because a + /// cycle is a broken theme and not a hang. + /// + /// The theme's own `[palette]` is consulted BEFORE the sixteen names, which + /// is helix's order too (ThemePalette::new extends the defaults with the + /// theme's, so a theme that defines `red` means its own red). `default` is + /// helix's Color::Reset — no colour at all — and comes back as null so the + /// caller's fallback picks the right side's stand-in, ink for an fg and page + /// for a bg. + fn color(h: Helix, start: []const u8) ?Rgb { var name = start; for (0..8) |_| { if (name.len > 0 and name[0] == '#') return hex(h.path, name); - name = h.palette.get(name) orelse - std.process.fatal("{s}: color \"{s}\" is neither #rrggbb nor a [palette] entry", .{ h.path, start }); + if (h.palette.get(name)) |next| { + name = next; + continue; + } + if (std.mem.eql(u8, name, "default")) return null; + for (ansi_names, 0..) |n, i| if (std.mem.eql(u8, n, name)) return ansi_default[i]; + std.process.fatal("{s}: color \"{s}\" is neither #rrggbb nor a [palette] entry nor a terminal color name", .{ h.path, start }); } std.process.fatal("{s}: [palette] entry \"{s}\" never resolves to a color", .{ h.path, start }); } @@ -315,7 +458,10 @@ fn table(s: []const u8) Pair { const item = rest[0..end]; rest = rest[@min(end + 1, rest.len)..]; const eq = topLevel(item, '=') orelse continue; - const k = std.mem.trim(u8, item[0..eq], " \t"); + // the key gets unquoted too: TOML allows `{ "bg" = "selection" }` and + // night_rider is written that way, so matching the bare spelling only + // would drop its selection colour on the floor + const k = unquote(std.mem.trim(u8, item[0..eq], " \t")); const v = std.mem.trim(u8, item[eq + 1 ..], " \t"); if (!quoted(v)) continue; if (std.mem.eql(u8, k, "fg")) out.fg = unquote(v); @@ -355,7 +501,11 @@ fn readZed(arena: std.mem.Allocator, gpa: std.mem.Allocator, path: []const u8, t pal[i + 8] = z.need(try std.fmt.allocPrint(arena, "terminal.ansi.bright_{s}", .{n})); } try out.append(arena, .{ - .name = try slug(arena, (v.get("name") orelse std.process.fatal("{s}: a variant has no `name`", .{path})).string), + // `_zed` on every one of them: see the naming note at the top — + // zed re-ships gruvbox, ayu and one, so the source is part of the + // name rather than a suffix that appears only on the eight that + // happen to clash today + .name = try std.fmt.allocPrint(arena, "{s}_zed", .{try slug(arena, (v.get("name") orelse std.process.fatal("{s}: a variant has no `name`", .{path})).string)}), .bg = z.need("editor.background"), .fg = z.need("editor.foreground"), .tag_bg = z.need("status_bar.background"), @@ -401,14 +551,20 @@ const Zed = struct { } }; -/// `#rrggbb`, or zed's `#rrggbbaa`. The alpha is DROPPED: pardes paints opaque -/// cells, and the only translucent colors we read are the scrollbar's, where a -/// dropped alpha reads as a slightly bolder bar and nothing else. +/// `#rrggbb`, helix's `#rgb` shorthand, or zed's `#rrggbbaa`. The alpha is +/// DROPPED: pardes paints opaque cells, and the only translucent colors we read +/// are the scrollbar's, where a dropped alpha reads as a slightly bolder bar and +/// nothing else. `#rgb` doubles each nibble the way helix does (`#ccc` is +/// `#cccccc`, not `#c0c0c0`) — 58 colours in the vendored set are written that +/// way and rounding them down would darken those themes by a hair. fn hex(path: []const u8, s: []const u8) Rgb { const d = if (s.len > 0 and s[0] == '#') s[1..] else s; - if (d.len != 6 and d.len != 8) std.process.fatal("{s}: \"{s}\" is not #rrggbb", .{ path, s }); + if (d.len != 3 and d.len != 6 and d.len != 8) std.process.fatal("{s}: \"{s}\" is not #rgb or #rrggbb", .{ path, s }); var out: Rgb = undefined; - for (&out, 0..) |*c, i| c.* = std.fmt.parseInt(u8, d[i * 2 ..][0..2], 16) catch - std.process.fatal("{s}: \"{s}\" is not #rrggbb", .{ path, s }); + for (&out, 0..) |*c, i| { + const digits = if (d.len == 3) [2]u8{ d[i], d[i] } else d[i * 2 ..][0..2].*; + c.* = std.fmt.parseInt(u8, &digits, 16) catch + std.process.fatal("{s}: \"{s}\" is not #rgb or #rrggbb", .{ path, s }); + } return out; } |
