pragma Singleton // The shipped theme catalog, read from config/themes.json — full palettes for // every theme Panama ships, one file shared with the scripts (jq) and the // Node halves of contracts (fs). QML reads it here with FileView rather than // keeping a duplicated JS table: unlike palette.json's CURATED twin, nothing // in this file needs to run under Node. // // The embedded fallback below carries only the two default themes (Moon and // Day) so the shell renders correctly for the instant before the file loads — // and forever if it is missing or malformed. theme-catalog-contract pins the // fallback byte-identical to the catalog's moon and day records. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root // [{ id, name, scheme, accent, secondary, palette: {19 tokens}, ansi: {16} }] property var themes: root.fallbackThemes property string defaultDark: "moon" property string defaultLight: "day" property bool loaded: false property string lastError: "" function byId(id: string): var { return root.themes.find(theme => theme.id === id) ?? null; } // The palette every resolution falls back to: the default theme for the // scheme. Always complete, even before the file loads. function defaultPalette(scheme: string): var { const id = scheme === "light" ? root.defaultLight : root.defaultDark; const theme = root.byId(id) ?? root.fallbackThemes[scheme === "light" ? 1 : 0]; return theme.palette; } function themesFor(scheme: string): var { return root.themes.filter(theme => theme.scheme === scheme); } // A record is only accepted with every palette key present and valid, so // nothing downstream ever needs a per-token fallback. readonly property var paletteKeys: ["bg", "bgDark", "bgHighlight", "bgPanel", "bgPopover", "fg", "fgDim", "fgMuted", "gutter", "accentAlt", "cyan", "teal", "green", "yellow", "orange", "red", "redDeep", "magenta", "pink"] readonly property var ansiKeys: ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "brightBlack", "brightRed", "brightGreen", "brightYellow", "brightBlue", "brightMagenta", "brightCyan", "brightWhite"] function validHex(value: var): bool { return typeof value === "string" && /^#[0-9a-f]{6}$/.test(value); } function normalizeTheme(entry: var): var { if (!entry || typeof entry !== "object") return null; const id = String(entry.id ?? ""); const name = String(entry.name ?? ""); const scheme = String(entry.scheme ?? ""); if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(id) || name === "" || (scheme !== "dark" && scheme !== "light")) return null; if (!root.validHex(entry.accent) || !root.validHex(entry.secondary)) return null; const palette = {}; for (const key of root.paletteKeys) { const value = entry.palette?.[key]; if (!root.validHex(value)) return null; palette[key] = value; } const ansi = {}; for (const key of root.ansiKeys) { const value = entry.ansi?.[key]; if (!root.validHex(value)) return null; ansi[key] = value; } return { id: id, name: name, scheme: scheme, shipped: true, accent: entry.accent, secondary: entry.secondary, palette: palette, ansi: ansi }; } function consume(text: string): void { try { const parsed = JSON.parse(text); const themes = (parsed.themes ?? []) .map(entry => root.normalizeTheme(entry)) .filter(theme => theme !== null); if (themes.length === 0) throw new Error("no valid themes"); root.themes = themes; root.defaultDark = String(parsed.defaultDark ?? "moon"); root.defaultLight = String(parsed.defaultLight ?? "day"); root.loaded = true; root.lastError = ""; } catch (error) { root.lastError = "The theme catalog could not be read; using the built-in themes."; } } FileView { id: catalogFile path: Quickshell.shellDir + "/config/themes.json" printErrors: false onLoaded: root.consume(this.text()) onLoadFailed: root.lastError = "The theme catalog is missing; using the built-in themes." } // Byte-identical to the catalog's moon and day records — the shell's // pre-theme literals, pinned by theme-catalog-contract. readonly property var fallbackThemes: [ { id: "moon", name: "Tokyo Moon", scheme: "dark", shipped: true, accent: "#82aaff", secondary: "#b172b0", palette: { bg: "#222436", bgDark: "#1e2030", bgHighlight: "#2f334d", bgPanel: "#2e2f3d", bgPopover: "#21212f", fg: "#c8d3f5", fgDim: "#828bb8", fgMuted: "#636da6", gutter: "#3b4261", accentAlt: "#65bcff", cyan: "#86e1fc", teal: "#4fd6be", green: "#c3e88d", yellow: "#ffc777", orange: "#ff966c", red: "#ff757f", redDeep: "#c53b53", magenta: "#c099ff", pink: "#fca7ea" }, ansi: { black: "#1b1d2b", red: "#ff757f", green: "#c3e88d", yellow: "#ffc777", blue: "#82aaff", magenta: "#c099ff", cyan: "#86e1fc", white: "#828bb8", brightBlack: "#444a73", brightRed: "#ff8d94", brightGreen: "#c7fb6d", brightYellow: "#ffd8ab", brightBlue: "#9ab8ff", brightMagenta: "#caabff", brightCyan: "#b2ebff", brightWhite: "#c8d3f5" } }, { id: "day", name: "Tokyo Day", scheme: "light", shipped: true, accent: "#2e7de9", secondary: "#9854f1", palette: { bg: "#e1e2e7", bgDark: "#d3d5de", bgHighlight: "#c4c8da", bgPanel: "#d9dae3", bgPopover: "#eaeaee", fg: "#3760bf", fgDim: "#6172b0", fgMuted: "#848cb5", gutter: "#a8aecb", accentAlt: "#007197", cyan: "#007197", teal: "#118c74", green: "#587539", yellow: "#8c6c3e", orange: "#b15c00", red: "#f52a65", redDeep: "#c64343", magenta: "#9854f1", pink: "#d20065" }, ansi: { black: "#b4b5b9", red: "#f52a65", green: "#587539", yellow: "#8c6c3e", blue: "#2e7de9", magenta: "#9854f1", cyan: "#007197", white: "#6172b0", brightBlack: "#a1a6c5", brightRed: "#ff4774", brightGreen: "#5c8524", brightYellow: "#a27629", brightBlue: "#358aff", brightMagenta: "#a463ff", brightCyan: "#007ea8", brightWhite: "#3760bf" } } ] }