hermes-agent/apps/desktop/src/themes/install.ts
Brooklyn Nicholson 62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00

92 lines
3.6 KiB
TypeScript

/**
* Install desktop themes from external sources.
*
* The heavy lifting (network + .vsix unzip) lives in the Electron main process
* (`electron/vscode-marketplace.cjs`), reached via `window.hermesDesktop.themes`.
* Main hands back the raw theme JSON; we parse + convert + persist here so the
* conversion stays in one unit-testable place.
*/
import type { DesktopMarketplaceThemeResult } from '@/global'
import type { DesktopTheme } from './types'
import { installUserTheme } from './user-themes'
import { convertVscodeColorTheme, parseVscodeTheme, vscodeThemeSlug } from './vscode'
/** A `publisher.extension` id, e.g. `dracula-theme.theme-dracula`. */
export const MARKETPLACE_ID_RE = /^[\w-]+\.[\w-]+$/
/** Parse + convert + persist a pasted VS Code theme JSON. */
export function installVscodeThemeFromText(text: string, opts?: { label?: string; source?: string }): DesktopTheme {
const raw = parseVscodeTheme(text)
const { theme } = convertVscodeColorTheme(raw, opts)
return installUserTheme(theme)
}
/**
* Fold every color theme an extension contributes into ONE desktop theme family.
*
* Many extensions ship a light *and* a dark variant (GitHub, Solarized, Winter
* is Coming…). Rather than install them as separate flat entries — which made
* the light/dark toggle a no-op and let "install in dark mode" land on the light
* variant — we map the first light variant onto `colors` and the first dark
* variant onto `darkColors`. The result is a single picker entry whose light/dark
* toggle switches between the real variants. A single-variant extension fills
* both slots with its one palette (the toggle is a no-op, as it must be).
*/
export function buildThemeFromMarketplace(result: DesktopMarketplaceThemeResult): DesktopTheme {
if (!result.themes.length) {
throw new Error(`"${result.extensionId}" does not contribute any color themes.`)
}
const variants = result.themes.map(file => {
const raw = parseVscodeTheme(file.contents)
const label = file.label || raw.name || result.displayName
const { mode, theme } = convertVscodeColorTheme(raw, { label, source: result.extensionId })
return { mode, palette: theme.colors, terminal: theme.terminal }
})
const fallback = variants[0]
const light = variants.find(variant => variant.mode === 'light') ?? fallback
const dark = variants.find(variant => variant.mode === 'dark') ?? fallback
// The terminal ANSI palette tracks the painted variant the same way colors do
// (light → terminal, dark → darkTerminal); each falls back to the other so a
// single-variant import still themes the terminal in both modes.
const terminal = light.terminal ?? dark.terminal
const darkTerminal = dark.terminal ?? light.terminal
return {
name: vscodeThemeSlug(result.displayName),
label: result.displayName,
description: `VS Code · ${result.extensionId}`,
colors: light.palette,
darkColors: dark.palette,
...(terminal ? { terminal } : {}),
...(darkTerminal ? { darkTerminal } : {})
}
}
/**
* Download a Marketplace extension and install the theme family it contributes
* (see `buildThemeFromMarketplace`). Returns the single installed theme.
*/
export async function installVscodeThemeFromMarketplace(id: string): Promise<DesktopTheme> {
const trimmed = id.trim()
if (!MARKETPLACE_ID_RE.test(trimmed)) {
throw new Error('Expected a Marketplace id like "publisher.extension".')
}
const api = window.hermesDesktop?.themes
if (!api?.fetchMarketplace) {
throw new Error('Marketplace install is only available in the desktop app.')
}
const result = await api.fetchMarketplace(trimmed)
return installUserTheme(buildThemeFromMarketplace(result))
}