2026-07-17 15:45:25 +08:00
|
|
|
import { describe, it } from "node:test";
|
|
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
import fs from "node:fs";
|
|
|
|
|
import os from "node:os";
|
|
|
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
|
import { formatTmuxStatusLine } from "../src/status.js";
|
|
|
|
|
import { loadSnapshotFromDir } from "../src/session.js";
|
|
|
|
|
import {
|
|
|
|
|
miniBar,
|
|
|
|
|
THEME_TOKYONIGHT,
|
|
|
|
|
THEME_GROKDAY,
|
|
|
|
|
paletteForGrokTheme,
|
|
|
|
|
readGrokUiConfig,
|
|
|
|
|
resolveTheme,
|
|
|
|
|
normalizeGrokThemeName,
|
|
|
|
|
} from "../src/theme.js";
|
|
|
|
|
|
|
|
|
|
const pkgRoot = path.join(
|
|
|
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
|
|
|
"..",
|
|
|
|
|
"..",
|
|
|
|
|
);
|
|
|
|
|
const fixtureSession = path.join(pkgRoot, "fixtures", "session");
|
|
|
|
|
|
|
|
|
|
describe("theme", () => {
|
|
|
|
|
it("maps Grok theme names to palettes", () => {
|
|
|
|
|
assert.equal(normalizeGrokThemeName("TokyoNight"), "tokyonight");
|
|
|
|
|
assert.equal(paletteForGrokTheme("tokyonight").name, "tokyonight");
|
|
|
|
|
assert.equal(paletteForGrokTheme("grokday").name, "grokday");
|
|
|
|
|
assert.equal(paletteForGrokTheme("light").name, "grokday");
|
|
|
|
|
assert.equal(paletteForGrokTheme("dark").name, "groknight");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("tokyonight palette uses Tokyo Night ink", () => {
|
|
|
|
|
assert.equal(THEME_TOKYONIGHT.value, "#c0caf5");
|
|
|
|
|
assert.equal(THEME_TOKYONIGHT.ok, "#9ece6a");
|
|
|
|
|
assert.equal(THEME_TOKYONIGHT.mark, "#7aa2f7");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("readGrokUiConfig parses [ui] theme", () => {
|
|
|
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-cfg-"));
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.join(tmp, "config.toml"),
|
|
|
|
|
`[ui]\ntheme = "tokyonight"\nauto_dark_theme = "tokyonight"\nauto_light_theme = "grokday"\n`,
|
|
|
|
|
);
|
|
|
|
|
const cfg = readGrokUiConfig(tmp);
|
|
|
|
|
assert.equal(cfg.theme, "tokyonight");
|
|
|
|
|
assert.equal(cfg.autoLightTheme, "grokday");
|
|
|
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-17 16:27:35 +08:00
|
|
|
it("resolveTheme follows Grok config (no lock)", () => {
|
2026-07-17 15:45:25 +08:00
|
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-cfg2-"));
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.join(tmp, "config.toml"),
|
|
|
|
|
`[ui]\ntheme = "tokyonight"\n`,
|
|
|
|
|
);
|
2026-07-17 16:27:35 +08:00
|
|
|
// env wants grokday but without LOCK → still follow Grok config
|
|
|
|
|
const t = resolveTheme(undefined, { GROK_HUD_THEME: "grokday" }, {
|
|
|
|
|
grokHome: tmp,
|
|
|
|
|
});
|
2026-07-17 15:45:25 +08:00
|
|
|
assert.equal(t.name, "tokyonight");
|
|
|
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-17 16:27:35 +08:00
|
|
|
it("resolveTheme follows explicit Grok theme switches", () => {
|
|
|
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-cfg3-"));
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.join(tmp, "config.toml"),
|
|
|
|
|
`[ui]\ntheme = "rosepinemoon"\n`,
|
|
|
|
|
);
|
|
|
|
|
const t = resolveTheme(undefined, {}, { grokHome: tmp });
|
|
|
|
|
assert.equal(t.name, "rosepinemoon");
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.join(tmp, "config.toml"),
|
|
|
|
|
`[ui]\ntheme = "grokday"\n`,
|
|
|
|
|
);
|
|
|
|
|
const t2 = resolveTheme(undefined, {}, { grokHome: tmp });
|
|
|
|
|
assert.equal(t2.name, "grokday");
|
|
|
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-17 15:45:25 +08:00
|
|
|
it("miniBar works with tokyonight", () => {
|
|
|
|
|
const bar = miniBar(50, 8, THEME_TOKYONIGHT);
|
|
|
|
|
assert.match(bar, /#9ece6a|#e0af68|#f7768e/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("formatTmuxStatusLine uses provided palette colours", () => {
|
|
|
|
|
const snap = loadSnapshotFromDir(fixtureSession)!;
|
|
|
|
|
const line = formatTmuxStatusLine(
|
|
|
|
|
snap,
|
|
|
|
|
{ available: true, percent: 22, message: "GrokBuild 9%" },
|
|
|
|
|
THEME_TOKYONIGHT,
|
|
|
|
|
);
|
|
|
|
|
assert.match(line, /#c0caf5|#565f89|#7aa2f7|#9ece6a/);
|
|
|
|
|
assert.match(line, /37%/);
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-17 16:27:35 +08:00
|
|
|
it("grokday is light-paper friendly high contrast", () => {
|
|
|
|
|
assert.equal(THEME_GROKDAY.name, "grokday");
|
|
|
|
|
// solid paper bg (not transparent default)
|
|
|
|
|
assert.match(THEME_GROKDAY.statusBg, /^#/);
|
|
|
|
|
// near-black ink
|
|
|
|
|
assert.match(THEME_GROKDAY.value, /^#0/);
|
|
|
|
|
// labels darker than mid-grey washout
|
|
|
|
|
const label = THEME_GROKDAY.label.replace("#", "");
|
|
|
|
|
const r = parseInt(label.slice(0, 2), 16);
|
|
|
|
|
assert.ok(r < 0xa0, "label must be dark enough on white");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("tmuxRole on grokday avoids dim (readable on paper)", async () => {
|
|
|
|
|
const { tmuxRole, isLightTheme } = await import("../src/theme.js");
|
|
|
|
|
assert.equal(isLightTheme(THEME_GROKDAY), true);
|
|
|
|
|
const label = tmuxRole(THEME_GROKDAY, "label", "ctx ");
|
|
|
|
|
const sep = tmuxRole(THEME_GROKDAY, "sep", " · ");
|
|
|
|
|
assert.doesNotMatch(label, /dim/);
|
|
|
|
|
assert.doesNotMatch(sep, /dim/);
|
|
|
|
|
assert.match(label, /italics|bold/);
|
|
|
|
|
assert.match(label, /#57534e|#0c0a09|#5b21b6|fg=/);
|
2026-07-17 15:45:25 +08:00
|
|
|
});
|
|
|
|
|
});
|