(theme: ThemeJson, mode: "dark" | "light")
| 239 | } |
| 240 | |
| 241 | export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { |
| 242 | const defs = theme.defs ?? {} |
| 243 | function resolveColor(c: ColorValue, chain: string[] = []): RGBA { |
| 244 | if (c instanceof RGBA) return c |
| 245 | if (typeof c === "string") { |
| 246 | if (c === "transparent" || c === "none") return RGBA.fromInts(0, 0, 0, 0) |
| 247 | |
| 248 | if (c.startsWith("#")) return RGBA.fromHex(c) |
| 249 | |
| 250 | if (chain.includes(c)) { |
| 251 | throw new Error(`Circular color reference: ${[...chain, c].join(" -> ")}`) |
| 252 | } |
| 253 | |
| 254 | const next = defs[c] ?? theme.theme[c as ThemeColor] |
| 255 | if (next === undefined) { |
| 256 | throw new Error(`Color reference "${c}" not found in defs or theme`) |
| 257 | } |
| 258 | return resolveColor(next, [...chain, c]) |
| 259 | } |
| 260 | if (typeof c === "number") { |
| 261 | return ansiToRgba(c) |
| 262 | } |
| 263 | return resolveColor(c[mode], chain) |
| 264 | } |
| 265 | |
| 266 | const resolved = Object.fromEntries( |
| 267 | Object.entries(theme.theme) |
| 268 | .filter(([key]) => key !== "selectedListItemText" && key !== "backgroundMenu" && key !== "thinkingOpacity") |
| 269 | .map(([key, value]) => { |
| 270 | return [key, resolveColor(value as ColorValue)] |
| 271 | }), |
| 272 | ) as Partial<Record<ThemeColor, RGBA>> |
| 273 | |
| 274 | // Handle selectedListItemText separately since it's optional |
| 275 | const hasSelectedListItemText = theme.theme.selectedListItemText !== undefined |
| 276 | if (hasSelectedListItemText) { |
| 277 | resolved.selectedListItemText = resolveColor(theme.theme.selectedListItemText!) |
| 278 | } else { |
| 279 | // Backward compatibility: if selectedListItemText is not defined, use background color |
| 280 | // This preserves the current behavior for all existing themes |
| 281 | resolved.selectedListItemText = resolved.background |
| 282 | } |
| 283 | |
| 284 | // Handle backgroundMenu - optional with fallback to backgroundElement |
| 285 | if (theme.theme.backgroundMenu !== undefined) { |
| 286 | resolved.backgroundMenu = resolveColor(theme.theme.backgroundMenu) |
| 287 | } else { |
| 288 | resolved.backgroundMenu = resolved.backgroundElement |
| 289 | } |
| 290 | |
| 291 | // Handle thinkingOpacity - optional with default of 0.6 |
| 292 | const thinkingOpacity = theme.theme.thinkingOpacity ?? 0.6 |
| 293 | |
| 294 | return { |
| 295 | ...resolved, |
| 296 | _hasSelectedListItemText: hasSelectedListItemText, |
| 297 | thinkingOpacity, |
| 298 | } as Theme |
no test coverage detected