@tooee/themes 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,3 @@
1
- import { type ReactNode } from "react";
2
- import { SyntaxStyle } from "@opentui/core";
3
1
  type HexColor = `#${string}`;
4
2
  type RefName = string;
5
3
  type Variant = {
@@ -66,46 +64,8 @@ export interface ResolvedTheme {
66
64
  syntaxOperator: string;
67
65
  syntaxPunctuation: string;
68
66
  }
67
+ export declare const RESOLVED_KEYS: (keyof ResolvedTheme)[];
68
+ export declare const FALLBACKS: Record<string, string>;
69
69
  export declare function resolveTheme(json: ThemeJSON, mode: "dark" | "light"): ResolvedTheme;
70
- export declare function buildSyntaxStyle(resolved: ResolvedTheme): SyntaxStyle;
71
- export declare function loadThemes(): Map<string, ThemeJSON>;
72
- export declare function getThemeNames(): string[];
73
- export interface Theme {
74
- name: string;
75
- mode: "dark" | "light";
76
- colors: ResolvedTheme;
77
- syntax: SyntaxStyle;
78
- }
79
- export declare const defaultTheme: Theme;
80
- interface ThemeContextValue {
81
- theme: ResolvedTheme;
82
- syntax: SyntaxStyle;
83
- name: string;
84
- mode: "dark" | "light";
85
- }
86
- export interface ThemeProviderProps {
87
- /** Theme name (e.g. "tokyonight", "catppuccin", "dracula") */
88
- name?: string;
89
- /** Color mode */
90
- mode?: "dark" | "light";
91
- /** Full Theme object (overrides name/mode if provided) */
92
- theme?: Theme;
93
- children: ReactNode;
94
- }
95
- export declare function ThemeProvider({ name, mode, theme: themeProp, children }: ThemeProviderProps): ReactNode;
96
- export declare function useTheme(): ThemeContextValue;
97
- interface ThemeSwitcherContextValue extends ThemeContextValue {
98
- nextTheme: () => void;
99
- prevTheme: () => void;
100
- setTheme: (name: string) => void;
101
- allThemes: string[];
102
- }
103
- export interface ThemeSwitcherProviderProps {
104
- initialTheme?: string;
105
- initialMode?: "dark" | "light";
106
- children: ReactNode;
107
- }
108
- export declare function ThemeSwitcherProvider({ initialTheme, initialMode, children, }: ThemeSwitcherProviderProps): ReactNode;
109
- export declare function useThemeSwitcher(): ThemeSwitcherContextValue;
110
70
  export {};
111
- //# sourceMappingURL=theme.d.ts.map
71
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,KAAK,QAAQ,GAAG,IAAI,MAAM,EAAE,CAAA;AAC5B,KAAK,OAAO,GAAG,MAAM,CAAA;AACrB,KAAK,OAAO,GAAG;IAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAAA;AACtE,KAAK,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;AAE9C,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAA;IACzC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CAClC;AAMD,MAAM,WAAW,aAAa;IAE5B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IAEpB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,uBAAuB,EAAE,MAAM,CAAA;IAE/B,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,CAAA;IACtB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,gBAAgB,EAAE,MAAM,CAAA;IACxB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,MAAM,CAAA;IAEzB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IAEjB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAGD,eAAO,MAAM,aAAa,EAAE,CAAC,MAAM,aAAa,CAAC,EAqDhD,CAAA;AAGD,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAqD5C,CAAA;AAMD,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,aAAa,CAuBnF"}
package/dist/types.js ADDED
@@ -0,0 +1,145 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Theme JSON format (OpenCode-compatible)
3
+ // ---------------------------------------------------------------------------
4
+ // All keys of ResolvedTheme for iteration
5
+ export const RESOLVED_KEYS = [
6
+ "primary",
7
+ "secondary",
8
+ "accent",
9
+ "error",
10
+ "warning",
11
+ "success",
12
+ "info",
13
+ "text",
14
+ "textMuted",
15
+ "background",
16
+ "backgroundPanel",
17
+ "backgroundElement",
18
+ "border",
19
+ "borderActive",
20
+ "borderSubtle",
21
+ "cursorLine",
22
+ "selection",
23
+ "diffAdded",
24
+ "diffRemoved",
25
+ "diffContext",
26
+ "diffHunkHeader",
27
+ "diffHighlightAdded",
28
+ "diffHighlightRemoved",
29
+ "diffAddedBg",
30
+ "diffRemovedBg",
31
+ "diffContextBg",
32
+ "diffLineNumber",
33
+ "diffAddedLineNumberBg",
34
+ "diffRemovedLineNumberBg",
35
+ "markdownText",
36
+ "markdownHeading",
37
+ "markdownLink",
38
+ "markdownLinkText",
39
+ "markdownCode",
40
+ "markdownBlockQuote",
41
+ "markdownEmph",
42
+ "markdownStrong",
43
+ "markdownHorizontalRule",
44
+ "markdownListItem",
45
+ "markdownListEnumeration",
46
+ "markdownImage",
47
+ "markdownImageText",
48
+ "markdownCodeBlock",
49
+ "syntaxComment",
50
+ "syntaxKeyword",
51
+ "syntaxFunction",
52
+ "syntaxVariable",
53
+ "syntaxString",
54
+ "syntaxNumber",
55
+ "syntaxType",
56
+ "syntaxOperator",
57
+ "syntaxPunctuation",
58
+ ];
59
+ // Fallbacks used when a theme key is missing
60
+ export const FALLBACKS = {
61
+ primary: "#808080",
62
+ secondary: "#808080",
63
+ accent: "#808080",
64
+ error: "#808080",
65
+ warning: "#808080",
66
+ success: "#808080",
67
+ info: "#808080",
68
+ text: "#d4d4d4",
69
+ textMuted: "#808080",
70
+ background: "#1e1e1e",
71
+ backgroundPanel: "#1e1e1e",
72
+ backgroundElement: "#1e1e1e",
73
+ cursorLine: "#1e1e1e",
74
+ selection: "#1e1e1e",
75
+ border: "#808080",
76
+ borderActive: "#808080",
77
+ borderSubtle: "#808080",
78
+ diffAdded: "#4fd6be",
79
+ diffRemoved: "#c53b53",
80
+ diffContext: "#808080",
81
+ diffHunkHeader: "#808080",
82
+ diffHighlightAdded: "#4fd6be",
83
+ diffHighlightRemoved: "#c53b53",
84
+ diffAddedBg: "#1e3a1e",
85
+ diffRemovedBg: "#3a1e1e",
86
+ diffContextBg: "#1e1e1e",
87
+ diffLineNumber: "#808080",
88
+ diffAddedLineNumberBg: "#1e3a1e",
89
+ diffRemovedLineNumberBg: "#3a1e1e",
90
+ markdownText: "#d4d4d4",
91
+ markdownHeading: "#808080",
92
+ markdownLink: "#808080",
93
+ markdownLinkText: "#808080",
94
+ markdownCode: "#808080",
95
+ markdownBlockQuote: "#808080",
96
+ markdownEmph: "#808080",
97
+ markdownStrong: "#808080",
98
+ markdownHorizontalRule: "#808080",
99
+ markdownListItem: "#808080",
100
+ markdownListEnumeration: "#808080",
101
+ markdownImage: "#808080",
102
+ markdownImageText: "#808080",
103
+ markdownCodeBlock: "#d4d4d4",
104
+ syntaxComment: "#808080",
105
+ syntaxKeyword: "#808080",
106
+ syntaxFunction: "#808080",
107
+ syntaxVariable: "#808080",
108
+ syntaxString: "#808080",
109
+ syntaxNumber: "#808080",
110
+ syntaxType: "#808080",
111
+ syntaxOperator: "#808080",
112
+ syntaxPunctuation: "#808080",
113
+ };
114
+ // ---------------------------------------------------------------------------
115
+ // Resolution
116
+ // ---------------------------------------------------------------------------
117
+ export function resolveTheme(json, mode) {
118
+ const defs = json.defs ?? {};
119
+ function resolveColor(c) {
120
+ if (typeof c === "string") {
121
+ if (c === "transparent" || c === "none")
122
+ return "#00000000";
123
+ if (c.startsWith("#"))
124
+ return c;
125
+ if (defs[c] != null)
126
+ return resolveColor(defs[c]);
127
+ if (json.theme[c] !== undefined)
128
+ return resolveColor(json.theme[c]);
129
+ return "#808080";
130
+ }
131
+ return resolveColor(c[mode]);
132
+ }
133
+ const result = {};
134
+ for (const key of RESOLVED_KEYS) {
135
+ const val = json.theme[key];
136
+ result[key] = val !== undefined ? resolveColor(val) : (FALLBACKS[key] ?? "#808080");
137
+ }
138
+ // Dynamic fallbacks that reference other resolved keys
139
+ if (json.theme["cursorLine"] === undefined)
140
+ result.cursorLine = result.backgroundElement;
141
+ if (json.theme["selection"] === undefined)
142
+ result.selection = result.backgroundPanel;
143
+ return result;
144
+ }
145
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@tooee/themes",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "Theme management with 34 bundled themes for Tooee",
5
+ "keywords": [
6
+ "cli",
7
+ "colors",
8
+ "opentui",
9
+ "terminal",
10
+ "themes",
11
+ "tui"
12
+ ],
13
+ "homepage": "https://github.com/gingerhendrix/tooee",
14
+ "bugs": "https://github.com/gingerhendrix/tooee/issues",
5
15
  "license": "MIT",
6
16
  "author": "Gareth Andrew",
7
17
  "repository": {
@@ -9,15 +19,10 @@
9
19
  "url": "https://github.com/gingerhendrix/tooee.git",
10
20
  "directory": "packages/themes"
11
21
  },
12
- "homepage": "https://github.com/gingerhendrix/tooee",
13
- "bugs": "https://github.com/gingerhendrix/tooee/issues",
14
- "keywords": [
15
- "tui",
16
- "terminal",
17
- "cli",
18
- "opentui",
19
- "themes",
20
- "colors"
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "themes"
21
26
  ],
22
27
  "type": "module",
23
28
  "exports": {
@@ -28,16 +33,12 @@
28
33
  }
29
34
  }
30
35
  },
31
- "files": [
32
- "dist",
33
- "src",
34
- "themes"
35
- ],
36
36
  "scripts": {
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@tooee/config": "0.1.9"
40
+ "@tooee/config": "0.1.12",
41
+ "@tooee/fuzzy": "0.1.12"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@opentui/core": "^0.1.86",
@@ -1,6 +1,7 @@
1
1
  import { useState, useMemo, useCallback } from "react"
2
2
  import { useKeyboard } from "@opentui/react"
3
- import { useTheme } from "./theme.js"
3
+ import { fuzzyMatch } from "@tooee/fuzzy"
4
+ import { useTheme } from "./context.js"
4
5
 
5
6
  export interface ThemePickerEntry {
6
7
  id: string
@@ -15,28 +16,6 @@ interface ThemePickerProps {
15
16
  onNavigate: (name: string) => void
16
17
  }
17
18
 
18
- function fuzzyMatch(query: string, text: string): number | null {
19
- const lowerQuery = query.toLowerCase()
20
- const lowerText = text.toLowerCase()
21
-
22
- let qi = 0
23
- let score = 0
24
- let lastMatchIndex = -2
25
-
26
- for (let ti = 0; ti < lowerText.length && qi < lowerQuery.length; ti++) {
27
- if (lowerText[ti] === lowerQuery[qi]) {
28
- if (ti === 0) score += 3
29
- else if (" -./".includes(lowerText[ti - 1]!)) score += 2
30
- else if (ti === lastMatchIndex + 1) score += 1
31
-
32
- lastMatchIndex = ti
33
- qi++
34
- }
35
- }
36
-
37
- return qi === lowerQuery.length ? score : null
38
- }
39
-
40
19
  export function ThemePicker({
41
20
  entries,
42
21
  currentTheme,
@@ -0,0 +1,142 @@
1
+ import { createContext, useContext, useState, useCallback, type ReactNode } from "react"
2
+ import { type SyntaxStyle } from "@opentui/core"
3
+ import { writeGlobalConfig } from "@tooee/config"
4
+ import type { ResolvedTheme } from "./types.js"
5
+ import {
6
+ type Theme,
7
+ buildTheme,
8
+ getThemeNames,
9
+ defaultTheme,
10
+ DEFAULT_THEME_NAME,
11
+ DEFAULT_MODE,
12
+ } from "./loader.js"
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Context
16
+ // ---------------------------------------------------------------------------
17
+
18
+ interface ThemeContextValue {
19
+ theme: ResolvedTheme
20
+ syntax: SyntaxStyle
21
+ name: string
22
+ mode: "dark" | "light"
23
+ }
24
+
25
+ const ThemeContext = createContext<ThemeContextValue>({
26
+ theme: defaultTheme.colors,
27
+ syntax: defaultTheme.syntax,
28
+ name: defaultTheme.name,
29
+ mode: defaultTheme.mode,
30
+ })
31
+
32
+ export interface ThemeProviderProps {
33
+ /** Theme name (e.g. "tokyonight", "catppuccin", "dracula") */
34
+ name?: string
35
+ /** Color mode */
36
+ mode?: "dark" | "light"
37
+ /** Full Theme object (overrides name/mode if provided) */
38
+ theme?: Theme
39
+ children: ReactNode
40
+ }
41
+
42
+ export function ThemeProvider({ name, mode, theme: themeProp, children }: ThemeProviderProps) {
43
+ const resolved = themeProp
44
+ ? {
45
+ theme: themeProp.colors,
46
+ syntax: themeProp.syntax,
47
+ name: themeProp.name,
48
+ mode: themeProp.mode,
49
+ }
50
+ : (() => {
51
+ const t = buildTheme(name ?? DEFAULT_THEME_NAME, mode ?? DEFAULT_MODE)
52
+ return { theme: t.colors, syntax: t.syntax, name: t.name, mode: t.mode }
53
+ })()
54
+
55
+ return <ThemeContext.Provider value={resolved}>{children}</ThemeContext.Provider>
56
+ }
57
+
58
+ export function useTheme(): ThemeContextValue {
59
+ return useContext(ThemeContext)
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // ThemeSwitcherProvider + useThemeSwitcher
64
+ // ---------------------------------------------------------------------------
65
+
66
+ interface ThemeSwitcherContextValue extends ThemeContextValue {
67
+ nextTheme: () => void
68
+ prevTheme: () => void
69
+ setTheme: (name: string, opts?: { persist?: boolean }) => void
70
+ allThemes: string[]
71
+ }
72
+
73
+ const ThemeSwitcherContext = createContext<ThemeSwitcherContextValue | null>(null)
74
+
75
+ export interface ThemeSwitcherProviderProps {
76
+ initialTheme?: string
77
+ initialMode?: "dark" | "light"
78
+ children: ReactNode
79
+ }
80
+
81
+ export function ThemeSwitcherProvider({
82
+ initialTheme,
83
+ initialMode,
84
+ children,
85
+ }: ThemeSwitcherProviderProps) {
86
+ const allThemes = getThemeNames()
87
+ const [themeName, setThemeName] = useState(initialTheme ?? DEFAULT_THEME_NAME)
88
+ const [mode, _setMode] = useState<"dark" | "light">(initialMode ?? DEFAULT_MODE)
89
+
90
+ const theme = buildTheme(themeName, mode)
91
+
92
+ const nextTheme = useCallback(() => {
93
+ const idx = allThemes.indexOf(themeName)
94
+ const next = allThemes[(idx + 1) % allThemes.length]
95
+ setThemeName(next)
96
+ writeGlobalConfig({ theme: { name: next, mode } })
97
+ }, [allThemes, mode, themeName])
98
+
99
+ const prevTheme = useCallback(() => {
100
+ const idx = allThemes.indexOf(themeName)
101
+ const prev = allThemes[(idx - 1 + allThemes.length) % allThemes.length]
102
+ setThemeName(prev)
103
+ writeGlobalConfig({ theme: { name: prev, mode } })
104
+ }, [allThemes, mode, themeName])
105
+
106
+ const setThemeByName = useCallback(
107
+ (name: string, opts?: { persist?: boolean }) => {
108
+ setThemeName(name)
109
+ if (opts?.persist) {
110
+ writeGlobalConfig({ theme: { name, mode } })
111
+ }
112
+ },
113
+ [mode],
114
+ )
115
+
116
+ const value: ThemeSwitcherContextValue = {
117
+ theme: theme.colors,
118
+ syntax: theme.syntax,
119
+ name: theme.name,
120
+ mode,
121
+ nextTheme,
122
+ prevTheme,
123
+ setTheme: setThemeByName,
124
+ allThemes,
125
+ }
126
+
127
+ return (
128
+ <ThemeSwitcherContext.Provider value={value}>
129
+ <ThemeContext.Provider
130
+ value={{ theme: theme.colors, syntax: theme.syntax, name: theme.name, mode }}
131
+ >
132
+ {children}
133
+ </ThemeContext.Provider>
134
+ </ThemeSwitcherContext.Provider>
135
+ )
136
+ }
137
+
138
+ export function useThemeSwitcher(): ThemeSwitcherContextValue {
139
+ const ctx = useContext(ThemeSwitcherContext)
140
+ if (!ctx) throw new Error("useThemeSwitcher must be used within ThemeSwitcherProvider")
141
+ return ctx
142
+ }
package/src/index.ts CHANGED
@@ -1,21 +1,13 @@
1
- export {
2
- ThemeProvider,
3
- ThemeSwitcherProvider,
4
- useTheme,
5
- useThemeSwitcher,
6
- defaultTheme,
7
- resolveTheme,
8
- buildSyntaxStyle,
9
- loadThemes,
10
- getThemeNames,
11
- } from "./theme.js"
12
- export type {
13
- Theme,
14
- ThemeJSON,
15
- ResolvedTheme,
16
- ThemeProviderProps,
17
- ThemeSwitcherProviderProps,
18
- } from "./theme.js"
1
+ export { resolveTheme } from "./types.js"
2
+ export type { ThemeJSON, ResolvedTheme } from "./types.js"
3
+
4
+ export { buildSyntaxStyle } from "./syntax-rules.js"
5
+
6
+ export { loadThemes, getThemeNames, defaultTheme } from "./loader.js"
7
+ export type { Theme } from "./loader.js"
8
+
9
+ export { ThemeProvider, ThemeSwitcherProvider, useTheme, useThemeSwitcher } from "./context.js"
10
+ export type { ThemeProviderProps, ThemeSwitcherProviderProps } from "./context.js"
19
11
 
20
12
  export { ThemePicker } from "./ThemePicker.js"
21
13
  export type { ThemePickerEntry } from "./ThemePicker.js"
package/src/loader.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { type SyntaxStyle } from "@opentui/core"
2
+ import { readFileSync, readdirSync, existsSync } from "fs"
3
+ import { join, basename, dirname } from "path"
4
+ import { type ThemeJSON, type ResolvedTheme, resolveTheme } from "./types.js"
5
+ import { buildSyntaxStyle } from "./syntax-rules.js"
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Theme loading
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface Theme {
12
+ name: string
13
+ mode: "dark" | "light"
14
+ colors: ResolvedTheme
15
+ syntax: SyntaxStyle
16
+ }
17
+
18
+ /** Cache of loaded theme JSONs by name */
19
+ const themeJsonCache = new Map<string, ThemeJSON>()
20
+
21
+ function loadJsonThemesFromDir(dir: string, target: Map<string, ThemeJSON>) {
22
+ try {
23
+ if (!existsSync(dir)) return
24
+ for (const file of readdirSync(dir)) {
25
+ if (!file.endsWith(".json")) continue
26
+ const name = basename(file, ".json")
27
+ try {
28
+ const content = readFileSync(join(dir, file), "utf-8")
29
+ target.set(name, JSON.parse(content) as ThemeJSON)
30
+ } catch {
31
+ // skip invalid files
32
+ }
33
+ }
34
+ } catch {
35
+ // dir not readable
36
+ }
37
+ }
38
+
39
+ /** Load all bundled themes from packages/themes/themes/ */
40
+ function loadBundledThemes(): Map<string, ThemeJSON> {
41
+ if (themeJsonCache.size > 0) return themeJsonCache
42
+
43
+ // Bundled themes
44
+ const bundledDir = join(dirname(new URL(import.meta.url).pathname), "..", "themes")
45
+ loadJsonThemesFromDir(bundledDir, themeJsonCache)
46
+
47
+ // XDG config: ~/.config/tooee/themes/
48
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? "", ".config")
49
+ loadJsonThemesFromDir(join(xdgConfig, "tooee", "themes"), themeJsonCache)
50
+
51
+ // Project-local: search upward for .tooee/themes/
52
+ let dir = process.cwd()
53
+ const seen = new Set<string>()
54
+ while (dir && !seen.has(dir)) {
55
+ seen.add(dir)
56
+ loadJsonThemesFromDir(join(dir, ".tooee", "themes"), themeJsonCache)
57
+ const parent = dirname(dir)
58
+ if (parent === dir) break
59
+ dir = parent
60
+ }
61
+
62
+ return themeJsonCache
63
+ }
64
+
65
+ export function loadThemes(): Map<string, ThemeJSON> {
66
+ return loadBundledThemes()
67
+ }
68
+
69
+ export function getThemeNames(): string[] {
70
+ return Array.from(loadThemes().keys()).sort()
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Default theme
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export const DEFAULT_THEME_NAME = "tokyonight"
78
+ export const DEFAULT_MODE: "dark" | "light" = "dark"
79
+
80
+ export function buildTheme(name: string, mode: "dark" | "light"): Theme {
81
+ const themes = loadThemes()
82
+ const json = themes.get(name)
83
+ if (!json) {
84
+ // Fall back to tokyonight, then first available, then hardcoded
85
+ const fallbackJson = themes.get(DEFAULT_THEME_NAME) ?? themes.values().next().value
86
+ if (fallbackJson) {
87
+ const resolved = resolveTheme(fallbackJson, mode)
88
+ return { name, mode, colors: resolved, syntax: buildSyntaxStyle(resolved) }
89
+ }
90
+ // Absolute fallback — hardcoded Tokyo Night colors
91
+ return hardcodedDefaultTheme
92
+ }
93
+ const resolved = resolveTheme(json, mode)
94
+ return { name, mode, colors: resolved, syntax: buildSyntaxStyle(resolved) }
95
+ }
96
+
97
+ const hardcodedDefaultTheme: Theme = (() => {
98
+ const colors: ResolvedTheme = {
99
+ primary: "#7aa2f7",
100
+ secondary: "#bb9af7",
101
+ accent: "#7dcfff",
102
+ error: "#f7768e",
103
+ warning: "#e0af68",
104
+ success: "#9ece6a",
105
+ info: "#7aa2f7",
106
+ text: "#c0caf5",
107
+ textMuted: "#565f89",
108
+ background: "#1a1b26",
109
+ backgroundPanel: "#1e2030",
110
+ backgroundElement: "#222436",
111
+ cursorLine: "#222436",
112
+ selection: "#1e2030",
113
+ border: "#565f89",
114
+ borderActive: "#737aa2",
115
+ borderSubtle: "#414868",
116
+ diffAdded: "#4fd6be",
117
+ diffRemoved: "#c53b53",
118
+ diffContext: "#828bb8",
119
+ diffHunkHeader: "#828bb8",
120
+ diffHighlightAdded: "#b8db87",
121
+ diffHighlightRemoved: "#e26a75",
122
+ diffAddedBg: "#20303b",
123
+ diffRemovedBg: "#37222c",
124
+ diffContextBg: "#1e2030",
125
+ diffLineNumber: "#222436",
126
+ diffAddedLineNumberBg: "#1b2b34",
127
+ diffRemovedLineNumberBg: "#2d1f26",
128
+ markdownText: "#c0caf5",
129
+ markdownHeading: "#bb9af7",
130
+ markdownLink: "#7aa2f7",
131
+ markdownLinkText: "#7dcfff",
132
+ markdownCode: "#9ece6a",
133
+ markdownBlockQuote: "#e0af68",
134
+ markdownEmph: "#e0af68",
135
+ markdownStrong: "#ff966c",
136
+ markdownHorizontalRule: "#565f89",
137
+ markdownListItem: "#7aa2f7",
138
+ markdownListEnumeration: "#7dcfff",
139
+ markdownImage: "#7aa2f7",
140
+ markdownImageText: "#7dcfff",
141
+ markdownCodeBlock: "#c0caf5",
142
+ syntaxComment: "#565f89",
143
+ syntaxKeyword: "#bb9af7",
144
+ syntaxFunction: "#7aa2f7",
145
+ syntaxVariable: "#c0caf5",
146
+ syntaxString: "#9ece6a",
147
+ syntaxNumber: "#ff9e64",
148
+ syntaxType: "#2ac3de",
149
+ syntaxOperator: "#89ddff",
150
+ syntaxPunctuation: "#a9b1d6",
151
+ }
152
+ return { name: DEFAULT_THEME_NAME, mode: DEFAULT_MODE, colors, syntax: buildSyntaxStyle(colors) }
153
+ })()
154
+
155
+ export const defaultTheme: Theme = hardcodedDefaultTheme