import { app } from 'electron'; import * as fsp from 'fs/promises'; import * as path from 'path'; export interface SavedThemeFileDescriptor { fileName: string; modifiedAt: number; path: string; } const SAVED_THEME_FILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.json$/; function resolveSavedThemesPath(): string { return path.join(app.getPath('userData'), 'themes'); } async function ensureSavedThemesPath(): Promise { const themesPath = resolveSavedThemesPath(); await fsp.mkdir(themesPath, { recursive: true }); return themesPath; } function assertSavedThemeFileName(fileName: string): string { const normalized = typeof fileName === 'string' ? fileName.trim() : ''; if (!SAVED_THEME_FILE_NAME_PATTERN.test(normalized) || normalized.includes('..')) { throw new Error('Invalid saved theme file name.'); } return normalized; } async function resolveSavedThemeFilePath(fileName: string): Promise { const themesPath = await ensureSavedThemesPath(); return path.join(themesPath, assertSavedThemeFileName(fileName)); } export async function getSavedThemesPath(): Promise { return await ensureSavedThemesPath(); } export async function listSavedThemes(): Promise { const themesPath = await ensureSavedThemesPath(); const entries = await fsp.readdir(themesPath, { withFileTypes: true }); const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.json')); const descriptors = await Promise.all(files.map(async (entry) => { const filePath = path.join(themesPath, entry.name); const stats = await fsp.stat(filePath); return { fileName: entry.name, modifiedAt: Math.round(stats.mtimeMs), path: filePath } satisfies SavedThemeFileDescriptor; })); return descriptors.sort((left, right) => right.modifiedAt - left.modifiedAt || left.fileName.localeCompare(right.fileName)); } export async function readSavedTheme(fileName: string): Promise { const filePath = await resolveSavedThemeFilePath(fileName); return await fsp.readFile(filePath, 'utf8'); } export async function writeSavedTheme(fileName: string, text: string): Promise { const filePath = await resolveSavedThemeFilePath(fileName); await fsp.writeFile(filePath, text, 'utf8'); return true; } export async function deleteSavedTheme(fileName: string): Promise { const filePath = await resolveSavedThemeFilePath(fileName); try { await fsp.unlink(filePath); return true; } catch (error) { if ((error as { code?: string }).code === 'ENOENT') { return true; } throw error; } }