@fresh-editor/fresh-editor 0.2.20 → 0.2.22

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.
@@ -333,8 +333,10 @@ interface ThemeEditorState {
333
333
  themeData: Record<string, unknown>;
334
334
  /** Original theme data (for change detection) */
335
335
  originalThemeData: Record<string, unknown>;
336
- /** Theme name */
336
+ /** Theme display name */
337
337
  themeName: string;
338
+ /** Theme registry key (for lookups) */
339
+ themeKey: string;
338
340
  /** Theme file path (null for new themes) */
339
341
  themePath: string | null;
340
342
  /** Expanded sections */
@@ -345,8 +347,10 @@ interface ThemeEditorState {
345
347
  selectedIndex: number;
346
348
  /** Whether there are unsaved changes */
347
349
  hasChanges: boolean;
348
- /** Available built-in themes */
349
- builtinThemes: string[];
350
+ /** All themes from registry: key → {name, pack} */
351
+ themeRegistry: Map<string, {name: string; pack: string}>;
352
+ /** Keys of builtin themes (empty pack) */
353
+ builtinKeys: Set<string>;
350
354
  /** Pending save name for overwrite confirmation */
351
355
  pendingSaveName: string | null;
352
356
  /** Whether current theme is a built-in (requires Save As) */
@@ -355,6 +359,8 @@ interface ThemeEditorState {
355
359
  savedCursorPath: string | null;
356
360
  /** Whether to close the editor after a successful save */
357
361
  closeAfterSave: boolean;
362
+ /** Whether the Save As prompt has been pre-filled (to distinguish first vs second Enter) */
363
+ saveAsPreFilled: boolean;
358
364
  /** Which panel has focus */
359
365
  focusPanel: "tree" | "picker";
360
366
  /** Focus target within picker panel */
@@ -409,17 +415,20 @@ const state: ThemeEditorState = {
409
415
  sourceBufferId: null,
410
416
  themeData: {},
411
417
  originalThemeData: {},
412
- themeName: "custom",
418
+ themeName: "",
419
+ themeKey: "",
413
420
  themePath: null,
414
421
  expandedSections: new Set(["editor", "syntax"]),
415
422
  visibleFields: [],
416
423
  selectedIndex: 0,
417
424
  hasChanges: false,
418
- builtinThemes: [],
425
+ themeRegistry: new Map(),
426
+ builtinKeys: new Set(),
419
427
  pendingSaveName: null,
420
428
  isBuiltin: false,
421
429
  savedCursorPath: null,
422
430
  closeAfterSave: false,
431
+ saveAsPreFilled: false,
423
432
  focusPanel: "tree",
424
433
  pickerFocus: { type: "hex-input" },
425
434
  filterText: "",
@@ -666,33 +675,50 @@ function findThemesDir(): string {
666
675
  /**
667
676
  * Load list of available built-in themes
668
677
  */
669
- async function loadBuiltinThemes(): Promise<string[]> {
678
+ /**
679
+ * Load all themes from the registry, returning a map of key → display name.
680
+ *
681
+ * The registry is keyed by unique keys (repo URLs, file:// paths, or bare
682
+ * names for builtins). Each value contains a `name` field (display name)
683
+ * and `_key`/`_pack` metadata.
684
+ */
685
+ /**
686
+ * Load theme registry and populate state.themeRegistry + state.builtinKeys.
687
+ */
688
+ async function loadThemeRegistry(): Promise<void> {
670
689
  try {
671
- editor.debug("[theme_editor] loadBuiltinThemes: calling editor.getBuiltinThemes()");
690
+ editor.debug("[theme_editor] loadThemeRegistry: calling editor.getBuiltinThemes()");
672
691
  const rawThemes = editor.getBuiltinThemes();
673
- editor.debug(`[theme_editor] loadBuiltinThemes: got rawThemes type=${typeof rawThemes}`);
674
- // getBuiltinThemes returns a JSON string, need to parse it
675
- const builtinThemes = typeof rawThemes === "string"
676
- ? JSON.parse(rawThemes) as Record<string, string>
677
- : rawThemes as Record<string, string>;
678
- editor.debug(`[theme_editor] loadBuiltinThemes: parsed ${Object.keys(builtinThemes).length} themes`);
679
- return Object.keys(builtinThemes);
692
+ const themes = typeof rawThemes === "string"
693
+ ? JSON.parse(rawThemes) as Record<string, Record<string, unknown>>
694
+ : rawThemes as Record<string, Record<string, unknown>>;
695
+ state.themeRegistry = new Map();
696
+ state.builtinKeys = new Set();
697
+ for (const [key, data] of Object.entries(themes)) {
698
+ const name = (data?.name as string) || key;
699
+ const pack = (data?._pack as string) || "";
700
+ state.themeRegistry.set(key, {name, pack});
701
+ // Builtin themes have an empty pack; user themes start with "user"
702
+ if (!pack || (!pack.startsWith("user") && !pack.startsWith("pkg"))) {
703
+ state.builtinKeys.add(key);
704
+ }
705
+ }
706
+ editor.debug(`[theme_editor] loadThemeRegistry: loaded ${state.themeRegistry.size} themes (${state.builtinKeys.size} builtin)`);
680
707
  } catch (e) {
681
- editor.debug(`[theme_editor] Failed to load built-in themes list: ${e}`);
708
+ editor.debug(`[theme_editor] Failed to load theme registry: ${e}`);
682
709
  throw e;
683
710
  }
684
711
  }
685
712
 
686
713
  /**
687
- * Load theme data by name from the in-memory theme registry.
688
- * Works for all theme types (builtin, user, package) — no file I/O needed.
714
+ * Load theme data by key from the in-memory theme registry.
689
715
  */
690
- function loadThemeFile(name: string): Record<string, unknown> | null {
716
+ function loadThemeFile(key: string): Record<string, unknown> | null {
691
717
  try {
692
- const data = editor.getThemeData(name);
718
+ const data = editor.getThemeData(key);
693
719
  return data as Record<string, unknown> | null;
694
720
  } catch (e) {
695
- editor.debug(`[theme_editor] Failed to load theme data for '${name}': ${e}`);
721
+ editor.debug(`[theme_editor] Failed to load theme data for '${key}': ${e}`);
696
722
  return null;
697
723
  }
698
724
  }
@@ -1556,28 +1582,17 @@ async function onThemeOpenPromptConfirmed(args: {
1556
1582
  }): Promise<boolean> {
1557
1583
  if (args.prompt_type !== "theme-open") return true;
1558
1584
 
1559
- const value = args.input.trim();
1560
-
1561
- // Parse the value to determine if it's user or builtin
1562
- let isBuiltin = false;
1563
- let themeName = value;
1564
-
1565
- if (value.startsWith("user:")) {
1566
- themeName = value.slice(5);
1567
- isBuiltin = false;
1568
- } else if (value.startsWith("builtin:")) {
1569
- themeName = value.slice(8);
1570
- isBuiltin = true;
1571
- } else {
1572
- // Fallback: check if it's a builtin theme
1573
- isBuiltin = state.builtinThemes.includes(value);
1574
- }
1585
+ const key = args.input.trim();
1586
+ const isBuiltin = state.builtinKeys.has(key);
1587
+ const entry = state.themeRegistry.get(key);
1588
+ const themeName = entry?.name || key;
1575
1589
 
1576
- const themeData = loadThemeFile(themeName);
1590
+ const themeData = loadThemeFile(key);
1577
1591
  if (themeData) {
1578
1592
  state.themeData = deepClone(themeData);
1579
1593
  state.originalThemeData = deepClone(themeData);
1580
1594
  state.themeName = themeName;
1595
+ state.themeKey = key;
1581
1596
  state.themePath = null;
1582
1597
  state.isBuiltin = isBuiltin;
1583
1598
  state.hasChanges = false;
@@ -1604,10 +1619,31 @@ async function onThemeSaveAsPromptConfirmed(args: {
1604
1619
 
1605
1620
  const name = args.input.trim();
1606
1621
  if (name) {
1622
+ // If user accepted a suggestion without typing, pre-fill the prompt so they can edit the name
1623
+ if (args.selected_index !== null && !state.saveAsPreFilled) {
1624
+ state.saveAsPreFilled = true;
1625
+ editor.startPromptWithInitial(editor.t("prompt.save_as"), "theme-save-as", name);
1626
+ editor.setPromptSuggestions([{
1627
+ text: state.themeName,
1628
+ description: state.isBuiltin
1629
+ ? editor.t("suggestion.current_builtin")
1630
+ : editor.t("suggestion.current"),
1631
+ value: state.themeName,
1632
+ }]);
1633
+ return true;
1634
+ }
1635
+ state.saveAsPreFilled = false;
1636
+
1607
1637
  // Reject names that match a built-in theme
1608
- if (state.builtinThemes.includes(name)) {
1609
- editor.setStatus(editor.t("error.name_is_builtin", { name }));
1610
- theme_editor_save_as();
1638
+ if (state.builtinKeys.has(name)) {
1639
+ editor.startPromptWithInitial(editor.t("prompt.save_as_builtin_error"), "theme-save-as", name);
1640
+ editor.setPromptSuggestions([{
1641
+ text: state.themeName,
1642
+ description: state.isBuiltin
1643
+ ? editor.t("suggestion.current_builtin")
1644
+ : editor.t("suggestion.current"),
1645
+ value: state.themeName,
1646
+ }]);
1611
1647
  return true;
1612
1648
  }
1613
1649
 
@@ -1669,30 +1705,19 @@ async function onThemeSelectInitialPromptConfirmed(args: {
1669
1705
  }
1670
1706
  editor.debug(`[theme_editor] prompt_type matched, processing selection...`);
1671
1707
 
1672
- const value = args.input.trim();
1673
-
1674
- // Parse the value to determine if it's user or builtin
1675
- let isBuiltin = false;
1676
- let themeName = value;
1677
-
1678
- if (value.startsWith("user:")) {
1679
- themeName = value.slice(5);
1680
- isBuiltin = false;
1681
- } else if (value.startsWith("builtin:")) {
1682
- themeName = value.slice(8);
1683
- isBuiltin = true;
1684
- } else {
1685
- // Fallback: check if it's a builtin theme
1686
- isBuiltin = state.builtinThemes.includes(value);
1687
- }
1708
+ const key = args.input.trim();
1709
+ const isBuiltin = state.builtinKeys.has(key);
1710
+ const entry = state.themeRegistry.get(key);
1711
+ const themeName = entry?.name || key;
1688
1712
 
1689
1713
  editor.debug(editor.t("status.loading"));
1690
1714
 
1691
- const themeData = loadThemeFile(themeName);
1715
+ const themeData = loadThemeFile(key);
1692
1716
  if (themeData) {
1693
1717
  state.themeData = deepClone(themeData);
1694
1718
  state.originalThemeData = deepClone(themeData);
1695
1719
  state.themeName = themeName;
1720
+ state.themeKey = key;
1696
1721
  state.themePath = null;
1697
1722
  state.isBuiltin = isBuiltin;
1698
1723
  state.hasChanges = false;
@@ -1734,6 +1759,11 @@ async function saveTheme(name?: string, restorePath?: string | null): Promise<bo
1734
1759
  // (must match Rust's normalize_theme_name so config name matches filename)
1735
1760
  const themeName = (name || state.themeName).toLowerCase().replace(/[_ ]/g, "-");
1736
1761
 
1762
+ if (!themeName) {
1763
+ editor.setStatus(editor.t("status.save_failed", { error: "No theme name" }));
1764
+ return false;
1765
+ }
1766
+
1737
1767
  try {
1738
1768
  // Build a complete theme object from all known fields.
1739
1769
  // This ensures we always write every field, even if state.themeData
@@ -1757,6 +1787,7 @@ async function saveTheme(name?: string, restorePath?: string | null): Promise<bo
1757
1787
 
1758
1788
  state.themePath = savedPath;
1759
1789
  state.themeName = themeName;
1790
+ state.themeKey = `file://${savedPath}`;
1760
1791
  state.isBuiltin = false; // After saving, it's now a user theme
1761
1792
  state.originalThemeData = deepClone(state.themeData);
1762
1793
  state.hasChanges = false;
@@ -2005,20 +2036,24 @@ async function onThemeInspectKey(data: {
2005
2036
  // Save context
2006
2037
  state.sourceSplitId = editor.getActiveSplitId();
2007
2038
  state.sourceBufferId = editor.getActiveBufferId();
2008
- state.builtinThemes = await loadBuiltinThemes();
2009
-
2010
- // Auto-load the current theme (builtin or user)
2011
- const isBuiltin = state.builtinThemes.includes(data.theme_name);
2012
- const themeData = loadThemeFile(data.theme_name);
2039
+ await loadThemeRegistry();
2040
+
2041
+ // Auto-load the current theme (data.theme_name is the config key)
2042
+ const themeKey = data.theme_name;
2043
+ const isBuiltin = state.builtinKeys.has(themeKey);
2044
+ const entry = state.themeRegistry.get(themeKey);
2045
+ const themeName = entry?.name || themeKey;
2046
+ const themeData = loadThemeFile(themeKey);
2013
2047
  if (themeData) {
2014
2048
  state.themeData = deepClone(themeData);
2015
2049
  state.originalThemeData = deepClone(themeData);
2016
- state.themeName = data.theme_name;
2050
+ state.themeName = themeName;
2051
+ state.themeKey = themeKey;
2017
2052
  state.themePath = null;
2018
2053
  state.isBuiltin = isBuiltin;
2019
2054
  state.hasChanges = false;
2020
2055
  } else {
2021
- editor.setStatus(`Failed to load theme '${data.theme_name}'`);
2056
+ editor.setStatus(`Failed to load theme '${themeName}'`);
2022
2057
  return;
2023
2058
  }
2024
2059
 
@@ -2420,46 +2455,35 @@ async function open_theme_editor() : Promise<void> {
2420
2455
 
2421
2456
  editor.debug("[theme_editor] loading builtin themes...");
2422
2457
  // Load available themes
2423
- state.builtinThemes = await loadBuiltinThemes();
2424
- editor.debug(`[theme_editor] loaded ${state.builtinThemes.length} builtin themes`);
2458
+ await loadThemeRegistry();
2459
+ editor.debug(`[theme_editor] loaded ${state.themeRegistry.size} themes (${state.builtinKeys.size} builtin)`);
2425
2460
 
2426
- // Get current theme name from config
2461
+ // Get current theme key from config
2427
2462
  const config = editor.getConfig() as Record<string, unknown>;
2428
- const currentThemeName = (config?.theme as string) || "dark";
2463
+ const currentThemeKey = (config?.theme as string) || "dark";
2429
2464
 
2430
2465
  // Prompt user to select which theme to edit
2431
2466
  editor.startPrompt(editor.t("prompt.select_theme_to_edit"), "theme-select-initial");
2432
2467
 
2433
2468
  const suggestions: PromptSuggestion[] = [];
2434
2469
 
2435
- // Add user themes first (from themes directory)
2436
- const userThemesDir = editor.getThemesDir();
2437
- try {
2438
- const entries = editor.readDir(userThemesDir);
2439
- for (const e of entries) {
2440
- if (e.is_file && e.name.endsWith(".json")) {
2441
- const name = e.name.replace(".json", "");
2442
- const isCurrent = name === currentThemeName;
2443
- suggestions.push({
2444
- text: name,
2445
- description: isCurrent ? editor.t("suggestion.user_theme_current") : editor.t("suggestion.user_theme"),
2446
- value: `user:${name}`,
2447
- });
2448
- }
2470
+ // Build suggestions from theme registry (user themes first, then builtins)
2471
+ const userSuggestions: PromptSuggestion[] = [];
2472
+ const builtinSuggestions: PromptSuggestion[] = [];
2473
+ for (const [key, {name}] of state.themeRegistry) {
2474
+ const isCurrent = key === currentThemeKey || name === currentThemeKey;
2475
+ const isBuiltin = state.builtinKeys.has(key);
2476
+ const desc = isBuiltin
2477
+ ? (isCurrent ? editor.t("suggestion.builtin_theme_current") : editor.t("suggestion.builtin_theme"))
2478
+ : (isCurrent ? editor.t("suggestion.user_theme_current") : editor.t("suggestion.user_theme"));
2479
+ const suggestion = { text: name, description: desc, value: key };
2480
+ if (isBuiltin) {
2481
+ builtinSuggestions.push(suggestion);
2482
+ } else {
2483
+ userSuggestions.push(suggestion);
2449
2484
  }
2450
- } catch {
2451
- // No user themes directory
2452
- }
2453
-
2454
- // Add built-in themes
2455
- for (const name of state.builtinThemes) {
2456
- const isCurrent = name === currentThemeName;
2457
- suggestions.push({
2458
- text: name,
2459
- description: isCurrent ? editor.t("suggestion.builtin_theme_current") : editor.t("suggestion.builtin_theme"),
2460
- value: `builtin:${name}`,
2461
- });
2462
2485
  }
2486
+ suggestions.push(...userSuggestions, ...builtinSuggestions);
2463
2487
 
2464
2488
  // Sort suggestions to put current theme first
2465
2489
  suggestions.sort((a, b) => {
@@ -2643,32 +2667,20 @@ function theme_editor_open() : void {
2643
2667
 
2644
2668
  const suggestions: PromptSuggestion[] = [];
2645
2669
 
2646
- // Add user themes first (from themes directory)
2647
- const userThemesDir = editor.getThemesDir();
2648
- try {
2649
- const entries = editor.readDir(userThemesDir);
2650
- for (const e of entries) {
2651
- if (e.is_file && e.name.endsWith(".json")) {
2652
- const name = e.name.replace(".json", "");
2653
- suggestions.push({
2654
- text: name,
2655
- description: editor.t("suggestion.user_theme"),
2656
- value: `user:${name}`,
2657
- });
2658
- }
2670
+ // Build suggestions from theme registry (user themes first, then builtins)
2671
+ const userSuggestions: PromptSuggestion[] = [];
2672
+ const builtinSuggestions: PromptSuggestion[] = [];
2673
+ for (const [key, {name}] of state.themeRegistry) {
2674
+ const isBuiltin = state.builtinKeys.has(key);
2675
+ const desc = isBuiltin ? editor.t("suggestion.builtin_theme") : editor.t("suggestion.user_theme");
2676
+ const suggestion = { text: name, description: desc, value: key };
2677
+ if (isBuiltin) {
2678
+ builtinSuggestions.push(suggestion);
2679
+ } else {
2680
+ userSuggestions.push(suggestion);
2659
2681
  }
2660
- } catch {
2661
- // No user themes directory
2662
- }
2663
-
2664
- // Add built-in themes
2665
- for (const name of state.builtinThemes) {
2666
- suggestions.push({
2667
- text: name,
2668
- description: editor.t("suggestion.builtin_theme"),
2669
- value: `builtin:${name}`,
2670
- });
2671
2682
  }
2683
+ suggestions.push(...userSuggestions, ...builtinSuggestions);
2672
2684
 
2673
2685
  editor.setPromptSuggestions(suggestions);
2674
2686
  }
@@ -2757,11 +2769,14 @@ function theme_editor_save_as() : void {
2757
2769
  state.savedCursorPath = getCurrentFieldPath();
2758
2770
  }
2759
2771
 
2772
+ state.saveAsPreFilled = false;
2760
2773
  editor.startPrompt(editor.t("prompt.save_as"), "theme-save-as");
2761
2774
 
2762
2775
  editor.setPromptSuggestions([{
2763
2776
  text: state.themeName,
2764
- description: editor.t("suggestion.current"),
2777
+ description: state.isBuiltin
2778
+ ? editor.t("suggestion.current_builtin")
2779
+ : editor.t("suggestion.current"),
2765
2780
  value: state.themeName,
2766
2781
  }]);
2767
2782
  }
@@ -2772,8 +2787,7 @@ registerHandler("theme_editor_save_as", theme_editor_save_as);
2772
2787
  */
2773
2788
  async function theme_editor_reload() : Promise<void> {
2774
2789
  if (state.themePath) {
2775
- const themeName = state.themeName;
2776
- const themeData = loadThemeFile(themeName);
2790
+ const themeData = loadThemeFile(state.themeKey);
2777
2791
  if (themeData) {
2778
2792
  state.themeData = deepClone(themeData);
2779
2793
  state.originalThemeData = deepClone(themeData);
@@ -2840,7 +2854,8 @@ async function onThemeDeletePromptConfirmed(args: {
2840
2854
  // Reset to default theme
2841
2855
  state.themeData = createDefaultTheme();
2842
2856
  state.originalThemeData = deepClone(state.themeData);
2843
- state.themeName = "custom";
2857
+ state.themeName = "";
2858
+ state.themeKey = "";
2844
2859
  state.themePath = null;
2845
2860
  state.hasChanges = false;
2846
2861
  updateDisplay();