()
| 260 | |
| 261 | /** Parse every theme folder under the themes dir into a validated CustomTheme. */ |
| 262 | export async function listCustomThemes(): Promise<CustomTheme[]> { |
| 263 | const dir = getCustomThemesDir() |
| 264 | let entries: fsSync.Dirent[] |
| 265 | try { |
| 266 | entries = await fs.readdir(dir, { withFileTypes: true }) |
| 267 | } catch { |
| 268 | return [] |
| 269 | } |
| 270 | const themes: CustomTheme[] = [] |
| 271 | for (const entry of entries) { |
| 272 | if (!entry.isDirectory() || entry.name.startsWith('.')) continue |
| 273 | const slug = entry.name |
| 274 | const folder = path.join(dir, slug) |
| 275 | try { |
| 276 | const css = await fs.readFile(path.join(folder, 'theme.css'), 'utf8') |
| 277 | let manifestRaw: unknown = null |
| 278 | try { |
| 279 | manifestRaw = JSON.parse(await fs.readFile(path.join(folder, 'manifest.json'), 'utf8')) |
| 280 | } catch { |
| 281 | /* manifest optional — fall back to slug-named defaults */ |
| 282 | } |
| 283 | const m = parseThemeManifest(manifestRaw, slug) |
| 284 | themes.push({ |
| 285 | slug, |
| 286 | name: m.name, |
| 287 | author: m.author, |
| 288 | version: m.version, |
| 289 | description: m.description, |
| 290 | modes: m.modes, |
| 291 | css, |
| 292 | preview: m.preview |
| 293 | }) |
| 294 | } catch { |
| 295 | themes.push({ |
| 296 | slug, |
| 297 | name: slug, |
| 298 | modes: 'both', |
| 299 | css: '', |
| 300 | error: 'No readable theme.css here. Add one (see README.md) or use New theme.' |
| 301 | }) |
| 302 | } |
| 303 | } |
| 304 | themes.sort((a, b) => a.name.localeCompare(b.name)) |
| 305 | return themes |
| 306 | } |
| 307 | |
| 308 | function slugify(name: string): string { |
| 309 | return ( |
no test coverage detected