@fresh-editor/fresh-editor 0.1.70 → 0.1.74

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.
@@ -89,122 +89,108 @@ interface ThemeField {
89
89
  }
90
90
 
91
91
  // =============================================================================
92
- // Theme Field Metadata
92
+ // Theme Schema (loaded dynamically from Rust)
93
93
  // =============================================================================
94
94
 
95
- const THEME_SECTIONS: ThemeSection[] = [
96
- {
97
- name: "editor",
98
- displayName: "Editor",
99
- description: "Main editor area colors",
100
- fields: [
101
- { key: "bg", displayName: "Background", description: "Editor background color", section: "editor" },
102
- { key: "fg", displayName: "Foreground", description: "Default text color", section: "editor" },
103
- { key: "cursor", displayName: "Cursor", description: "Cursor color", section: "editor" },
104
- { key: "inactive_cursor", displayName: "Inactive Cursor", description: "Cursor color in unfocused splits", section: "editor" },
105
- { key: "selection_bg", displayName: "Selection Background", description: "Selected text background", section: "editor" },
106
- { key: "current_line_bg", displayName: "Current Line Background", description: "Background of the line containing cursor", section: "editor" },
107
- { key: "line_number_fg", displayName: "Line Number Foreground", description: "Line number text color", section: "editor" },
108
- { key: "line_number_bg", displayName: "Line Number Background", description: "Line number gutter background", section: "editor" },
109
- ],
110
- },
111
- {
112
- name: "ui",
113
- displayName: "UI Elements",
114
- description: "User interface colors (tabs, menus, status bar, etc.)",
115
- fields: [
116
- { key: "tab_active_fg", displayName: "Active Tab Foreground", description: "Active tab text color", section: "ui" },
117
- { key: "tab_active_bg", displayName: "Active Tab Background", description: "Active tab background color", section: "ui" },
118
- { key: "tab_inactive_fg", displayName: "Inactive Tab Foreground", description: "Inactive tab text color", section: "ui" },
119
- { key: "tab_inactive_bg", displayName: "Inactive Tab Background", description: "Inactive tab background color", section: "ui" },
120
- { key: "tab_separator_bg", displayName: "Tab Separator", description: "Tab bar separator color", section: "ui" },
121
- { key: "tab_close_hover_fg", displayName: "Tab Close Hover", description: "Tab close button hover color", section: "ui" },
122
- { key: "tab_hover_bg", displayName: "Tab Hover Background", description: "Tab hover background color", section: "ui" },
123
- { key: "menu_bg", displayName: "Menu Background", description: "Menu bar background", section: "ui" },
124
- { key: "menu_fg", displayName: "Menu Foreground", description: "Menu bar text color", section: "ui" },
125
- { key: "menu_active_bg", displayName: "Menu Active Background", description: "Active menu item background", section: "ui" },
126
- { key: "menu_active_fg", displayName: "Menu Active Foreground", description: "Active menu item text color", section: "ui" },
127
- { key: "menu_dropdown_bg", displayName: "Menu Dropdown Background", description: "Dropdown menu background", section: "ui" },
128
- { key: "menu_dropdown_fg", displayName: "Menu Dropdown Foreground", description: "Dropdown menu text color", section: "ui" },
129
- { key: "menu_highlight_bg", displayName: "Menu Highlight Background", description: "Highlighted menu item background", section: "ui" },
130
- { key: "menu_highlight_fg", displayName: "Menu Highlight Foreground", description: "Highlighted menu item text color", section: "ui" },
131
- { key: "menu_border_fg", displayName: "Menu Border", description: "Menu border color", section: "ui" },
132
- { key: "menu_separator_fg", displayName: "Menu Separator", description: "Menu separator line color", section: "ui" },
133
- { key: "menu_hover_bg", displayName: "Menu Hover Background", description: "Menu item hover background", section: "ui" },
134
- { key: "menu_hover_fg", displayName: "Menu Hover Foreground", description: "Menu item hover text color", section: "ui" },
135
- { key: "menu_disabled_fg", displayName: "Menu Disabled Foreground", description: "Disabled menu item text color", section: "ui" },
136
- { key: "menu_disabled_bg", displayName: "Menu Disabled Background", description: "Disabled menu item background", section: "ui" },
137
- { key: "status_bar_fg", displayName: "Status Bar Foreground", description: "Status bar text color", section: "ui" },
138
- { key: "status_bar_bg", displayName: "Status Bar Background", description: "Status bar background color", section: "ui" },
139
- { key: "prompt_fg", displayName: "Prompt Foreground", description: "Command prompt text color", section: "ui" },
140
- { key: "prompt_bg", displayName: "Prompt Background", description: "Command prompt background", section: "ui" },
141
- { key: "prompt_selection_fg", displayName: "Prompt Selection Foreground", description: "Prompt selected text color", section: "ui" },
142
- { key: "prompt_selection_bg", displayName: "Prompt Selection Background", description: "Prompt selection background", section: "ui" },
143
- { key: "popup_border_fg", displayName: "Popup Border", description: "Popup window border color", section: "ui" },
144
- { key: "popup_bg", displayName: "Popup Background", description: "Popup window background", section: "ui" },
145
- { key: "popup_selection_bg", displayName: "Popup Selection Background", description: "Popup selected item background", section: "ui" },
146
- { key: "popup_text_fg", displayName: "Popup Text Foreground", description: "Popup window text color", section: "ui" },
147
- { key: "suggestion_bg", displayName: "Suggestion Background", description: "Autocomplete suggestion background", section: "ui" },
148
- { key: "suggestion_selected_bg", displayName: "Suggestion Selected Background", description: "Selected suggestion background", section: "ui" },
149
- { key: "help_bg", displayName: "Help Background", description: "Help panel background", section: "ui" },
150
- { key: "help_fg", displayName: "Help Foreground", description: "Help panel text color", section: "ui" },
151
- { key: "help_key_fg", displayName: "Help Key Foreground", description: "Help keybinding text color", section: "ui" },
152
- { key: "help_separator_fg", displayName: "Help Separator", description: "Help panel separator color", section: "ui" },
153
- { key: "help_indicator_fg", displayName: "Help Indicator Foreground", description: "Help indicator text color", section: "ui" },
154
- { key: "help_indicator_bg", displayName: "Help Indicator Background", description: "Help indicator background", section: "ui" },
155
- { key: "inline_code_bg", displayName: "Inline Code Background", description: "Inline code block background", section: "ui" },
156
- { key: "split_separator_fg", displayName: "Split Separator", description: "Split pane separator color", section: "ui" },
157
- { key: "split_separator_hover_fg", displayName: "Split Separator Hover", description: "Split separator hover color", section: "ui" },
158
- { key: "scrollbar_track_fg", displayName: "Scrollbar Track", description: "Scrollbar track color", section: "ui" },
159
- { key: "scrollbar_thumb_fg", displayName: "Scrollbar Thumb", description: "Scrollbar thumb color", section: "ui" },
160
- { key: "scrollbar_track_hover_fg", displayName: "Scrollbar Track Hover", description: "Scrollbar track hover color", section: "ui" },
161
- { key: "scrollbar_thumb_hover_fg", displayName: "Scrollbar Thumb Hover", description: "Scrollbar thumb hover color", section: "ui" },
162
- { key: "compose_margin_bg", displayName: "Compose Margin Background", description: "Compose mode margin background", section: "ui" },
163
- { key: "semantic_highlight_bg", displayName: "Semantic Highlight Background", description: "Word under cursor highlight", section: "ui" },
164
- { key: "terminal_bg", displayName: "Terminal Background", description: "Embedded terminal background (use Default for transparency)", section: "ui" },
165
- { key: "terminal_fg", displayName: "Terminal Foreground", description: "Embedded terminal default text color", section: "ui" },
166
- ],
167
- },
168
- {
169
- name: "search",
170
- displayName: "Search",
171
- description: "Search result highlighting colors",
172
- fields: [
173
- { key: "match_bg", displayName: "Match Background", description: "Search match background color", section: "search" },
174
- { key: "match_fg", displayName: "Match Foreground", description: "Search match text color", section: "search" },
175
- ],
176
- },
177
- {
178
- name: "diagnostic",
179
- displayName: "Diagnostics",
180
- description: "LSP diagnostic colors (errors, warnings, etc.)",
181
- fields: [
182
- { key: "error_fg", displayName: "Error Foreground", description: "Error message text color", section: "diagnostic" },
183
- { key: "error_bg", displayName: "Error Background", description: "Error highlight background", section: "diagnostic" },
184
- { key: "warning_fg", displayName: "Warning Foreground", description: "Warning message text color", section: "diagnostic" },
185
- { key: "warning_bg", displayName: "Warning Background", description: "Warning highlight background", section: "diagnostic" },
186
- { key: "info_fg", displayName: "Info Foreground", description: "Info message text color", section: "diagnostic" },
187
- { key: "info_bg", displayName: "Info Background", description: "Info highlight background", section: "diagnostic" },
188
- { key: "hint_fg", displayName: "Hint Foreground", description: "Hint message text color", section: "diagnostic" },
189
- { key: "hint_bg", displayName: "Hint Background", description: "Hint highlight background", section: "diagnostic" },
190
- ],
191
- },
192
- {
193
- name: "syntax",
194
- displayName: "Syntax Highlighting",
195
- description: "Code syntax highlighting colors",
196
- fields: [
197
- { key: "keyword", displayName: "Keyword", description: "Language keywords (if, for, fn, etc.)", section: "syntax" },
198
- { key: "string", displayName: "String", description: "String literals", section: "syntax" },
199
- { key: "comment", displayName: "Comment", description: "Code comments", section: "syntax" },
200
- { key: "function", displayName: "Function", description: "Function names", section: "syntax" },
201
- { key: "type", displayName: "Type", description: "Type names", section: "syntax" },
202
- { key: "variable", displayName: "Variable", description: "Variable names", section: "syntax" },
203
- { key: "constant", displayName: "Constant", description: "Constants and literals", section: "syntax" },
204
- { key: "operator", displayName: "Operator", description: "Operators (+, -, =, etc.)", section: "syntax" },
205
- ],
206
- },
207
- ];
95
+ /**
96
+ * Cached theme sections loaded from the API.
97
+ * This is populated on first use and reflects the actual theme structure from Rust.
98
+ */
99
+ let cachedThemeSections: ThemeSection[] | null = null;
100
+
101
+ /**
102
+ * Load theme sections from the Rust API.
103
+ * Parses the raw JSON Schema and resolves $ref references.
104
+ * Uses i18n keys for localized display names.
105
+ */
106
+ function loadThemeSections(): ThemeSection[] {
107
+ if (cachedThemeSections !== null) {
108
+ return cachedThemeSections;
109
+ }
110
+
111
+ const schema = editor.getThemeSchema();
112
+ const defs = schema.$defs || {};
113
+
114
+ // Helper to resolve $ref and get the referenced schema
115
+ const resolveRef = (refStr: string): Record<string, unknown> | null => {
116
+ // $ref format: "#/$defs/TypeName"
117
+ const prefix = "#/$defs/";
118
+ if (refStr.startsWith(prefix)) {
119
+ const typeName = refStr.slice(prefix.length);
120
+ return defs[typeName] as Record<string, unknown> || null;
121
+ }
122
+ return null;
123
+ };
124
+
125
+ const sections: ThemeSection[] = [];
126
+ const properties = schema.properties || {};
127
+
128
+ // Section ordering
129
+ const sectionOrder = ["editor", "ui", "search", "diagnostic", "syntax"];
130
+
131
+ for (const [sectionName, sectionSchema] of Object.entries(properties)) {
132
+ // Skip "name" field - it's not a color section
133
+ if (sectionName === "name") continue;
134
+
135
+ const sectionObj = sectionSchema as Record<string, unknown>;
136
+ const sectionDesc = (sectionObj.description as string) || "";
137
+
138
+ // Resolve $ref to get the actual type definition
139
+ const refStr = sectionObj.$ref as string | undefined;
140
+ const resolvedSchema = refStr ? resolveRef(refStr) : sectionObj;
141
+ if (!resolvedSchema) continue;
142
+
143
+ const sectionProps = resolvedSchema.properties as Record<string, unknown> || {};
144
+ const fields: ThemeFieldDef[] = [];
145
+
146
+ for (const [fieldName, fieldSchema] of Object.entries(sectionProps)) {
147
+ const fieldObj = fieldSchema as Record<string, unknown>;
148
+ const fieldDesc = (fieldObj.description as string) || "";
149
+
150
+ // Generate i18n keys from field names
151
+ const i18nName = `field.${fieldName}`;
152
+ const i18nDesc = `field.${fieldName}_desc`;
153
+
154
+ fields.push({
155
+ key: fieldName,
156
+ displayName: editor.t(i18nName) || fieldDesc || fieldName,
157
+ description: editor.t(i18nDesc) || fieldDesc,
158
+ section: sectionName,
159
+ });
160
+ }
161
+
162
+ // Sort fields alphabetically (use simple comparison to avoid ICU issues in Deno)
163
+ fields.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
164
+
165
+ // Generate i18n keys for section
166
+ const sectionI18nName = `section.${sectionName}`;
167
+ const sectionI18nDesc = `section.${sectionName}_desc`;
168
+
169
+ sections.push({
170
+ name: sectionName,
171
+ displayName: editor.t(sectionI18nName) || sectionDesc || sectionName,
172
+ description: editor.t(sectionI18nDesc) || sectionDesc,
173
+ fields,
174
+ });
175
+ }
176
+
177
+ // Sort sections in logical order
178
+ sections.sort((a, b) => {
179
+ const aIdx = sectionOrder.indexOf(a.name);
180
+ const bIdx = sectionOrder.indexOf(b.name);
181
+ return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
182
+ });
183
+
184
+ cachedThemeSections = sections;
185
+ return cachedThemeSections;
186
+ }
187
+
188
+ /**
189
+ * Get theme sections (loads from API if not cached)
190
+ */
191
+ function getThemeSections(): ThemeSection[] {
192
+ return loadThemeSections();
193
+ }
208
194
 
209
195
  // =============================================================================
210
196
  // State Management
@@ -234,6 +220,12 @@ interface ThemeEditorState {
234
220
  hasChanges: boolean;
235
221
  /** Available built-in themes */
236
222
  builtinThemes: string[];
223
+ /** Pending save name for overwrite confirmation */
224
+ pendingSaveName: string | null;
225
+ /** Whether current theme is a built-in (requires Save As) */
226
+ isBuiltin: boolean;
227
+ /** Saved cursor field path (for restoring after prompts) */
228
+ savedCursorPath: string | null;
237
229
  }
238
230
 
239
231
  const state: ThemeEditorState = {
@@ -251,6 +243,9 @@ const state: ThemeEditorState = {
251
243
  selectedIndex: 0,
252
244
  hasChanges: false,
253
245
  builtinThemes: [],
246
+ pendingSaveName: null,
247
+ isBuiltin: false,
248
+ savedCursorPath: null,
254
249
  };
255
250
 
256
251
  // =============================================================================
@@ -266,10 +261,26 @@ const colors = {
266
261
  modified: [255, 255, 100] as RGB, // Yellow
267
262
  footer: [100, 100, 100] as RGB, // Gray
268
263
  colorBlock: [200, 200, 200] as RGB, // Light gray for color swatch outline
264
+ selectionBg: [50, 50, 80] as RGB, // Dark blue-gray for selected field
269
265
  };
270
266
 
271
- // Color block character for swatches
272
- const COLOR_BLOCK = "██";
267
+ // =============================================================================
268
+ // Keyboard Shortcuts (defined once, used in mode and i18n)
269
+ // =============================================================================
270
+
271
+ /**
272
+ * Keyboard shortcuts for the theme editor.
273
+ * These are defined once and used both in the mode definition and in the UI hints.
274
+ */
275
+ const SHORTCUTS = {
276
+ open: "C-o",
277
+ save: "C-s",
278
+ save_as: "C-S-s",
279
+ delete: "C-d",
280
+ reload: "C-r",
281
+ close: "C-q",
282
+ help: "F1",
283
+ };
273
284
 
274
285
  // =============================================================================
275
286
  // Mode Definition
@@ -279,18 +290,24 @@ editor.defineMode(
279
290
  "theme-editor",
280
291
  "normal",
281
292
  [
293
+ // Navigation (standard keys that don't conflict with typing)
282
294
  ["Return", "theme_editor_edit_color"],
283
295
  ["Space", "theme_editor_edit_color"],
284
- ["Tab", "theme_editor_toggle_section"],
285
- ["c", "theme_editor_copy_from_builtin"],
286
- ["n", "theme_editor_set_name"],
287
- ["s", "theme_editor_save"],
288
- ["S", "theme_editor_save_as"],
289
- ["d", "theme_editor_set_as_default"],
290
- ["q", "theme_editor_close"],
296
+ ["Tab", "theme_editor_nav_next_section"],
297
+ ["S-Tab", "theme_editor_nav_prev_section"],
298
+ ["Up", "theme_editor_nav_up"],
299
+ ["Down", "theme_editor_nav_down"],
291
300
  ["Escape", "theme_editor_close"],
292
- ["r", "theme_editor_reload"],
293
- ["?", "theme_editor_show_help"],
301
+ [SHORTCUTS.help, "theme_editor_show_help"],
302
+
303
+ // Ctrl+ shortcuts (match common editor conventions)
304
+ [SHORTCUTS.open, "theme_editor_open"],
305
+ [SHORTCUTS.save, "theme_editor_save"],
306
+ [SHORTCUTS.save_as, "theme_editor_save_as"],
307
+ [SHORTCUTS.delete, "theme_editor_delete"],
308
+ [SHORTCUTS.reload, "theme_editor_reload"],
309
+ [SHORTCUTS.close, "theme_editor_close"],
310
+ ["C-h", "theme_editor_show_help"], // Alternative help key
294
311
  ],
295
312
  true // read-only
296
313
  );
@@ -450,7 +467,7 @@ async function loadBuiltinThemes(): Promise<string[]> {
450
467
  }
451
468
 
452
469
  /**
453
- * Load a theme file
470
+ * Load a theme file from built-in themes directory
454
471
  */
455
472
  async function loadThemeFile(name: string): Promise<Record<string, unknown> | null> {
456
473
  const themesDir = findThemesDir();
@@ -465,11 +482,48 @@ async function loadThemeFile(name: string): Promise<Record<string, unknown> | nu
465
482
  }
466
483
  }
467
484
 
485
+ /**
486
+ * Load a user theme file
487
+ */
488
+ async function loadUserThemeFile(name: string): Promise<{ data: Record<string, unknown>; path: string } | null> {
489
+ const userThemesDir = getUserThemesDir();
490
+ const themePath = editor.pathJoin(userThemesDir, `${name}.json`);
491
+
492
+ try {
493
+ const content = await editor.readFile(themePath);
494
+ return { data: JSON.parse(content), path: themePath };
495
+ } catch {
496
+ editor.debug(`Failed to load user theme: ${name}`);
497
+ return null;
498
+ }
499
+ }
500
+
501
+ /**
502
+ * List available user themes
503
+ */
504
+ function listUserThemes(): string[] {
505
+ const userThemesDir = getUserThemesDir();
506
+ try {
507
+ const entries = editor.readDir(userThemesDir);
508
+ return entries
509
+ .filter(e => e.is_file && e.name.endsWith(".json"))
510
+ .map(e => e.name.replace(".json", ""));
511
+ } catch {
512
+ return [];
513
+ }
514
+ }
515
+
468
516
  /**
469
517
  * Get user themes directory
470
- * Uses XDG_CONFIG_HOME if set, otherwise falls back to $HOME/.config
518
+ * Uses the API to get the correct path
471
519
  */
472
520
  function getUserThemesDir(): string {
521
+ // Use the API if available (new method)
522
+ if (typeof editor.getThemesDir === "function") {
523
+ return editor.getThemesDir();
524
+ }
525
+
526
+ // Fallback for older versions (deprecated)
473
527
  // Check XDG_CONFIG_HOME first (standard on Linux)
474
528
  const xdgConfig = editor.getEnv("XDG_CONFIG_HOME");
475
529
  if (xdgConfig) {
@@ -494,16 +548,17 @@ function getUserThemesDir(): string {
494
548
  */
495
549
  function buildVisibleFields(): ThemeField[] {
496
550
  const fields: ThemeField[] = [];
551
+ const themeSections = getThemeSections();
497
552
 
498
- for (const section of THEME_SECTIONS) {
553
+ for (const section of themeSections) {
499
554
  const expanded = state.expandedSections.has(section.name);
500
555
 
501
- // Section header
556
+ // Section header - displayName and description are already translated in getThemeSections()
502
557
  fields.push({
503
558
  def: {
504
559
  key: section.name,
505
- displayName: editor.t(`section.${section.name}`),
506
- description: editor.t(`section.${section.name}_desc`),
560
+ displayName: section.displayName,
561
+ description: section.description,
507
562
  section: section.name,
508
563
  },
509
564
  value: [0, 0, 0], // Placeholder
@@ -519,12 +574,9 @@ function buildVisibleFields(): ThemeField[] {
519
574
  const path = `${section.name}.${fieldDef.key}`;
520
575
  const value = getNestedValue(state.themeData, path) as ColorValue || [128, 128, 128];
521
576
 
577
+ // fieldDef displayName and description are already translated in getThemeSections()
522
578
  fields.push({
523
- def: {
524
- ...fieldDef,
525
- displayName: editor.t(`field.${fieldDef.key}`),
526
- description: editor.t(`field.${fieldDef.key}_desc`),
527
- },
579
+ def: fieldDef,
528
580
  value,
529
581
  path,
530
582
  depth: 1,
@@ -566,6 +618,21 @@ function buildDisplayEntries(): TextPropertyEntry[] {
566
618
  });
567
619
  }
568
620
 
621
+ // Key hints at the top (moved from footer)
622
+ entries.push({
623
+ text: editor.t("panel.nav_hint") + "\n",
624
+ properties: { type: "footer" },
625
+ });
626
+ entries.push({
627
+ text: editor.t("panel.action_hint", SHORTCUTS) + "\n",
628
+ properties: { type: "footer" },
629
+ });
630
+
631
+ entries.push({
632
+ text: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
633
+ properties: { type: "separator" },
634
+ });
635
+
569
636
  entries.push({
570
637
  text: "\n",
571
638
  properties: { type: "blank" },
@@ -603,11 +670,11 @@ function buildDisplayEntries(): TextPropertyEntry[] {
603
670
  properties: { type: "description", path: field.path },
604
671
  });
605
672
 
606
- // Color field with swatch
673
+ // Color field with swatch characters (X for fg preview, space for bg preview)
607
674
  const colorStr = formatColorValue(field.value);
608
675
 
609
676
  entries.push({
610
- text: `${indent} ${field.def.displayName}: ${colorStr}\n`,
677
+ text: `${indent} ${field.def.displayName}: X ${colorStr}\n`,
611
678
  properties: {
612
679
  type: "field",
613
680
  path: field.path,
@@ -623,25 +690,12 @@ function buildDisplayEntries(): TextPropertyEntry[] {
623
690
  });
624
691
  }
625
692
 
626
- // Footer
627
- entries.push({
628
- text: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
629
- properties: { type: "separator" },
630
- });
631
- entries.push({
632
- text: editor.t("panel.nav_hint") + "\n",
633
- properties: { type: "footer" },
634
- });
635
- entries.push({
636
- text: editor.t("panel.action_hint") + "\n",
637
- properties: { type: "footer" },
638
- });
639
-
640
693
  return entries;
641
694
  }
642
695
 
643
696
  /**
644
- * Helper to add a colored overlay
697
+ * Helper to add a colored overlay (foreground color)
698
+ * addOverlay signature: (bufferId, namespace, start, end, r, g, b, underline, bold, italic, bg_r, bg_g, bg_b, extend_to_line_end)
645
699
  */
646
700
  function addColorOverlay(
647
701
  bufferId: number,
@@ -650,7 +704,20 @@ function addColorOverlay(
650
704
  color: RGB,
651
705
  bold: boolean = false
652
706
  ): void {
653
- editor.addOverlay(bufferId, "theme", start, end, color[0], color[1], color[2], false, bold, false);
707
+ editor.addOverlay(bufferId, "theme", start, end, color[0], color[1], color[2], false, bold, false, -1, -1, -1, false);
708
+ }
709
+
710
+ /**
711
+ * Helper to add a background highlight overlay
712
+ * addOverlay signature: (bufferId, namespace, start, end, r, g, b, underline, bold, italic, bg_r, bg_g, bg_b, extend_to_line_end)
713
+ */
714
+ function addBackgroundHighlight(
715
+ bufferId: number,
716
+ start: number,
717
+ end: number,
718
+ bgColor: RGB
719
+ ): void {
720
+ editor.addOverlay(bufferId, "theme-selection", start, end, -1, -1, -1, false, false, false, bgColor[0], bgColor[1], bgColor[2], true);
654
721
  }
655
722
 
656
723
  /**
@@ -669,70 +736,6 @@ function isSpecialColor(value: ColorValue): boolean {
669
736
  return typeof value === "string" && SPECIAL_COLORS.includes(value);
670
737
  }
671
738
 
672
- /**
673
- * Add color swatches using virtual text
674
- */
675
- function addColorSwatches(): void {
676
- if (state.bufferId === null) return;
677
-
678
- // Clear existing swatches
679
- editor.removeVirtualTextsByPrefix(state.bufferId, "theme-swatch-");
680
-
681
- const entries = buildDisplayEntries();
682
- let byteOffset = 0;
683
-
684
- for (const entry of entries) {
685
- const props = entry.properties as Record<string, unknown>;
686
-
687
- if (props.type === "field" && props.colorValue) {
688
- const colorValue = props.colorValue as ColorValue;
689
- const path = props.path as string;
690
-
691
- // Find position after the field name colon
692
- const colonIdx = entry.text.indexOf(":");
693
- if (colonIdx >= 0) {
694
- const swatchPos = byteOffset + getUtf8ByteLength(entry.text.substring(0, colonIdx + 2));
695
- const swatchId = `theme-swatch-${path}`;
696
-
697
- if (isSpecialColor(colorValue)) {
698
- // For Default/Reset, show a placeholder indicator
699
- editor.addVirtualText(
700
- state.bufferId,
701
- swatchId,
702
- swatchPos,
703
- "∅ ", // Empty set symbol to indicate "use default"
704
- 150, // Gray color for the indicator
705
- 150,
706
- 150,
707
- true,
708
- false
709
- );
710
- } else {
711
- const rgb = parseColorToRgb(colorValue);
712
- if (rgb) {
713
- const useBg = isBackgroundColorField(path);
714
-
715
- // Add swatch with a trailing space included in the text
716
- editor.addVirtualText(
717
- state.bufferId,
718
- swatchId,
719
- swatchPos,
720
- useBg ? " " : COLOR_BLOCK + " ", // Include trailing space in swatch text
721
- rgb[0],
722
- rgb[1],
723
- rgb[2],
724
- true,
725
- useBg // use as background color
726
- );
727
- }
728
- }
729
- }
730
- }
731
-
732
- byteOffset += getUtf8ByteLength(entry.text);
733
- }
734
- }
735
-
736
739
  /**
737
740
  * Apply syntax highlighting
738
741
  */
@@ -741,15 +744,26 @@ function applyHighlighting(): void {
741
744
 
742
745
  const bufferId = state.bufferId;
743
746
  editor.clearNamespace(bufferId, "theme");
747
+ editor.clearNamespace(bufferId, "theme-selection");
744
748
 
745
749
  const entries = buildDisplayEntries();
746
750
  let byteOffset = 0;
747
751
 
752
+ // Get current field at cursor to highlight it
753
+ const currentField = getFieldAtCursor();
754
+ const currentFieldPath = currentField?.path;
755
+
748
756
  for (const entry of entries) {
749
757
  const text = entry.text;
750
758
  const textLen = getUtf8ByteLength(text);
751
759
  const props = entry.properties as Record<string, unknown>;
752
760
  const entryType = props.type as string;
761
+ const entryPath = props.path as string | undefined;
762
+
763
+ // Add selection highlight for current field/section
764
+ if (currentFieldPath && entryPath === currentFieldPath && (entryType === "field" || entryType === "section")) {
765
+ addBackgroundHighlight(bufferId, byteOffset, byteOffset + textLen, colors.selectionBg);
766
+ }
753
767
 
754
768
  if (entryType === "title") {
755
769
  addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.sectionHeader, true);
@@ -766,8 +780,26 @@ function applyHighlighting(): void {
766
780
  const nameEnd = byteOffset + getUtf8ByteLength(text.substring(0, colonPos));
767
781
  addColorOverlay(bufferId, byteOffset, nameEnd, colors.fieldName);
768
782
 
769
- // Value - custom color (green)
770
- const valueStart = nameEnd + getUtf8ByteLength(": ");
783
+ // Color the swatch characters with the field's actual color
784
+ // Text format: "FieldName: X #RRGGBB" (X=fg, space=bg)
785
+ const colorValue = props.colorValue as ColorValue;
786
+ const rgb = parseColorToRgb(colorValue);
787
+ if (rgb) {
788
+ // "X" is at colon + 2 (": " = 2 bytes), and is 1 byte
789
+ const swatchFgStart = nameEnd + getUtf8ByteLength(": ");
790
+ const swatchFgEnd = swatchFgStart + 1; // "X" is 1 byte
791
+ addColorOverlay(bufferId, swatchFgStart, swatchFgEnd, rgb);
792
+
793
+ // First space after "X" is the bg swatch, 1 byte
794
+ const swatchBgStart = swatchFgEnd;
795
+ const swatchBgEnd = swatchBgStart + 1;
796
+ // Use background color for the space
797
+ editor.addOverlay(bufferId, "theme", swatchBgStart, swatchBgEnd, -1, -1, -1, false, false, false, rgb[0], rgb[1], rgb[2], false);
798
+ }
799
+
800
+ // Value (hex code) - custom color (green)
801
+ // Format: ": X #RRGGBB" - value starts after "X " (X + 2 spaces)
802
+ const valueStart = nameEnd + getUtf8ByteLength(": X ");
771
803
  addColorOverlay(bufferId, valueStart, byteOffset + textLen, colors.customValue);
772
804
  }
773
805
  } else if (entryType === "separator" || entryType === "footer") {
@@ -776,20 +808,25 @@ function applyHighlighting(): void {
776
808
 
777
809
  byteOffset += textLen;
778
810
  }
779
-
780
- // Add color swatches
781
- addColorSwatches();
782
811
  }
783
812
 
784
813
  /**
785
- * Update display
814
+ * Update display (preserves cursor position)
786
815
  */
787
816
  function updateDisplay(): void {
788
817
  if (state.bufferId === null) return;
789
818
 
819
+ // Save current field path before updating
820
+ const currentPath = getCurrentFieldPath();
821
+
790
822
  const entries = buildDisplayEntries();
791
823
  editor.setVirtualBufferContent(state.bufferId, entries);
792
824
  applyHighlighting();
825
+
826
+ // Restore cursor to the same field if possible
827
+ if (currentPath) {
828
+ moveCursorToField(currentPath);
829
+ }
793
830
  }
794
831
 
795
832
  // =============================================================================
@@ -814,66 +851,87 @@ function getFieldAtCursor(): ThemeField | null {
814
851
  }
815
852
 
816
853
  /**
817
- * Start color editing prompt
854
+ * Get field by path
818
855
  */
819
- function editColorField(field: ThemeField): void {
820
- const currentValue = formatColorValue(field.value);
821
-
822
- // Use startPromptWithInitial to pre-fill with current value
823
- editor.startPromptWithInitial(editor.t("prompt.color_input", { field: field.def.displayName }), `theme-color-${field.path}`, currentValue);
856
+ function getFieldByPath(path: string): ThemeField | null {
857
+ return state.visibleFields.find(f => f.path === path) || null;
858
+ }
824
859
 
825
- // Build suggestions with named colors and current value
860
+ /**
861
+ * Build color suggestions for a field
862
+ */
863
+ function buildColorSuggestions(field: ThemeField): PromptSuggestion[] {
864
+ const currentValue = formatColorValue(field.value);
826
865
  const suggestions: PromptSuggestion[] = [
827
- {
828
- text: currentValue,
829
- description: editor.t("suggestion.current"),
830
- value: currentValue,
831
- },
866
+ { text: currentValue, description: editor.t("suggestion.current"), value: currentValue },
832
867
  ];
833
868
 
834
- // Add special colors first (Default/Reset for terminal transparency)
869
+ // Add special colors (Default/Reset for terminal transparency)
835
870
  for (const name of SPECIAL_COLORS) {
836
- suggestions.push({
837
- text: name,
838
- description: editor.t("suggestion.terminal_native"),
839
- value: name,
840
- });
871
+ suggestions.push({ text: name, description: editor.t("suggestion.terminal_native"), value: name });
841
872
  }
842
873
 
843
- // Add named colors as suggestions with hex format
874
+ // Add named colors with hex format
844
875
  for (const name of NAMED_COLOR_LIST) {
845
876
  const rgb = NAMED_COLORS[name];
846
877
  const hexValue = rgbToHex(rgb[0], rgb[1], rgb[2]);
847
- suggestions.push({
848
- text: name,
849
- description: hexValue,
850
- value: name,
851
- });
878
+ suggestions.push({ text: name, description: hexValue, value: name });
852
879
  }
853
880
 
854
- editor.setPromptSuggestions(suggestions);
881
+ return suggestions;
882
+ }
883
+
884
+ /**
885
+ * Start color editing prompt
886
+ */
887
+ function editColorField(field: ThemeField): void {
888
+ const currentValue = formatColorValue(field.value);
889
+ editor.startPromptWithInitial(
890
+ editor.t("prompt.color_input", { field: field.def.displayName }),
891
+ `theme-color-${field.path}`,
892
+ currentValue
893
+ );
894
+ editor.setPromptSuggestions(buildColorSuggestions(field));
895
+ }
896
+
897
+ interface ParseColorResult {
898
+ value?: ColorValue;
899
+ error?: string;
855
900
  }
856
901
 
857
902
  /**
858
- * Parse color input from user
903
+ * Parse color input from user with detailed error messages
859
904
  */
860
- function parseColorInput(input: string): ColorValue | null {
905
+ function parseColorInput(input: string): ParseColorResult {
861
906
  input = input.trim();
862
907
 
908
+ if (!input) {
909
+ return { error: "empty" };
910
+ }
911
+
863
912
  // Check for special colors (Default/Reset - use terminal's native color)
864
913
  if (SPECIAL_COLORS.includes(input)) {
865
- return input;
914
+ return { value: input };
866
915
  }
867
916
 
868
917
  // Check for named color
869
918
  if (input in NAMED_COLORS) {
870
- return input;
919
+ return { value: input };
871
920
  }
872
921
 
873
922
  // Try to parse as hex color #RRGGBB
874
- const hexResult = hexToRgb(input);
875
- if (hexResult) {
876
- return hexResult;
923
+ if (input.startsWith("#")) {
924
+ const hex = input.slice(1);
925
+ if (hex.length !== 6) {
926
+ return { error: "hex_length" };
927
+ }
928
+ if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
929
+ return { error: "hex_invalid" };
930
+ }
931
+ const hexResult = hexToRgb(input);
932
+ if (hexResult) {
933
+ return { value: hexResult };
934
+ }
877
935
  }
878
936
 
879
937
  // Try to parse as RGB array [r, g, b]
@@ -882,19 +940,46 @@ function parseColorInput(input: string): ColorValue | null {
882
940
  const r = parseInt(rgbMatch[1], 10);
883
941
  const g = parseInt(rgbMatch[2], 10);
884
942
  const b = parseInt(rgbMatch[3], 10);
885
-
886
- if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
887
- return [r, g, b];
943
+ if (r > 255 || g > 255 || b > 255) {
944
+ return { error: "rgb_range" };
888
945
  }
946
+ return { value: [r, g, b] };
889
947
  }
890
948
 
891
- return null;
949
+ // Unknown format
950
+ return { error: "unknown" };
892
951
  }
893
952
 
894
953
  // =============================================================================
895
954
  // Prompt Handlers
896
955
  // =============================================================================
897
956
 
957
+ /**
958
+ * Find best matching color name for partial input
959
+ */
960
+ function findMatchingColor(input: string): string | null {
961
+ const lower = input.toLowerCase();
962
+ // First try exact match
963
+ for (const name of Object.keys(NAMED_COLORS)) {
964
+ if (name.toLowerCase() === lower) return name;
965
+ }
966
+ for (const name of SPECIAL_COLORS) {
967
+ if (name.toLowerCase() === lower) return name;
968
+ }
969
+ // Then try prefix match
970
+ for (const name of Object.keys(NAMED_COLORS)) {
971
+ if (name.toLowerCase().startsWith(lower)) return name;
972
+ }
973
+ for (const name of SPECIAL_COLORS) {
974
+ if (name.toLowerCase().startsWith(lower)) return name;
975
+ }
976
+ // Then try contains match
977
+ for (const name of Object.keys(NAMED_COLORS)) {
978
+ if (name.toLowerCase().includes(lower)) return name;
979
+ }
980
+ return null;
981
+ }
982
+
898
983
  /**
899
984
  * Handle color prompt confirmation
900
985
  */
@@ -906,65 +991,112 @@ globalThis.onThemeColorPromptConfirmed = function(args: {
906
991
  if (!args.prompt_type.startsWith("theme-color-")) return true;
907
992
 
908
993
  const path = args.prompt_type.replace("theme-color-", "");
909
- const newValue = parseColorInput(args.input);
994
+ const field = getFieldByPath(path);
995
+ if (!field) return true;
910
996
 
911
- if (newValue !== null) {
912
- setNestedValue(state.themeData, path, newValue);
997
+ const result = parseColorInput(args.input);
998
+
999
+ if (result.value !== undefined) {
1000
+ // Valid color - apply it
1001
+ setNestedValue(state.themeData, path, result.value);
913
1002
  state.hasChanges = !deepEqual(state.themeData, state.originalThemeData);
914
- updateDisplay();
1003
+
1004
+ const entries = buildDisplayEntries();
1005
+ if (state.bufferId !== null) {
1006
+ editor.setVirtualBufferContent(state.bufferId, entries);
1007
+ applyHighlighting();
1008
+ }
1009
+ moveCursorToField(path);
915
1010
  editor.setStatus(editor.t("status.updated", { path }));
916
1011
  } else {
917
- editor.setStatus(editor.t("status.invalid_color"));
1012
+ // Invalid input - try to find a matching color name
1013
+ const matchedColor = findMatchingColor(args.input);
1014
+ if (matchedColor) {
1015
+ // Found a match - reopen prompt with the matched value
1016
+ editor.startPromptWithInitial(
1017
+ editor.t("prompt.color_input", { field: field.def.displayName }),
1018
+ `theme-color-${path}`,
1019
+ matchedColor
1020
+ );
1021
+ // Rebuild suggestions
1022
+ const suggestions: PromptSuggestion[] = buildColorSuggestions(field);
1023
+ editor.setPromptSuggestions(suggestions);
1024
+ editor.setStatus(editor.t("status.autocompleted", { value: matchedColor }));
1025
+ } else {
1026
+ // No match found - reopen prompt with original input
1027
+ editor.startPromptWithInitial(
1028
+ editor.t("prompt.color_input", { field: field.def.displayName }),
1029
+ `theme-color-${path}`,
1030
+ args.input
1031
+ );
1032
+ const suggestions: PromptSuggestion[] = buildColorSuggestions(field);
1033
+ editor.setPromptSuggestions(suggestions);
1034
+
1035
+ const errorKey = `error.color_${result.error}`;
1036
+ editor.setStatus(editor.t(errorKey, { input: args.input }));
1037
+ }
918
1038
  }
919
1039
 
920
1040
  return true;
921
1041
  };
922
1042
 
923
1043
  /**
924
- * Handle theme name prompt
1044
+ * Handle open theme prompt (both builtin and user themes)
925
1045
  */
926
- globalThis.onThemeNamePromptConfirmed = function(args: {
1046
+ globalThis.onThemeOpenPromptConfirmed = async function(args: {
927
1047
  prompt_type: string;
928
1048
  selected_index: number | null;
929
1049
  input: string;
930
- }): boolean {
931
- if (args.prompt_type !== "theme-name") return true;
932
-
933
- const name = args.input.trim();
934
- if (name) {
935
- state.themeName = name;
936
- state.themeData.name = name;
937
- state.hasChanges = true;
938
- updateDisplay();
939
- editor.setStatus(editor.t("status.name_set", { name }));
940
- }
1050
+ }): Promise<boolean> {
1051
+ if (args.prompt_type !== "theme-open") return true;
941
1052
 
942
- return true;
943
- };
1053
+ const value = args.input.trim();
944
1054
 
945
- /**
946
- * Handle copy from builtin prompt
947
- */
948
- globalThis.onThemeCopyPromptConfirmed = async function(args: {
949
- prompt_type: string;
950
- selected_index: number | null;
951
- input: string;
952
- }): Promise<boolean> {
953
- if (args.prompt_type !== "theme-copy-builtin") return true;
1055
+ // Parse the value to determine if it's user or builtin
1056
+ let isBuiltin = false;
1057
+ let themeName = value;
954
1058
 
955
- const themeName = args.input.trim();
956
- const themeData = await loadThemeFile(themeName);
1059
+ if (value.startsWith("user:")) {
1060
+ themeName = value.slice(5);
1061
+ isBuiltin = false;
1062
+ } else if (value.startsWith("builtin:")) {
1063
+ themeName = value.slice(8);
1064
+ isBuiltin = true;
1065
+ } else {
1066
+ // Fallback: check if it's a builtin theme
1067
+ isBuiltin = state.builtinThemes.includes(value);
1068
+ }
957
1069
 
958
- if (themeData) {
959
- state.themeData = deepClone(themeData);
960
- state.themeName = `${themeName}-custom`;
961
- state.themeData.name = state.themeName;
962
- state.themePath = null; // New theme, not saved yet
963
- state.hasChanges = true;
964
- updateDisplay();
965
- editor.setStatus(editor.t("status.copied", { theme: themeName }));
1070
+ if (isBuiltin) {
1071
+ // Load builtin theme
1072
+ const themeData = await loadThemeFile(themeName);
1073
+ if (themeData) {
1074
+ state.themeData = deepClone(themeData);
1075
+ state.originalThemeData = deepClone(themeData);
1076
+ state.themeName = themeName;
1077
+ state.themePath = null; // No user path for builtin
1078
+ state.isBuiltin = true;
1079
+ state.hasChanges = false;
1080
+ updateDisplay();
1081
+ editor.setStatus(editor.t("status.opened_builtin", { name: themeName }));
1082
+ } else {
1083
+ editor.setStatus(editor.t("status.load_failed", { name: themeName }));
1084
+ }
966
1085
  } else {
967
- editor.setStatus(editor.t("status.load_failed", { name: themeName }));
1086
+ // Load user theme
1087
+ const result = await loadUserThemeFile(themeName);
1088
+ if (result) {
1089
+ state.themeData = deepClone(result.data);
1090
+ state.originalThemeData = deepClone(result.data);
1091
+ state.themeName = themeName;
1092
+ state.themePath = result.path;
1093
+ state.isBuiltin = false;
1094
+ state.hasChanges = false;
1095
+ updateDisplay();
1096
+ editor.setStatus(editor.t("status.loaded", { name: themeName }));
1097
+ } else {
1098
+ editor.setStatus(editor.t("status.load_failed", { name: themeName }));
1099
+ }
968
1100
  }
969
1101
 
970
1102
  return true;
@@ -982,47 +1114,131 @@ globalThis.onThemeSaveAsPromptConfirmed = async function(args: {
982
1114
 
983
1115
  const name = args.input.trim();
984
1116
  if (name) {
1117
+ // Check if theme already exists
1118
+ const userThemesDir = getUserThemesDir();
1119
+ const targetPath = editor.pathJoin(userThemesDir, `${name}.json`);
1120
+
1121
+ if (editor.fileExists(targetPath)) {
1122
+ // Store pending save name for overwrite confirmation
1123
+ state.pendingSaveName = name;
1124
+ editor.startPrompt(editor.t("prompt.overwrite_confirm", { name }), "theme-overwrite-confirm");
1125
+ const suggestions: PromptSuggestion[] = [
1126
+ { text: editor.t("prompt.overwrite_yes"), description: "", value: "overwrite" },
1127
+ { text: editor.t("prompt.overwrite_no"), description: "", value: "cancel" },
1128
+ ];
1129
+ editor.setPromptSuggestions(suggestions);
1130
+ return true;
1131
+ }
1132
+
985
1133
  state.themeName = name;
986
1134
  state.themeData.name = name;
987
- await saveTheme(name);
1135
+ const restorePath = state.savedCursorPath;
1136
+ state.savedCursorPath = null;
1137
+ await saveTheme(name, restorePath);
1138
+ } else {
1139
+ state.savedCursorPath = null;
988
1140
  }
989
1141
 
990
1142
  return true;
991
1143
  };
992
1144
 
993
1145
  /**
994
- * Handle set as default prompt
1146
+ * Handle prompt cancellation
1147
+ */
1148
+ globalThis.onThemePromptCancelled = function(args: { prompt_type: string }): boolean {
1149
+ if (!args.prompt_type.startsWith("theme-")) return true;
1150
+
1151
+ // Clear saved cursor path on cancellation
1152
+ state.savedCursorPath = null;
1153
+ state.pendingSaveName = null;
1154
+
1155
+ editor.setStatus(editor.t("status.cancelled"));
1156
+ return true;
1157
+ };
1158
+
1159
+ /**
1160
+ * Handle initial theme selection prompt (when opening editor)
995
1161
  */
996
- globalThis.onThemeSetDefaultPromptConfirmed = async function(args: {
1162
+ globalThis.onThemeSelectInitialPromptConfirmed = async function(args: {
997
1163
  prompt_type: string;
998
1164
  selected_index: number | null;
999
1165
  input: string;
1000
1166
  }): Promise<boolean> {
1001
- if (args.prompt_type !== "theme-set-default") return true;
1167
+ if (args.prompt_type !== "theme-select-initial") return true;
1168
+
1169
+ const value = args.input.trim();
1170
+
1171
+ // Parse the value to determine if it's user or builtin
1172
+ let isBuiltin = false;
1173
+ let themeName = value;
1002
1174
 
1003
- const themeName = args.input.trim();
1004
- if (themeName) {
1005
- await setThemeAsDefault(themeName);
1175
+ if (value.startsWith("user:")) {
1176
+ themeName = value.slice(5);
1177
+ isBuiltin = false;
1178
+ } else if (value.startsWith("builtin:")) {
1179
+ themeName = value.slice(8);
1180
+ isBuiltin = true;
1181
+ } else {
1182
+ // Fallback: check if it's a builtin theme
1183
+ isBuiltin = state.builtinThemes.includes(value);
1006
1184
  }
1007
1185
 
1008
- return true;
1009
- };
1186
+ editor.setStatus(editor.t("status.loading"));
1187
+
1188
+ if (isBuiltin) {
1189
+ // Load builtin theme
1190
+ const themeData = await loadThemeFile(themeName);
1191
+ if (themeData) {
1192
+ state.themeData = deepClone(themeData);
1193
+ state.originalThemeData = deepClone(themeData);
1194
+ state.themeName = themeName;
1195
+ state.themePath = null; // No user path for builtin
1196
+ state.isBuiltin = true;
1197
+ state.hasChanges = false;
1198
+ } else {
1199
+ // Fallback to default theme if load failed
1200
+ state.themeData = createDefaultTheme();
1201
+ state.originalThemeData = deepClone(state.themeData);
1202
+ state.themeName = themeName;
1203
+ state.themePath = null;
1204
+ state.isBuiltin = true;
1205
+ state.hasChanges = false;
1206
+ }
1207
+ } else {
1208
+ // Load user theme
1209
+ const result = await loadUserThemeFile(themeName);
1210
+ if (result) {
1211
+ state.themeData = deepClone(result.data);
1212
+ state.originalThemeData = deepClone(result.data);
1213
+ state.themeName = themeName;
1214
+ state.themePath = result.path;
1215
+ state.isBuiltin = false;
1216
+ state.hasChanges = false;
1217
+ } else {
1218
+ // Fallback to default theme if load failed
1219
+ state.themeData = createDefaultTheme();
1220
+ state.originalThemeData = deepClone(state.themeData);
1221
+ state.themeName = themeName;
1222
+ state.themePath = null;
1223
+ state.isBuiltin = false;
1224
+ state.hasChanges = false;
1225
+ }
1226
+ }
1227
+
1228
+ // Now open the editor with loaded theme
1229
+ await doOpenThemeEditor();
1010
1230
 
1011
- /**
1012
- * Handle prompt cancellation
1013
- */
1014
- globalThis.onThemePromptCancelled = function(args: { prompt_type: string }): boolean {
1015
- if (!args.prompt_type.startsWith("theme-")) return true;
1016
- editor.setStatus(editor.t("status.cancelled"));
1017
1231
  return true;
1018
1232
  };
1019
1233
 
1020
1234
  // Register prompt handlers
1235
+ editor.on("prompt_confirmed", "onThemeSelectInitialPromptConfirmed");
1021
1236
  editor.on("prompt_confirmed", "onThemeColorPromptConfirmed");
1022
- editor.on("prompt_confirmed", "onThemeNamePromptConfirmed");
1023
- editor.on("prompt_confirmed", "onThemeCopyPromptConfirmed");
1237
+ editor.on("prompt_confirmed", "onThemeOpenPromptConfirmed");
1024
1238
  editor.on("prompt_confirmed", "onThemeSaveAsPromptConfirmed");
1025
- editor.on("prompt_confirmed", "onThemeSetDefaultPromptConfirmed");
1239
+ editor.on("prompt_confirmed", "onThemeDiscardPromptConfirmed");
1240
+ editor.on("prompt_confirmed", "onThemeOverwritePromptConfirmed");
1241
+ editor.on("prompt_confirmed", "onThemeDeletePromptConfirmed");
1026
1242
  editor.on("prompt_cancelled", "onThemePromptCancelled");
1027
1243
 
1028
1244
  // =============================================================================
@@ -1031,8 +1247,10 @@ editor.on("prompt_cancelled", "onThemePromptCancelled");
1031
1247
 
1032
1248
  /**
1033
1249
  * Save theme to file
1250
+ * @param name - Theme name to save as
1251
+ * @param restorePath - Optional field path to restore cursor to after save
1034
1252
  */
1035
- async function saveTheme(name?: string): Promise<boolean> {
1253
+ async function saveTheme(name?: string, restorePath?: string | null): Promise<boolean> {
1036
1254
  const themeName = name || state.themeName;
1037
1255
  const userThemesDir = getUserThemesDir();
1038
1256
 
@@ -1056,11 +1274,25 @@ async function saveTheme(name?: string): Promise<boolean> {
1056
1274
 
1057
1275
  state.themePath = themePath;
1058
1276
  state.themeName = themeName;
1277
+ state.isBuiltin = false; // After saving, it's now a user theme
1059
1278
  state.originalThemeData = deepClone(state.themeData);
1060
1279
  state.hasChanges = false;
1061
- updateDisplay();
1062
1280
 
1063
- editor.setStatus(editor.t("status.saved", { path: themePath }));
1281
+ // Update display
1282
+ const entries = buildDisplayEntries();
1283
+ if (state.bufferId !== null) {
1284
+ editor.setVirtualBufferContent(state.bufferId, entries);
1285
+ applyHighlighting();
1286
+ }
1287
+
1288
+ // Restore cursor position if provided
1289
+ if (restorePath) {
1290
+ moveCursorToField(restorePath);
1291
+ }
1292
+
1293
+ // Automatically apply the saved theme
1294
+ editor.applyTheme(themeName);
1295
+ editor.setStatus(editor.t("status.saved_and_applied", { name: themeName }));
1064
1296
  return true;
1065
1297
  } catch (e) {
1066
1298
  editor.setStatus(editor.t("status.save_failed", { error: String(e) }));
@@ -1068,19 +1300,6 @@ async function saveTheme(name?: string): Promise<boolean> {
1068
1300
  }
1069
1301
  }
1070
1302
 
1071
- /**
1072
- * Set a theme as the default in config and apply it immediately
1073
- */
1074
- async function setThemeAsDefault(themeName: string): Promise<void> {
1075
- try {
1076
- // Use the editor API to apply and persist the theme
1077
- editor.applyTheme(themeName);
1078
- editor.setStatus(editor.t("status.default_set", { name: themeName }));
1079
- } catch (e) {
1080
- editor.setStatus(editor.t("status.apply_failed", { error: String(e) }));
1081
- }
1082
- }
1083
-
1084
1303
  /**
1085
1304
  * Create a default/empty theme
1086
1305
  */
@@ -1174,12 +1393,228 @@ globalThis.onThemeEditorCursorMoved = function(data: {
1174
1393
 
1175
1394
  editor.on("cursor_moved", "onThemeEditorCursorMoved");
1176
1395
 
1396
+ /**
1397
+ * Handle buffer_closed event to reset state when buffer is closed by any means
1398
+ */
1399
+ globalThis.onThemeEditorBufferClosed = function(data: {
1400
+ buffer_id: number;
1401
+ }): void {
1402
+ if (state.bufferId !== null && data.buffer_id === state.bufferId) {
1403
+ // Reset state when our buffer is closed
1404
+ state.isOpen = false;
1405
+ state.bufferId = null;
1406
+ state.splitId = null;
1407
+ state.themeData = {};
1408
+ state.originalThemeData = {};
1409
+ state.hasChanges = false;
1410
+ }
1411
+ };
1412
+
1413
+ editor.on("buffer_closed", "onThemeEditorBufferClosed");
1414
+
1415
+ // =============================================================================
1416
+ // Smart Navigation - Skip Non-Selectable Lines
1417
+ // =============================================================================
1418
+
1419
+ interface SelectableEntry {
1420
+ byteOffset: number;
1421
+ valueByteOffset: number; // Position at the value (after "field: ")
1422
+ index: number;
1423
+ isSection: boolean;
1424
+ path: string;
1425
+ }
1426
+
1427
+ /**
1428
+ * Get byte offsets for all selectable entries (fields and sections)
1429
+ */
1430
+ function getSelectableEntries(): SelectableEntry[] {
1431
+ const entries = buildDisplayEntries();
1432
+ const selectableEntries: SelectableEntry[] = [];
1433
+ let byteOffset = 0;
1434
+
1435
+ for (const entry of entries) {
1436
+ const props = entry.properties as Record<string, unknown>;
1437
+ const entryType = props.type as string;
1438
+ const path = (props.path as string) || "";
1439
+
1440
+ // Only fields and sections are selectable (they have index property)
1441
+ if ((entryType === "field" || entryType === "section") && typeof props.index === "number") {
1442
+ // For fields, calculate position at the color value (after "FieldName: X ")
1443
+ let valueByteOffset = byteOffset;
1444
+ if (entryType === "field") {
1445
+ const colonIdx = entry.text.indexOf(":");
1446
+ if (colonIdx >= 0) {
1447
+ // Position at the hex value, after ": X " (colon + space + X + 2 spaces = 5 chars)
1448
+ valueByteOffset = byteOffset + getUtf8ByteLength(entry.text.substring(0, colonIdx + 5));
1449
+ }
1450
+ }
1451
+
1452
+ selectableEntries.push({
1453
+ byteOffset,
1454
+ valueByteOffset,
1455
+ index: props.index as number,
1456
+ isSection: entryType === "section",
1457
+ path,
1458
+ });
1459
+ }
1460
+
1461
+ byteOffset += getUtf8ByteLength(entry.text);
1462
+ }
1463
+
1464
+ return selectableEntries;
1465
+ }
1466
+
1467
+ /**
1468
+ * Get the current selectable entry index based on cursor position
1469
+ */
1470
+ function getCurrentSelectableIndex(): number {
1471
+ if (state.bufferId === null) return -1;
1472
+
1473
+ const props = editor.getTextPropertiesAtCursor(state.bufferId);
1474
+ if (props.length > 0 && typeof props[0].index === "number") {
1475
+ return props[0].index as number;
1476
+ }
1477
+ return -1;
1478
+ }
1479
+
1480
+ /**
1481
+ * Get the current field path at cursor
1482
+ */
1483
+ function getCurrentFieldPath(): string | null {
1484
+ if (state.bufferId === null) return null;
1485
+
1486
+ const props = editor.getTextPropertiesAtCursor(state.bufferId);
1487
+ if (props.length > 0 && typeof props[0].path === "string") {
1488
+ return props[0].path as string;
1489
+ }
1490
+ return null;
1491
+ }
1492
+
1493
+ /**
1494
+ * Move cursor to a field by path (positions at value for fields)
1495
+ */
1496
+ function moveCursorToField(path: string): void {
1497
+ if (state.bufferId === null) return;
1498
+
1499
+ const selectableEntries = getSelectableEntries();
1500
+ for (const entry of selectableEntries) {
1501
+ if (entry.path === path) {
1502
+ // Use valueByteOffset for fields, byteOffset for sections
1503
+ const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
1504
+ editor.setBufferCursor(state.bufferId, targetOffset);
1505
+ return;
1506
+ }
1507
+ }
1508
+ }
1509
+
1510
+ /**
1511
+ * Navigate to the next selectable field/section
1512
+ */
1513
+ globalThis.theme_editor_nav_down = function(): void {
1514
+ if (state.bufferId === null) return;
1515
+
1516
+ const selectableEntries = getSelectableEntries();
1517
+ const currentIndex = getCurrentSelectableIndex();
1518
+
1519
+ // Find next selectable entry after current
1520
+ for (const entry of selectableEntries) {
1521
+ if (entry.index > currentIndex) {
1522
+ // Use valueByteOffset for fields, byteOffset for sections
1523
+ const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
1524
+ editor.setBufferCursor(state.bufferId, targetOffset);
1525
+ return;
1526
+ }
1527
+ }
1528
+
1529
+ // Already at last selectable, stay there
1530
+ editor.setStatus(editor.t("status.at_last_field"));
1531
+ };
1532
+
1533
+ /**
1534
+ * Navigate to the previous selectable field/section
1535
+ */
1536
+ globalThis.theme_editor_nav_up = function(): void {
1537
+ if (state.bufferId === null) return;
1538
+
1539
+ const selectableEntries = getSelectableEntries();
1540
+ const currentIndex = getCurrentSelectableIndex();
1541
+
1542
+ // Find previous selectable entry before current
1543
+ for (let i = selectableEntries.length - 1; i >= 0; i--) {
1544
+ const entry = selectableEntries[i];
1545
+ if (entry.index < currentIndex) {
1546
+ // Use valueByteOffset for fields, byteOffset for sections
1547
+ const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
1548
+ editor.setBufferCursor(state.bufferId, targetOffset);
1549
+ return;
1550
+ }
1551
+ }
1552
+
1553
+ // Already at first selectable, stay there
1554
+ editor.setStatus(editor.t("status.at_first_field"));
1555
+ };
1556
+
1557
+ /**
1558
+ * Navigate to next element (Tab) - includes both fields and sections
1559
+ */
1560
+ globalThis.theme_editor_nav_next_section = function(): void {
1561
+ if (state.bufferId === null) return;
1562
+
1563
+ const selectableEntries = getSelectableEntries();
1564
+ const currentIndex = getCurrentSelectableIndex();
1565
+
1566
+ // Find next selectable entry after current
1567
+ for (const entry of selectableEntries) {
1568
+ if (entry.index > currentIndex) {
1569
+ // Use valueByteOffset for fields, byteOffset for sections
1570
+ const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
1571
+ editor.setBufferCursor(state.bufferId, targetOffset);
1572
+ return;
1573
+ }
1574
+ }
1575
+
1576
+ // Wrap to first entry
1577
+ if (selectableEntries.length > 0) {
1578
+ const entry = selectableEntries[0];
1579
+ const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
1580
+ editor.setBufferCursor(state.bufferId, targetOffset);
1581
+ }
1582
+ };
1583
+
1584
+ /**
1585
+ * Navigate to previous element (Shift+Tab) - includes both fields and sections
1586
+ */
1587
+ globalThis.theme_editor_nav_prev_section = function(): void {
1588
+ if (state.bufferId === null) return;
1589
+
1590
+ const selectableEntries = getSelectableEntries();
1591
+ const currentIndex = getCurrentSelectableIndex();
1592
+
1593
+ // Find previous selectable entry before current
1594
+ for (let i = selectableEntries.length - 1; i >= 0; i--) {
1595
+ const entry = selectableEntries[i];
1596
+ if (entry.index < currentIndex) {
1597
+ // Use valueByteOffset for fields, byteOffset for sections
1598
+ const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
1599
+ editor.setBufferCursor(state.bufferId, targetOffset);
1600
+ return;
1601
+ }
1602
+ }
1603
+
1604
+ // Wrap to last entry
1605
+ if (selectableEntries.length > 0) {
1606
+ const entry = selectableEntries[selectableEntries.length - 1];
1607
+ const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
1608
+ editor.setBufferCursor(state.bufferId, targetOffset);
1609
+ }
1610
+ };
1611
+
1177
1612
  // =============================================================================
1178
1613
  // Public Commands
1179
1614
  // =============================================================================
1180
1615
 
1181
1616
  /**
1182
- * Open the theme editor
1617
+ * Open the theme editor - prompts user to select theme first
1183
1618
  */
1184
1619
  globalThis.open_theme_editor = async function(): Promise<void> {
1185
1620
  if (state.isOpen) {
@@ -1187,8 +1622,6 @@ globalThis.open_theme_editor = async function(): Promise<void> {
1187
1622
  return;
1188
1623
  }
1189
1624
 
1190
- editor.setStatus(editor.t("status.loading"));
1191
-
1192
1625
  // Save context
1193
1626
  state.sourceSplitId = editor.getActiveSplitId();
1194
1627
  state.sourceBufferId = editor.getActiveBufferId();
@@ -1196,13 +1629,52 @@ globalThis.open_theme_editor = async function(): Promise<void> {
1196
1629
  // Load available themes
1197
1630
  state.builtinThemes = await loadBuiltinThemes();
1198
1631
 
1199
- // Create default theme data
1200
- state.themeData = createDefaultTheme();
1201
- state.originalThemeData = deepClone(state.themeData);
1202
- state.themeName = "custom";
1203
- state.themePath = null;
1204
- state.hasChanges = false;
1632
+ // Get current theme name from config
1633
+ const config = editor.getConfig() as Record<string, unknown>;
1634
+ const currentThemeName = (config?.theme as string) || "dark";
1635
+
1636
+ // Prompt user to select which theme to edit
1637
+ editor.startPrompt(editor.t("prompt.select_theme_to_edit"), "theme-select-initial");
1638
+
1639
+ const suggestions: PromptSuggestion[] = [];
1640
+
1641
+ // Add user themes first
1642
+ const userThemes = listUserThemes();
1643
+ for (const name of userThemes) {
1644
+ const isCurrent = name === currentThemeName;
1645
+ suggestions.push({
1646
+ text: name,
1647
+ description: isCurrent ? editor.t("suggestion.user_theme_current") : editor.t("suggestion.user_theme"),
1648
+ value: `user:${name}`,
1649
+ });
1650
+ }
1651
+
1652
+ // Add built-in themes
1653
+ for (const name of state.builtinThemes) {
1654
+ const isCurrent = name === currentThemeName;
1655
+ suggestions.push({
1656
+ text: name,
1657
+ description: isCurrent ? editor.t("suggestion.builtin_theme_current") : editor.t("suggestion.builtin_theme"),
1658
+ value: `builtin:${name}`,
1659
+ });
1660
+ }
1661
+
1662
+ // Sort suggestions to put current theme first
1663
+ suggestions.sort((a, b) => {
1664
+ const aIsCurrent = a.description.includes("current");
1665
+ const bIsCurrent = b.description.includes("current");
1666
+ if (aIsCurrent && !bIsCurrent) return -1;
1667
+ if (!aIsCurrent && bIsCurrent) return 1;
1668
+ return 0;
1669
+ });
1205
1670
 
1671
+ editor.setPromptSuggestions(suggestions);
1672
+ };
1673
+
1674
+ /**
1675
+ * Actually open the theme editor with loaded theme data
1676
+ */
1677
+ async function doOpenThemeEditor(): Promise<void> {
1206
1678
  // Build initial entries
1207
1679
  const entries = buildDisplayEntries();
1208
1680
 
@@ -1222,14 +1694,12 @@ globalThis.open_theme_editor = async function(): Promise<void> {
1222
1694
  state.bufferId = bufferId;
1223
1695
  state.splitId = null;
1224
1696
 
1225
- editor.setContext("theme-editor", true);
1226
-
1227
1697
  applyHighlighting();
1228
1698
  editor.setStatus(editor.t("status.ready"));
1229
1699
  } else {
1230
1700
  editor.setStatus(editor.t("status.open_failed"));
1231
1701
  }
1232
- };
1702
+ }
1233
1703
 
1234
1704
  /**
1235
1705
  * Close the theme editor
@@ -1238,11 +1708,23 @@ globalThis.theme_editor_close = function(): void {
1238
1708
  if (!state.isOpen) return;
1239
1709
 
1240
1710
  if (state.hasChanges) {
1241
- editor.setStatus(editor.t("status.unsaved_discarded"));
1711
+ // Show confirmation prompt before closing with unsaved changes
1712
+ editor.startPrompt(editor.t("prompt.discard_confirm"), "theme-discard-confirm");
1713
+ const suggestions: PromptSuggestion[] = [
1714
+ { text: editor.t("prompt.discard_yes"), description: "", value: "discard" },
1715
+ { text: editor.t("prompt.discard_no"), description: "", value: "keep" },
1716
+ ];
1717
+ editor.setPromptSuggestions(suggestions);
1718
+ return;
1242
1719
  }
1243
1720
 
1244
- editor.setContext("theme-editor", false);
1721
+ doCloseEditor();
1722
+ };
1245
1723
 
1724
+ /**
1725
+ * Actually close the editor (called after confirmation or when no changes)
1726
+ */
1727
+ function doCloseEditor(): void {
1246
1728
  // Close the buffer (this will switch to another buffer in the same split)
1247
1729
  if (state.bufferId !== null) {
1248
1730
  editor.closeBuffer(state.bufferId);
@@ -1257,6 +1739,27 @@ globalThis.theme_editor_close = function(): void {
1257
1739
  state.hasChanges = false;
1258
1740
 
1259
1741
  editor.setStatus(editor.t("status.closed"));
1742
+ }
1743
+
1744
+ /**
1745
+ * Handle discard confirmation prompt
1746
+ */
1747
+ globalThis.onThemeDiscardPromptConfirmed = function(args: {
1748
+ prompt_type: string;
1749
+ selected_index: number | null;
1750
+ input: string;
1751
+ }): boolean {
1752
+ if (args.prompt_type !== "theme-discard-confirm") return true;
1753
+
1754
+ const response = args.input.trim().toLowerCase();
1755
+ if (response === "discard" || args.selected_index === 0) {
1756
+ editor.setStatus(editor.t("status.unsaved_discarded"));
1757
+ doCloseEditor();
1758
+ } else {
1759
+ editor.setStatus(editor.t("status.cancelled"));
1760
+ }
1761
+
1762
+ return false;
1260
1763
  };
1261
1764
 
1262
1765
  /**
@@ -1297,49 +1800,116 @@ globalThis.theme_editor_toggle_section = function(): void {
1297
1800
  };
1298
1801
 
1299
1802
  /**
1300
- * Copy from a built-in theme
1803
+ * Open a theme (builtin or user) for editing
1301
1804
  */
1302
- globalThis.theme_editor_copy_from_builtin = function(): void {
1303
- editor.startPrompt(editor.t("prompt.copy_theme"), "theme-copy-builtin");
1805
+ globalThis.theme_editor_open = function(): void {
1806
+ editor.startPrompt(editor.t("prompt.open_theme"), "theme-open");
1304
1807
 
1305
- const suggestions: PromptSuggestion[] = state.builtinThemes.map(name => ({
1306
- text: name,
1307
- description: editor.t("suggestion.builtin_theme"),
1308
- value: name,
1309
- }));
1808
+ const suggestions: PromptSuggestion[] = [];
1310
1809
 
1311
- editor.setPromptSuggestions(suggestions);
1312
- };
1810
+ // Add user themes first
1811
+ const userThemes = listUserThemes();
1812
+ for (const name of userThemes) {
1813
+ suggestions.push({
1814
+ text: name,
1815
+ description: editor.t("suggestion.user_theme"),
1816
+ value: `user:${name}`,
1817
+ });
1818
+ }
1313
1819
 
1314
- /**
1315
- * Set theme name
1316
- */
1317
- globalThis.theme_editor_set_name = function(): void {
1318
- editor.startPrompt(editor.t("prompt.theme_name"), "theme-name");
1820
+ // Add built-in themes
1821
+ for (const name of state.builtinThemes) {
1822
+ suggestions.push({
1823
+ text: name,
1824
+ description: editor.t("suggestion.builtin_theme"),
1825
+ value: `builtin:${name}`,
1826
+ });
1827
+ }
1319
1828
 
1320
- editor.setPromptSuggestions([{
1321
- text: state.themeName,
1322
- description: editor.t("suggestion.current"),
1323
- value: state.themeName,
1324
- }]);
1829
+ editor.setPromptSuggestions(suggestions);
1325
1830
  };
1326
1831
 
1327
1832
  /**
1328
1833
  * Save theme
1329
1834
  */
1330
1835
  globalThis.theme_editor_save = async function(): Promise<void> {
1331
- if (!state.hasChanges && state.themePath) {
1836
+ // Save cursor path for restoration after save
1837
+ state.savedCursorPath = getCurrentFieldPath();
1838
+
1839
+ // Built-in themes require Save As
1840
+ if (state.isBuiltin) {
1841
+ editor.setStatus(editor.t("status.builtin_requires_save_as"));
1842
+ theme_editor_save_as();
1843
+ return;
1844
+ }
1845
+
1846
+ // If theme has never been saved (no path), trigger "Save As" instead
1847
+ if (!state.themePath) {
1848
+ theme_editor_save_as();
1849
+ return;
1850
+ }
1851
+
1852
+ if (!state.hasChanges) {
1332
1853
  editor.setStatus(editor.t("status.no_changes"));
1333
1854
  return;
1334
1855
  }
1335
1856
 
1336
- await saveTheme();
1857
+ // Check for name collision if name has changed since last save
1858
+ const userThemesDir = getUserThemesDir();
1859
+ const targetPath = editor.pathJoin(userThemesDir, `${state.themeName}.json`);
1860
+
1861
+ if (state.themePath !== targetPath && editor.fileExists(targetPath)) {
1862
+ // File exists with this name - ask for confirmation
1863
+ editor.startPrompt(editor.t("prompt.overwrite_confirm", { name: state.themeName }), "theme-overwrite-confirm");
1864
+ const suggestions: PromptSuggestion[] = [
1865
+ { text: editor.t("prompt.overwrite_yes"), description: "", value: "overwrite" },
1866
+ { text: editor.t("prompt.overwrite_no"), description: "", value: "cancel" },
1867
+ ];
1868
+ editor.setPromptSuggestions(suggestions);
1869
+ return;
1870
+ }
1871
+
1872
+ await saveTheme(undefined, state.savedCursorPath);
1873
+ };
1874
+
1875
+ /**
1876
+ * Handle overwrite confirmation prompt
1877
+ */
1878
+ globalThis.onThemeOverwritePromptConfirmed = async function(args: {
1879
+ prompt_type: string;
1880
+ selected_index: number | null;
1881
+ input: string;
1882
+ }): Promise<boolean> {
1883
+ if (args.prompt_type !== "theme-overwrite-confirm") return true;
1884
+
1885
+ const response = args.input.trim().toLowerCase();
1886
+ if (response === "overwrite" || args.selected_index === 0) {
1887
+ // Use pending name if set (from Save As), otherwise use current name
1888
+ const nameToSave = state.pendingSaveName || state.themeName;
1889
+ state.themeName = nameToSave;
1890
+ state.themeData.name = nameToSave;
1891
+ state.pendingSaveName = null;
1892
+ const restorePath = state.savedCursorPath;
1893
+ state.savedCursorPath = null;
1894
+ await saveTheme(nameToSave, restorePath);
1895
+ } else {
1896
+ state.pendingSaveName = null;
1897
+ state.savedCursorPath = null;
1898
+ editor.setStatus(editor.t("status.cancelled"));
1899
+ }
1900
+
1901
+ return false;
1337
1902
  };
1338
1903
 
1339
1904
  /**
1340
1905
  * Save theme as (new name)
1341
1906
  */
1342
1907
  globalThis.theme_editor_save_as = function(): void {
1908
+ // Save cursor path for restoration after save (if not already saved by theme_editor_save)
1909
+ if (!state.savedCursorPath) {
1910
+ state.savedCursorPath = getCurrentFieldPath();
1911
+ }
1912
+
1343
1913
  editor.startPrompt(editor.t("prompt.save_as"), "theme-save-as");
1344
1914
 
1345
1915
  editor.setPromptSuggestions([{
@@ -1349,34 +1919,6 @@ globalThis.theme_editor_save_as = function(): void {
1349
1919
  }]);
1350
1920
  };
1351
1921
 
1352
- /**
1353
- * Set current theme as default
1354
- */
1355
- globalThis.theme_editor_set_as_default = function(): void {
1356
- editor.startPrompt(editor.t("prompt.set_default"), "theme-set-default");
1357
-
1358
- // Suggest current theme and all builtins
1359
- const suggestions: PromptSuggestion[] = [];
1360
-
1361
- if (state.themeName && state.themePath) {
1362
- suggestions.push({
1363
- text: state.themeName,
1364
- description: editor.t("suggestion.current"),
1365
- value: state.themeName,
1366
- });
1367
- }
1368
-
1369
- for (const name of state.builtinThemes) {
1370
- suggestions.push({
1371
- text: name,
1372
- description: editor.t("suggestion.builtin"),
1373
- value: name,
1374
- });
1375
- }
1376
-
1377
- editor.setPromptSuggestions(suggestions);
1378
- };
1379
-
1380
1922
  /**
1381
1923
  * Reload theme
1382
1924
  */
@@ -1407,11 +1949,68 @@ globalThis.theme_editor_show_help = function(): void {
1407
1949
  editor.setStatus(editor.t("status.help"));
1408
1950
  };
1409
1951
 
1952
+ /**
1953
+ * Delete the current user theme
1954
+ */
1955
+ globalThis.theme_editor_delete = function(): void {
1956
+ // Can only delete saved user themes
1957
+ if (!state.themePath) {
1958
+ editor.setStatus(editor.t("status.cannot_delete_unsaved"));
1959
+ return;
1960
+ }
1961
+
1962
+ // Show confirmation dialog
1963
+ editor.startPrompt(editor.t("prompt.delete_confirm", { name: state.themeName }), "theme-delete-confirm");
1964
+ const suggestions: PromptSuggestion[] = [
1965
+ { text: editor.t("prompt.delete_yes"), description: "", value: "delete" },
1966
+ { text: editor.t("prompt.delete_no"), description: "", value: "cancel" },
1967
+ ];
1968
+ editor.setPromptSuggestions(suggestions);
1969
+ };
1970
+
1971
+ /**
1972
+ * Handle delete confirmation prompt
1973
+ */
1974
+ globalThis.onThemeDeletePromptConfirmed = async function(args: {
1975
+ prompt_type: string;
1976
+ selected_index: number | null;
1977
+ input: string;
1978
+ }): Promise<boolean> {
1979
+ if (args.prompt_type !== "theme-delete-confirm") return true;
1980
+
1981
+ const value = args.input.trim();
1982
+ if (value === "delete" || value === editor.t("prompt.delete_yes")) {
1983
+ if (state.themePath) {
1984
+ try {
1985
+ // Delete the theme file
1986
+ await editor.deleteFile(state.themePath);
1987
+ const deletedName = state.themeName;
1988
+
1989
+ // Reset to default theme
1990
+ state.themeData = createDefaultTheme();
1991
+ state.originalThemeData = deepClone(state.themeData);
1992
+ state.themeName = "custom";
1993
+ state.themePath = null;
1994
+ state.hasChanges = false;
1995
+ updateDisplay();
1996
+
1997
+ editor.setStatus(editor.t("status.deleted", { name: deletedName }));
1998
+ } catch (e) {
1999
+ editor.setStatus(editor.t("status.delete_failed", { error: String(e) }));
2000
+ }
2001
+ }
2002
+ } else {
2003
+ editor.setStatus(editor.t("status.cancelled"));
2004
+ }
2005
+
2006
+ return true;
2007
+ };
2008
+
1410
2009
  // =============================================================================
1411
2010
  // Command Registration
1412
2011
  // =============================================================================
1413
2012
 
1414
- // Main command to open theme editor
2013
+ // Main command to open theme editor (always available)
1415
2014
  editor.registerCommand(
1416
2015
  "%cmd.edit_theme",
1417
2016
  "%cmd.edit_theme_desc",
@@ -1419,76 +2018,21 @@ editor.registerCommand(
1419
2018
  "normal"
1420
2019
  );
1421
2020
 
1422
- // Context-specific commands
1423
- editor.registerCommand(
1424
- "%cmd.close_editor",
1425
- "%cmd.close_editor_desc",
1426
- "theme_editor_close",
1427
- "normal,theme-editor"
1428
- );
1429
-
1430
- editor.registerCommand(
1431
- "%cmd.edit_color",
1432
- "%cmd.edit_color_desc",
1433
- "theme_editor_edit_color",
1434
- "normal,theme-editor"
1435
- );
1436
-
1437
- editor.registerCommand(
1438
- "%cmd.toggle_section",
1439
- "%cmd.toggle_section_desc",
1440
- "theme_editor_toggle_section",
1441
- "normal,theme-editor"
1442
- );
1443
-
1444
- editor.registerCommand(
1445
- "%cmd.copy_builtin",
1446
- "%cmd.copy_builtin_desc",
1447
- "theme_editor_copy_from_builtin",
1448
- "normal,theme-editor"
1449
- );
1450
-
1451
- editor.registerCommand(
1452
- "%cmd.set_name",
1453
- "%cmd.set_name_desc",
1454
- "theme_editor_set_name",
1455
- "normal,theme-editor"
1456
- );
1457
-
1458
- editor.registerCommand(
1459
- "%cmd.save",
1460
- "%cmd.save_desc",
1461
- "theme_editor_save",
1462
- "normal,theme-editor"
1463
- );
1464
-
1465
- editor.registerCommand(
1466
- "%cmd.save_as",
1467
- "%cmd.save_as_desc",
1468
- "theme_editor_save_as",
1469
- "normal,theme-editor"
1470
- );
1471
-
1472
- editor.registerCommand(
1473
- "%cmd.set_default",
1474
- "%cmd.set_default_desc",
1475
- "theme_editor_set_as_default",
1476
- "normal,theme-editor"
1477
- );
1478
-
1479
- editor.registerCommand(
1480
- "%cmd.reload",
1481
- "%cmd.reload_desc",
1482
- "theme_editor_reload",
1483
- "normal,theme-editor"
1484
- );
1485
-
1486
- editor.registerCommand(
1487
- "%cmd.show_help",
1488
- "%cmd.show_help_desc",
1489
- "theme_editor_show_help",
1490
- "normal,theme-editor"
1491
- );
2021
+ // Buffer-scoped commands - only visible when a buffer with mode "theme-editor" is focused
2022
+ // The core automatically checks the focused buffer's mode against command contexts
2023
+ editor.registerCommand("%cmd.close_editor", "%cmd.close_editor_desc", "theme_editor_close", "theme-editor");
2024
+ editor.registerCommand("%cmd.edit_color", "%cmd.edit_color_desc", "theme_editor_edit_color", "theme-editor");
2025
+ editor.registerCommand("%cmd.toggle_section", "%cmd.toggle_section_desc", "theme_editor_toggle_section", "theme-editor");
2026
+ editor.registerCommand("%cmd.open_theme", "%cmd.open_theme_desc", "theme_editor_open", "theme-editor");
2027
+ editor.registerCommand("%cmd.save", "%cmd.save_desc", "theme_editor_save", "theme-editor");
2028
+ editor.registerCommand("%cmd.save_as", "%cmd.save_as_desc", "theme_editor_save_as", "theme-editor");
2029
+ editor.registerCommand("%cmd.reload", "%cmd.reload_desc", "theme_editor_reload", "theme-editor");
2030
+ editor.registerCommand("%cmd.show_help", "%cmd.show_help_desc", "theme_editor_show_help", "theme-editor");
2031
+ editor.registerCommand("%cmd.delete_theme", "%cmd.delete_theme_desc", "theme_editor_delete", "theme-editor");
2032
+ editor.registerCommand("%cmd.nav_up", "%cmd.nav_up_desc", "theme_editor_nav_up", "theme-editor");
2033
+ editor.registerCommand("%cmd.nav_down", "%cmd.nav_down_desc", "theme_editor_nav_down", "theme-editor");
2034
+ editor.registerCommand("%cmd.nav_next", "%cmd.nav_next_desc", "theme_editor_nav_next_section", "theme-editor");
2035
+ editor.registerCommand("%cmd.nav_prev", "%cmd.nav_prev_desc", "theme_editor_nav_prev_section", "theme-editor");
1492
2036
 
1493
2037
  // =============================================================================
1494
2038
  // Plugin Initialization