@fresh-editor/fresh-editor 0.2.11 → 0.2.13
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.
- package/CHANGELOG.md +89 -0
- package/README.md +10 -0
- package/package.json +1 -1
- package/plugins/audit_mode.ts +79 -58
- package/plugins/check-types.sh +1 -0
- package/plugins/clangd-lsp.ts +9 -6
- package/plugins/clangd_support.ts +12 -8
- package/plugins/code-tour.ts +15 -10
- package/plugins/config-schema.json +70 -3
- package/plugins/csharp_support.ts +15 -10
- package/plugins/css-lsp.ts +9 -6
- package/plugins/diagnostics_panel.ts +25 -18
- package/plugins/examples/README.md +1 -2
- package/plugins/examples/async_demo.ts +28 -28
- package/plugins/examples/bookmarks.ts +34 -32
- package/plugins/examples/buffer_query_demo.ts +20 -20
- package/plugins/examples/hello_world.ts +46 -10
- package/plugins/examples/virtual_buffer_demo.ts +16 -12
- package/plugins/find_references.ts +7 -5
- package/plugins/git_blame.ts +13 -9
- package/plugins/git_explorer.ts +9 -6
- package/plugins/git_find_file.ts +7 -5
- package/plugins/git_grep.ts +3 -2
- package/plugins/git_gutter.ts +15 -10
- package/plugins/git_log.ts +27 -18
- package/plugins/go-lsp.ts +9 -6
- package/plugins/html-lsp.ts +9 -6
- package/plugins/java-lsp.ts +9 -6
- package/plugins/json-lsp.ts +9 -6
- package/plugins/latex-lsp.ts +9 -6
- package/plugins/lib/finder.ts +1 -0
- package/plugins/lib/fresh.d.ts +141 -16
- package/plugins/live_grep.ts +3 -2
- package/plugins/markdown_compose.ts +33 -23
- package/plugins/markdown_source.ts +24 -10
- package/plugins/marksman-lsp.ts +9 -6
- package/plugins/merge_conflict.ts +33 -22
- package/plugins/odin-lsp.ts +9 -6
- package/plugins/path_complete.ts +3 -2
- package/plugins/pkg.ts +70 -48
- package/plugins/python-lsp.ts +9 -6
- package/plugins/rust-lsp.ts +102 -6
- package/plugins/search_replace.ts +32 -21
- package/plugins/templ-lsp.ts +9 -6
- package/plugins/test_i18n.ts +3 -2
- package/plugins/theme_editor.i18n.json +28 -14
- package/plugins/theme_editor.ts +1230 -495
- package/plugins/typescript-lsp.ts +9 -6
- package/plugins/vi_mode.ts +487 -297
- package/plugins/welcome.ts +9 -6
- package/plugins/zig-lsp.ts +9 -6
package/plugins/theme_editor.ts
CHANGED
|
@@ -57,6 +57,130 @@ const ALL_COLOR_NAMES = [...NAMED_COLOR_LIST, ...SPECIAL_COLORS];
|
|
|
57
57
|
*/
|
|
58
58
|
type ColorValue = RGB | string;
|
|
59
59
|
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Layout Constants & Panel Types
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
const LEFT_WIDTH = 38;
|
|
65
|
+
const RIGHT_WIDTH = 61;
|
|
66
|
+
|
|
67
|
+
type PickerFocusTarget =
|
|
68
|
+
| { type: "hex-input" }
|
|
69
|
+
| { type: "named-colors"; index: number }
|
|
70
|
+
| { type: "palette"; row: number; col: number };
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Named Color Grid (for picker panel)
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
const NAMED_COLORS_PER_ROW = 6;
|
|
77
|
+
const NAMED_COLOR_GRID: Array<Array<{ display: string; value: string; rgb: RGB | null }>> = [
|
|
78
|
+
[
|
|
79
|
+
{ display: "Black", value: "Black", rgb: [0, 0, 0] },
|
|
80
|
+
{ display: "Red", value: "Red", rgb: [255, 0, 0] },
|
|
81
|
+
{ display: "Green", value: "Green", rgb: [0, 128, 0] },
|
|
82
|
+
{ display: "Yellow", value: "Yellow", rgb: [255, 255, 0] },
|
|
83
|
+
{ display: "Blue", value: "Blue", rgb: [0, 0, 255] },
|
|
84
|
+
{ display: "Magenta", value: "Magenta", rgb: [255, 0, 255] },
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
{ display: "Cyan", value: "Cyan", rgb: [0, 255, 255] },
|
|
88
|
+
{ display: "Gray", value: "Gray", rgb: [128, 128, 128] },
|
|
89
|
+
{ display: "DkGray", value: "DarkGray", rgb: [169, 169, 169] },
|
|
90
|
+
{ display: "LtRed", value: "LightRed", rgb: [255, 128, 128] },
|
|
91
|
+
{ display: "LtGreen", value: "LightGreen", rgb: [144, 238, 144] },
|
|
92
|
+
{ display: "LtYellw", value: "LightYellow", rgb: [255, 255, 224] },
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
{ display: "LtBlue", value: "LightBlue", rgb: [173, 216, 230] },
|
|
96
|
+
{ display: "LtMag", value: "LightMagenta", rgb: [255, 128, 255] },
|
|
97
|
+
{ display: "LtCyan", value: "LightCyan", rgb: [224, 255, 255] },
|
|
98
|
+
{ display: "White", value: "White", rgb: [255, 255, 255] },
|
|
99
|
+
{ display: "Default", value: "Default", rgb: null },
|
|
100
|
+
{ display: "Reset", value: "Reset", rgb: null },
|
|
101
|
+
],
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Extended Color Palette
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
const PALETTE_COLS = 12;
|
|
109
|
+
const PALETTE_ROWS = 4;
|
|
110
|
+
const PALETTE_LIGHTNESSES = [25, 40, 60, 75];
|
|
111
|
+
|
|
112
|
+
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
113
|
+
s /= 100;
|
|
114
|
+
l /= 100;
|
|
115
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
116
|
+
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
|
117
|
+
const m = l - c / 2;
|
|
118
|
+
let r = 0, g = 0, b = 0;
|
|
119
|
+
if (h < 60) { r = c; g = x; }
|
|
120
|
+
else if (h < 120) { r = x; g = c; }
|
|
121
|
+
else if (h < 180) { g = c; b = x; }
|
|
122
|
+
else if (h < 240) { g = x; b = c; }
|
|
123
|
+
else if (h < 300) { r = x; b = c; }
|
|
124
|
+
else { r = c; b = x; }
|
|
125
|
+
return [
|
|
126
|
+
Math.round((r + m) * 255),
|
|
127
|
+
Math.round((g + m) * 255),
|
|
128
|
+
Math.round((b + m) * 255),
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let cachedPalette: RGB[][] | null = null;
|
|
133
|
+
|
|
134
|
+
function getExtendedPalette(): RGB[][] {
|
|
135
|
+
if (cachedPalette) return cachedPalette;
|
|
136
|
+
const palette: RGB[][] = [];
|
|
137
|
+
for (let row = 0; row < PALETTE_ROWS; row++) {
|
|
138
|
+
const rowColors: RGB[] = [];
|
|
139
|
+
for (let col = 0; col < PALETTE_COLS; col++) {
|
|
140
|
+
const hue = col * 30;
|
|
141
|
+
rowColors.push(hslToRgb(hue, 80, PALETTE_LIGHTNESSES[row]));
|
|
142
|
+
}
|
|
143
|
+
palette.push(rowColors);
|
|
144
|
+
}
|
|
145
|
+
cachedPalette = palette;
|
|
146
|
+
return palette;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Preview Tokens
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
const PREVIEW_LINES: Array<Array<{ text: string; syntaxType: string }>> = [
|
|
154
|
+
[
|
|
155
|
+
{ text: "fn", syntaxType: "keyword" },
|
|
156
|
+
{ text: " ", syntaxType: "" },
|
|
157
|
+
{ text: "main", syntaxType: "function" },
|
|
158
|
+
{ text: "() {", syntaxType: "operator" },
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
{ text: " ", syntaxType: "" },
|
|
162
|
+
{ text: "let", syntaxType: "keyword" },
|
|
163
|
+
{ text: " greeting = ", syntaxType: "" },
|
|
164
|
+
{ text: "\"Hello\"", syntaxType: "string" },
|
|
165
|
+
{ text: ";", syntaxType: "" },
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
{ text: " ", syntaxType: "" },
|
|
169
|
+
{ text: "// A comment", syntaxType: "comment" },
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
{ text: " ", syntaxType: "" },
|
|
173
|
+
{ text: "println!", syntaxType: "function" },
|
|
174
|
+
{ text: "(", syntaxType: "" },
|
|
175
|
+
{ text: "\"{}\", ", syntaxType: "string" },
|
|
176
|
+
{ text: "greeting", syntaxType: "variable" },
|
|
177
|
+
{ text: ");", syntaxType: "" },
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
{ text: "}", syntaxType: "" },
|
|
181
|
+
],
|
|
182
|
+
];
|
|
183
|
+
|
|
60
184
|
/**
|
|
61
185
|
* Theme section definition
|
|
62
186
|
*/
|
|
@@ -229,6 +353,22 @@ interface ThemeEditorState {
|
|
|
229
353
|
isBuiltin: boolean;
|
|
230
354
|
/** Saved cursor field path (for restoring after prompts) */
|
|
231
355
|
savedCursorPath: string | null;
|
|
356
|
+
/** Whether to close the editor after a successful save */
|
|
357
|
+
closeAfterSave: boolean;
|
|
358
|
+
/** Which panel has focus */
|
|
359
|
+
focusPanel: "tree" | "picker";
|
|
360
|
+
/** Focus target within picker panel */
|
|
361
|
+
pickerFocus: PickerFocusTarget;
|
|
362
|
+
/** Filter text for tree */
|
|
363
|
+
filterText: string;
|
|
364
|
+
/** Whether filter input is active */
|
|
365
|
+
filterActive: boolean;
|
|
366
|
+
/** First visible tree line index for virtual scrolling */
|
|
367
|
+
treeScrollOffset: number;
|
|
368
|
+
/** Cached viewport height */
|
|
369
|
+
viewportHeight: number;
|
|
370
|
+
/** Cached viewport width */
|
|
371
|
+
viewportWidth: number;
|
|
232
372
|
}
|
|
233
373
|
|
|
234
374
|
/**
|
|
@@ -252,6 +392,11 @@ function isThemeEditorOpen(): boolean {
|
|
|
252
392
|
state.themeData = {};
|
|
253
393
|
state.originalThemeData = {};
|
|
254
394
|
state.hasChanges = false;
|
|
395
|
+
state.focusPanel = "tree";
|
|
396
|
+
state.selectedIndex = 0;
|
|
397
|
+
state.treeScrollOffset = 0;
|
|
398
|
+
state.filterText = "";
|
|
399
|
+
state.filterActive = false;
|
|
255
400
|
}
|
|
256
401
|
|
|
257
402
|
return exists;
|
|
@@ -274,6 +419,14 @@ const state: ThemeEditorState = {
|
|
|
274
419
|
pendingSaveName: null,
|
|
275
420
|
isBuiltin: false,
|
|
276
421
|
savedCursorPath: null,
|
|
422
|
+
closeAfterSave: false,
|
|
423
|
+
focusPanel: "tree",
|
|
424
|
+
pickerFocus: { type: "hex-input" },
|
|
425
|
+
filterText: "",
|
|
426
|
+
filterActive: false,
|
|
427
|
+
treeScrollOffset: 0,
|
|
428
|
+
viewportHeight: 40,
|
|
429
|
+
viewportWidth: 120,
|
|
277
430
|
};
|
|
278
431
|
|
|
279
432
|
// =============================================================================
|
|
@@ -290,6 +443,13 @@ const colors = {
|
|
|
290
443
|
footer: [100, 100, 100] as RGB, // Gray
|
|
291
444
|
colorBlock: [200, 200, 200] as RGB, // Light gray for color swatch outline
|
|
292
445
|
selectionBg: [50, 50, 80] as RGB, // Dark blue-gray for selected field
|
|
446
|
+
divider: [60, 60, 80] as RGB, // Muted divider color
|
|
447
|
+
header: [100, 180, 255] as RGB, // Header blue
|
|
448
|
+
pickerLabel: [180, 180, 200] as RGB, // Picker section labels
|
|
449
|
+
pickerValue: [255, 255, 255] as RGB, // Picker value text
|
|
450
|
+
pickerFocusBg: [40, 60, 100] as RGB, // Picker focused item bg
|
|
451
|
+
filterText: [200, 200, 100] as RGB, // Filter input text
|
|
452
|
+
previewBg: [25, 25, 30] as RGB, // Preview background
|
|
293
453
|
};
|
|
294
454
|
|
|
295
455
|
// =============================================================================
|
|
@@ -305,8 +465,8 @@ const SHORTCUTS = {
|
|
|
305
465
|
save: "C-s",
|
|
306
466
|
save_as: "C-S-s",
|
|
307
467
|
delete: "C-d",
|
|
468
|
+
close: "Esc",
|
|
308
469
|
reload: "C-r",
|
|
309
|
-
close: "C-q",
|
|
310
470
|
help: "F1",
|
|
311
471
|
};
|
|
312
472
|
|
|
@@ -318,14 +478,17 @@ editor.defineMode(
|
|
|
318
478
|
"theme-editor",
|
|
319
479
|
"normal",
|
|
320
480
|
[
|
|
321
|
-
// Navigation
|
|
322
|
-
["Return", "
|
|
323
|
-
["Space", "
|
|
324
|
-
["Tab", "
|
|
325
|
-
["S-Tab", "
|
|
481
|
+
// Navigation
|
|
482
|
+
["Return", "theme_editor_enter"],
|
|
483
|
+
["Space", "theme_editor_enter"],
|
|
484
|
+
["Tab", "theme_editor_focus_tab"],
|
|
485
|
+
["S-Tab", "theme_editor_focus_shift_tab"],
|
|
326
486
|
["Up", "theme_editor_nav_up"],
|
|
327
487
|
["Down", "theme_editor_nav_down"],
|
|
328
|
-
["
|
|
488
|
+
["Left", "theme_editor_nav_left"],
|
|
489
|
+
["Right", "theme_editor_nav_right"],
|
|
490
|
+
["Escape", "theme_editor_escape"],
|
|
491
|
+
["/", "theme_editor_filter"],
|
|
329
492
|
[SHORTCUTS.help, "theme_editor_show_help"],
|
|
330
493
|
|
|
331
494
|
// Ctrl+ shortcuts (match common editor conventions)
|
|
@@ -334,7 +497,6 @@ editor.defineMode(
|
|
|
334
497
|
[SHORTCUTS.save_as, "theme_editor_save_as"],
|
|
335
498
|
[SHORTCUTS.delete, "theme_editor_delete"],
|
|
336
499
|
[SHORTCUTS.reload, "theme_editor_reload"],
|
|
337
|
-
[SHORTCUTS.close, "theme_editor_close"],
|
|
338
500
|
["C-h", "theme_editor_show_help"], // Alternative help key
|
|
339
501
|
],
|
|
340
502
|
true // read-only
|
|
@@ -500,82 +662,19 @@ async function loadBuiltinThemes(): Promise<string[]> {
|
|
|
500
662
|
}
|
|
501
663
|
|
|
502
664
|
/**
|
|
503
|
-
* Load
|
|
665
|
+
* Load theme data by name from the in-memory theme registry.
|
|
666
|
+
* Works for all theme types (builtin, user, package) — no file I/O needed.
|
|
504
667
|
*/
|
|
505
|
-
|
|
668
|
+
function loadThemeFile(name: string): Record<string, unknown> | null {
|
|
506
669
|
try {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
const builtinThemes = typeof rawThemes === "string"
|
|
510
|
-
? JSON.parse(rawThemes) as Record<string, string>
|
|
511
|
-
: rawThemes as Record<string, string>;
|
|
512
|
-
if (name in builtinThemes) {
|
|
513
|
-
return JSON.parse(builtinThemes[name]);
|
|
514
|
-
}
|
|
515
|
-
return null;
|
|
670
|
+
const data = editor.getThemeData(name);
|
|
671
|
+
return data as Record<string, unknown> | null;
|
|
516
672
|
} catch (e) {
|
|
517
673
|
editor.debug(`[theme_editor] Failed to load theme data for '${name}': ${e}`);
|
|
518
674
|
return null;
|
|
519
675
|
}
|
|
520
676
|
}
|
|
521
677
|
|
|
522
|
-
/**
|
|
523
|
-
* Load a user theme file
|
|
524
|
-
*/
|
|
525
|
-
async function loadUserThemeFile(name: string): Promise<{ data: Record<string, unknown>; path: string } | null> {
|
|
526
|
-
const userThemesDir = getUserThemesDir();
|
|
527
|
-
const themePath = editor.pathJoin(userThemesDir, `${name}.json`);
|
|
528
|
-
|
|
529
|
-
try {
|
|
530
|
-
const content = await editor.readFile(themePath);
|
|
531
|
-
return { data: JSON.parse(content), path: themePath };
|
|
532
|
-
} catch {
|
|
533
|
-
editor.debug(`Failed to load user theme: ${name}`);
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* List available user themes
|
|
540
|
-
*/
|
|
541
|
-
function listUserThemes(): string[] {
|
|
542
|
-
const userThemesDir = getUserThemesDir();
|
|
543
|
-
try {
|
|
544
|
-
const entries = editor.readDir(userThemesDir);
|
|
545
|
-
return entries
|
|
546
|
-
.filter(e => e.is_file && e.name.endsWith(".json"))
|
|
547
|
-
.map(e => e.name.replace(".json", ""));
|
|
548
|
-
} catch {
|
|
549
|
-
return [];
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Get user themes directory
|
|
555
|
-
* Uses the API to get the correct path
|
|
556
|
-
*/
|
|
557
|
-
function getUserThemesDir(): string {
|
|
558
|
-
// Use the API if available (new method)
|
|
559
|
-
if (typeof editor.getThemesDir === "function") {
|
|
560
|
-
return editor.getThemesDir();
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Fallback for older versions (deprecated)
|
|
564
|
-
// Check XDG_CONFIG_HOME first (standard on Linux)
|
|
565
|
-
const xdgConfig = editor.getEnv("XDG_CONFIG_HOME");
|
|
566
|
-
if (xdgConfig) {
|
|
567
|
-
return editor.pathJoin(xdgConfig, "fresh", "themes");
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Fall back to $HOME/.config
|
|
571
|
-
const home = editor.getEnv("HOME");
|
|
572
|
-
if (home) {
|
|
573
|
-
return editor.pathJoin(home, ".config", "fresh", "themes");
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return editor.pathJoin(editor.getCwd(), "themes");
|
|
577
|
-
}
|
|
578
|
-
|
|
579
678
|
// =============================================================================
|
|
580
679
|
// Field Building
|
|
581
680
|
// =============================================================================
|
|
@@ -586,11 +685,21 @@ function getUserThemesDir(): string {
|
|
|
586
685
|
function buildVisibleFields(): ThemeField[] {
|
|
587
686
|
const fields: ThemeField[] = [];
|
|
588
687
|
const themeSections = getThemeSections();
|
|
688
|
+
const filter = state.filterText.toLowerCase();
|
|
589
689
|
|
|
590
690
|
for (const section of themeSections) {
|
|
591
691
|
const expanded = state.expandedSections.has(section.name);
|
|
592
692
|
|
|
593
|
-
//
|
|
693
|
+
// When filtering, check if section or any of its fields match
|
|
694
|
+
if (filter) {
|
|
695
|
+
const sectionMatches = section.name.toLowerCase().includes(filter) ||
|
|
696
|
+
section.displayName.toLowerCase().includes(filter);
|
|
697
|
+
const anyFieldMatches = section.fields.some(f =>
|
|
698
|
+
f.key.toLowerCase().includes(filter) || f.displayName.toLowerCase().includes(filter));
|
|
699
|
+
if (!sectionMatches && !anyFieldMatches) continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Section header
|
|
594
703
|
fields.push({
|
|
595
704
|
def: {
|
|
596
705
|
key: section.name,
|
|
@@ -608,10 +717,16 @@ function buildVisibleFields(): ThemeField[] {
|
|
|
608
717
|
// Section fields
|
|
609
718
|
if (expanded) {
|
|
610
719
|
for (const fieldDef of section.fields) {
|
|
720
|
+
// Filter individual fields
|
|
721
|
+
if (filter) {
|
|
722
|
+
const fieldMatches = fieldDef.key.toLowerCase().includes(filter) ||
|
|
723
|
+
fieldDef.displayName.toLowerCase().includes(filter);
|
|
724
|
+
if (!fieldMatches) continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
611
727
|
const path = `${section.name}.${fieldDef.key}`;
|
|
612
728
|
const value = getNestedValue(state.themeData, path) as ColorValue || [128, 128, 128];
|
|
613
729
|
|
|
614
|
-
// fieldDef displayName and description are already translated in getThemeSections()
|
|
615
730
|
fields.push({
|
|
616
731
|
def: fieldDef,
|
|
617
732
|
value,
|
|
@@ -630,109 +745,423 @@ function buildVisibleFields(): ThemeField[] {
|
|
|
630
745
|
// UI Building
|
|
631
746
|
// =============================================================================
|
|
632
747
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
function buildDisplayEntries(): TextPropertyEntry[] {
|
|
637
|
-
const entries: TextPropertyEntry[] = [];
|
|
748
|
+
// =============================================================================
|
|
749
|
+
// Tree Panel Builder (Left)
|
|
750
|
+
// =============================================================================
|
|
638
751
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
752
|
+
interface TreeLine {
|
|
753
|
+
text: string;
|
|
754
|
+
type: string;
|
|
755
|
+
index?: number;
|
|
756
|
+
path?: string;
|
|
757
|
+
selected?: boolean;
|
|
758
|
+
colorValue?: ColorValue;
|
|
759
|
+
}
|
|
645
760
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
text: `${editor.t("panel.file", { path: state.themePath })}\n`,
|
|
649
|
-
properties: { type: "file-path" },
|
|
650
|
-
});
|
|
651
|
-
} else {
|
|
652
|
-
entries.push({
|
|
653
|
-
text: editor.t("panel.new_theme") + "\n",
|
|
654
|
-
properties: { type: "file-path" },
|
|
655
|
-
});
|
|
656
|
-
}
|
|
761
|
+
function buildTreeLines(): TreeLine[] {
|
|
762
|
+
const lines: TreeLine[] = [];
|
|
657
763
|
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
entries.push({
|
|
664
|
-
text: editor.t("panel.action_hint", SHORTCUTS) + "\n",
|
|
665
|
-
properties: { type: "footer" },
|
|
764
|
+
// Header
|
|
765
|
+
const modMarker = state.hasChanges ? " [*]" : "";
|
|
766
|
+
lines.push({
|
|
767
|
+
text: `Theme Editor: ${state.themeName}${modMarker}`,
|
|
768
|
+
type: "header",
|
|
666
769
|
});
|
|
667
770
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
properties: { type: "separator" },
|
|
671
|
-
});
|
|
771
|
+
// Separator
|
|
772
|
+
lines.push({ text: "─".repeat(36), type: "separator" });
|
|
672
773
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
774
|
+
// Filter
|
|
775
|
+
if (state.filterText) {
|
|
776
|
+
lines.push({
|
|
777
|
+
text: `Filter: [${state.filterText}]`,
|
|
778
|
+
type: "filter",
|
|
779
|
+
});
|
|
780
|
+
lines.push({ text: "─".repeat(36), type: "separator" });
|
|
781
|
+
}
|
|
677
782
|
|
|
678
|
-
//
|
|
783
|
+
// Build visible fields
|
|
679
784
|
state.visibleFields = buildVisibleFields();
|
|
680
785
|
|
|
786
|
+
// Clamp selectedIndex
|
|
787
|
+
if (state.selectedIndex >= state.visibleFields.length) {
|
|
788
|
+
state.selectedIndex = Math.max(0, state.visibleFields.length - 1);
|
|
789
|
+
}
|
|
790
|
+
|
|
681
791
|
for (let i = 0; i < state.visibleFields.length; i++) {
|
|
682
792
|
const field = state.visibleFields[i];
|
|
683
|
-
const
|
|
793
|
+
const isSelected = i === state.selectedIndex;
|
|
684
794
|
|
|
685
795
|
if (field.isSection) {
|
|
686
|
-
// Section header
|
|
687
796
|
const icon = field.expanded ? "▼" : ">";
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
},
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
// Section description
|
|
699
|
-
entries.push({
|
|
700
|
-
text: `${indent} // ${field.def.description}\n`,
|
|
701
|
-
properties: { type: "description", path: field.path },
|
|
797
|
+
const sel = isSelected && state.focusPanel === "tree" ? "▸" : " ";
|
|
798
|
+
lines.push({
|
|
799
|
+
text: `${sel}${icon} ${field.def.displayName}`,
|
|
800
|
+
type: "tree-section",
|
|
801
|
+
index: i,
|
|
802
|
+
path: field.path,
|
|
803
|
+
selected: isSelected,
|
|
702
804
|
});
|
|
703
805
|
} else {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
806
|
+
const sel = isSelected && state.focusPanel === "tree" ? "▸" : " ";
|
|
807
|
+
const name = field.def.key.length > 13 ? field.def.key.slice(0, 12) + "…" : field.def.key;
|
|
808
|
+
const colorStr = formatColorValue(field.value);
|
|
809
|
+
const valueStr = colorStr.length > 9 ? colorStr.slice(0, 8) + "…" : colorStr;
|
|
810
|
+
lines.push({
|
|
811
|
+
text: ` ${sel} ${name.padEnd(13)} ██ ${valueStr}`,
|
|
812
|
+
type: "tree-field",
|
|
813
|
+
index: i,
|
|
814
|
+
path: field.path,
|
|
815
|
+
selected: isSelected,
|
|
816
|
+
colorValue: field.value,
|
|
708
817
|
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
709
820
|
|
|
710
|
-
|
|
711
|
-
|
|
821
|
+
return lines;
|
|
822
|
+
}
|
|
712
823
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
824
|
+
// =============================================================================
|
|
825
|
+
// Picker Panel Builder (Right)
|
|
826
|
+
// =============================================================================
|
|
827
|
+
|
|
828
|
+
interface PickerLine {
|
|
829
|
+
text: string;
|
|
830
|
+
type: string;
|
|
831
|
+
namedRow?: number;
|
|
832
|
+
paletteRow?: number;
|
|
833
|
+
previewLineIdx?: number;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function buildPickerLines(): PickerLine[] {
|
|
837
|
+
const lines: PickerLine[] = [];
|
|
838
|
+
const field = state.visibleFields[state.selectedIndex];
|
|
839
|
+
|
|
840
|
+
if (!field || field.isSection) {
|
|
841
|
+
// Section selected - show section info
|
|
842
|
+
if (field) {
|
|
843
|
+
lines.push({ text: `${field.def.displayName}`, type: "picker-title" });
|
|
844
|
+
lines.push({ text: `"${field.def.description}"`, type: "picker-desc" });
|
|
845
|
+
} else {
|
|
846
|
+
lines.push({ text: "No field selected", type: "picker-title" });
|
|
847
|
+
}
|
|
848
|
+
lines.push({ text: "─".repeat(RIGHT_WIDTH - 2), type: "picker-separator" });
|
|
849
|
+
lines.push({ text: "Select a color field to edit", type: "picker-desc" });
|
|
850
|
+
return lines;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Field title
|
|
854
|
+
lines.push({ text: `${field.path} - ${field.def.displayName}`, type: "picker-title" });
|
|
855
|
+
lines.push({ text: `"${field.def.description}"`, type: "picker-desc" });
|
|
856
|
+
lines.push({ text: "─".repeat(RIGHT_WIDTH - 2), type: "picker-separator" });
|
|
857
|
+
|
|
858
|
+
// Hex / RGB value
|
|
859
|
+
const colorStr = formatColorValue(field.value);
|
|
860
|
+
const rgb = parseColorToRgb(field.value);
|
|
861
|
+
let valueLine = `Hex: ${colorStr}`;
|
|
862
|
+
if (rgb) {
|
|
863
|
+
valueLine += ` RGB: ${rgb[0]}, ${rgb[1]}, ${rgb[2]}`;
|
|
864
|
+
}
|
|
865
|
+
lines.push({ text: valueLine, type: "picker-hex" });
|
|
866
|
+
|
|
867
|
+
lines.push({ text: "", type: "picker-blank" });
|
|
868
|
+
|
|
869
|
+
// Named Colors section
|
|
870
|
+
lines.push({ text: "Named Colors:", type: "picker-label" });
|
|
871
|
+
for (let row = 0; row < NAMED_COLOR_GRID.length; row++) {
|
|
872
|
+
let rowText = "";
|
|
873
|
+
for (const item of NAMED_COLOR_GRID[row]) {
|
|
874
|
+
rowText += " " + item.display.padEnd(8);
|
|
875
|
+
}
|
|
876
|
+
lines.push({ text: rowText, type: "picker-named-row", namedRow: row });
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
lines.push({ text: "", type: "picker-blank" });
|
|
880
|
+
|
|
881
|
+
// Extended Color Palette section
|
|
882
|
+
lines.push({ text: "Color Palette:", type: "picker-label" });
|
|
883
|
+
const palette = getExtendedPalette();
|
|
884
|
+
for (let row = 0; row < PALETTE_ROWS; row++) {
|
|
885
|
+
let rowText = "";
|
|
886
|
+
for (let col = 0; col < PALETTE_COLS; col++) {
|
|
887
|
+
rowText += (col === 0 ? " " : " ") + "██";
|
|
888
|
+
}
|
|
889
|
+
lines.push({ text: rowText, type: "picker-palette-row", paletteRow: row });
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
lines.push({ text: "─".repeat(RIGHT_WIDTH - 2), type: "picker-separator" });
|
|
893
|
+
|
|
894
|
+
// Preview section
|
|
895
|
+
lines.push({ text: "Preview:", type: "picker-label" });
|
|
896
|
+
for (let i = 0; i < PREVIEW_LINES.length; i++) {
|
|
897
|
+
let lineText = " ";
|
|
898
|
+
for (const token of PREVIEW_LINES[i]) {
|
|
899
|
+
lineText += token.text;
|
|
900
|
+
}
|
|
901
|
+
lines.push({ text: lineText, type: "picker-preview-line", previewLineIdx: i });
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return lines;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// =============================================================================
|
|
908
|
+
// UI Building - Two Panel Merge
|
|
909
|
+
// =============================================================================
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Compute inline style for a left-panel tree entry
|
|
913
|
+
*/
|
|
914
|
+
function styleForLeftEntry(item: TreeLine | undefined): { style?: Partial<OverlayOptions>; inlineOverlays?: InlineOverlay[] } {
|
|
915
|
+
if (!item) return {};
|
|
916
|
+
const type = item.type;
|
|
917
|
+
if (type === "header") {
|
|
918
|
+
return { style: { fg: colors.header, bold: true } };
|
|
919
|
+
} else if (type === "separator") {
|
|
920
|
+
return { style: { fg: colors.divider } };
|
|
921
|
+
} else if (type === "filter") {
|
|
922
|
+
return { style: { fg: colors.filterText } };
|
|
923
|
+
} else if (type === "tree-section") {
|
|
924
|
+
return { style: { fg: colors.sectionHeader, bold: true } };
|
|
925
|
+
} else if (type === "tree-field") {
|
|
926
|
+
const inlines: InlineOverlay[] = [];
|
|
927
|
+
const text = " " + item.text; // matches leftText construction below
|
|
928
|
+
const paddedLen = getUtf8ByteLength(text.padEnd(LEFT_WIDTH));
|
|
929
|
+
const colorValue = item.colorValue;
|
|
930
|
+
const swatchIdx = colorValue !== undefined ? text.indexOf("██") : -1;
|
|
931
|
+
const rgb = colorValue !== undefined ? parseColorToRgb(colorValue) : null;
|
|
932
|
+
|
|
933
|
+
if (rgb && swatchIdx >= 0) {
|
|
934
|
+
const swatchStart = getUtf8ByteLength(text.substring(0, swatchIdx));
|
|
935
|
+
const swatchEnd = swatchStart + getUtf8ByteLength("██");
|
|
936
|
+
// Non-overlapping segments: fieldName | swatch | value
|
|
937
|
+
inlines.push({ start: 0, end: swatchStart, style: { fg: colors.fieldName } });
|
|
938
|
+
inlines.push({ start: swatchStart, end: swatchEnd, style: { fg: rgb, bg: rgb } });
|
|
939
|
+
const valueStart = swatchEnd + getUtf8ByteLength(" ");
|
|
940
|
+
if (valueStart < paddedLen) {
|
|
941
|
+
inlines.push({ start: valueStart, end: paddedLen, style: { fg: colors.customValue } });
|
|
942
|
+
}
|
|
943
|
+
return { inlineOverlays: inlines };
|
|
722
944
|
}
|
|
945
|
+
// No swatch — use entry-level style
|
|
946
|
+
return { style: { fg: colors.fieldName } };
|
|
947
|
+
}
|
|
948
|
+
return {};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Compute inline style for a right-panel picker entry
|
|
953
|
+
*/
|
|
954
|
+
function styleForRightEntry(item: PickerLine | undefined): { style?: Partial<OverlayOptions>; inlineOverlays?: InlineOverlay[] } {
|
|
955
|
+
if (!item) return {};
|
|
956
|
+
const type = item.type;
|
|
957
|
+
if (type === "picker-title") {
|
|
958
|
+
return { style: { fg: colors.header, bold: true } };
|
|
959
|
+
} else if (type === "picker-desc") {
|
|
960
|
+
return { style: { fg: colors.description } };
|
|
961
|
+
} else if (type === "picker-separator") {
|
|
962
|
+
return { style: { fg: colors.divider } };
|
|
963
|
+
} else if (type === "picker-hex") {
|
|
964
|
+
return { style: { fg: colors.pickerValue } };
|
|
965
|
+
} else if (type === "picker-label") {
|
|
966
|
+
return { style: { fg: colors.pickerLabel } };
|
|
967
|
+
} else if (type === "picker-named-row") {
|
|
968
|
+
const namedRow = item.namedRow!;
|
|
969
|
+
const gridRow = NAMED_COLOR_GRID[namedRow];
|
|
970
|
+
if (!gridRow) return {};
|
|
971
|
+
const inlines: InlineOverlay[] = [];
|
|
972
|
+
// Entry text is " " + item.text. Byte positions are relative to that.
|
|
973
|
+
const bytePos = getUtf8ByteLength(" "); // the prepended " "
|
|
974
|
+
let innerPos = 0;
|
|
975
|
+
for (let col = 0; col < gridRow.length; col++) {
|
|
976
|
+
const cellItem = gridRow[col];
|
|
977
|
+
const cellText = " " + cellItem.display.padEnd(8);
|
|
978
|
+
const cellLen = getUtf8ByteLength(cellText);
|
|
979
|
+
const cellStart = bytePos + getUtf8ByteLength(item.text.substring(0, innerPos));
|
|
980
|
+
const cellEnd = cellStart + cellLen;
|
|
981
|
+
if (cellItem.rgb) {
|
|
982
|
+
inlines.push({ start: cellStart, end: cellEnd, style: { fg: cellItem.rgb }, properties: { namedCol: col } });
|
|
983
|
+
} else {
|
|
984
|
+
inlines.push({ start: cellStart, end: cellEnd, style: { fg: colors.pickerLabel }, properties: { namedCol: col } });
|
|
985
|
+
}
|
|
986
|
+
innerPos += cellText.length;
|
|
987
|
+
}
|
|
988
|
+
return { inlineOverlays: inlines.length > 0 ? inlines : undefined };
|
|
989
|
+
} else if (type === "picker-palette-row") {
|
|
990
|
+
const paletteRow = item.paletteRow!;
|
|
991
|
+
const palette = getExtendedPalette();
|
|
992
|
+
const rowColors = palette[paletteRow];
|
|
993
|
+
if (!rowColors) return {};
|
|
994
|
+
const inlines: InlineOverlay[] = [];
|
|
995
|
+
// entry text = " " + item.text
|
|
996
|
+
// item.text = " ██ ██ ██..." (starts with " " then "██" pairs)
|
|
997
|
+
let bytePos = getUtf8ByteLength(" "); // prepended " "
|
|
998
|
+
let innerPos = 0;
|
|
999
|
+
for (let col = 0; col < PALETTE_COLS; col++) {
|
|
1000
|
+
const prefix = col === 0 ? " " : " ";
|
|
1001
|
+
innerPos += prefix.length;
|
|
1002
|
+
bytePos = getUtf8ByteLength(" ") + getUtf8ByteLength(item.text.substring(0, innerPos));
|
|
1003
|
+
const swatchLen = getUtf8ByteLength("██");
|
|
1004
|
+
const rgb = rowColors[col];
|
|
1005
|
+
inlines.push({ start: bytePos, end: bytePos + swatchLen, style: { fg: rgb, bg: rgb }, properties: { paletteCol: col } });
|
|
1006
|
+
innerPos += 2; // "██" is 2 JS chars
|
|
1007
|
+
}
|
|
1008
|
+
return { inlineOverlays: inlines.length > 0 ? inlines : undefined };
|
|
1009
|
+
} else if (type === "picker-preview-line") {
|
|
1010
|
+
const previewLineIdx = item.previewLineIdx!;
|
|
1011
|
+
const tokens = PREVIEW_LINES[previewLineIdx];
|
|
1012
|
+
if (!tokens) return {};
|
|
1013
|
+
const inlines: InlineOverlay[] = [];
|
|
1014
|
+
// entry text = " " + item.text
|
|
1015
|
+
// item.text = " " + token texts concatenated
|
|
1016
|
+
const editorBg = getNestedValue(state.themeData, "editor.bg") as ColorValue;
|
|
1017
|
+
const bgRgb = parseColorToRgb(editorBg);
|
|
1018
|
+
const entryText = " " + item.text;
|
|
1019
|
+
const entryLen = getUtf8ByteLength(entryText);
|
|
1020
|
+
const baseStyle: Partial<OverlayOptions> | undefined = bgRgb ? { bg: bgRgb } : undefined;
|
|
1021
|
+
|
|
1022
|
+
// Skip the leading " " + " " (from entry " " prefix + item.text leading " ")
|
|
1023
|
+
let charPos = 2; // " " prefix + " " in item.text
|
|
1024
|
+
let bytePos = getUtf8ByteLength(" ");
|
|
1025
|
+
for (const token of tokens) {
|
|
1026
|
+
const tokenLen = getUtf8ByteLength(token.text);
|
|
1027
|
+
if (token.syntaxType) {
|
|
1028
|
+
const syntaxPath = `syntax.${token.syntaxType}`;
|
|
1029
|
+
const syntaxColor = getNestedValue(state.themeData, syntaxPath) as ColorValue;
|
|
1030
|
+
const syntaxRgb = parseColorToRgb(syntaxColor);
|
|
1031
|
+
if (syntaxRgb) {
|
|
1032
|
+
inlines.push({ start: bytePos, end: bytePos + tokenLen, style: { fg: syntaxRgb } });
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
const fgColor = getNestedValue(state.themeData, "editor.fg") as ColorValue;
|
|
1036
|
+
const fgRgb = parseColorToRgb(fgColor);
|
|
1037
|
+
if (fgRgb) {
|
|
1038
|
+
inlines.push({ start: bytePos, end: bytePos + tokenLen, style: { fg: fgRgb } });
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
bytePos += tokenLen;
|
|
1042
|
+
}
|
|
1043
|
+
return { style: baseStyle, inlineOverlays: inlines.length > 0 ? inlines : undefined };
|
|
1044
|
+
}
|
|
1045
|
+
return {};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function buildDisplayEntries(): TextPropertyEntry[] {
|
|
1049
|
+
const entries: TextPropertyEntry[] = [];
|
|
1050
|
+
|
|
1051
|
+
const allLeftLines = buildTreeLines();
|
|
1052
|
+
const rightLines = buildPickerLines();
|
|
1053
|
+
|
|
1054
|
+
// Virtual scrolling: only show a viewport-sized slice of tree lines
|
|
1055
|
+
// Reserve 2 rows for status bar + hints, 1 for possible scroll indicator
|
|
1056
|
+
const treeVisibleRows = Math.max(8, state.viewportHeight - 2);
|
|
1057
|
+
|
|
1058
|
+
// Adjust scroll offset to keep selectedIndex visible
|
|
1059
|
+
// Find which line index in allLeftLines corresponds to selectedIndex
|
|
1060
|
+
let selectedLineIdx = -1;
|
|
1061
|
+
for (let i = 0; i < allLeftLines.length; i++) {
|
|
1062
|
+
if (allLeftLines[i].index === state.selectedIndex && allLeftLines[i].selected) {
|
|
1063
|
+
selectedLineIdx = i;
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (selectedLineIdx >= 0) {
|
|
1068
|
+
if (selectedLineIdx < state.treeScrollOffset) {
|
|
1069
|
+
state.treeScrollOffset = selectedLineIdx;
|
|
1070
|
+
}
|
|
1071
|
+
if (selectedLineIdx >= state.treeScrollOffset + treeVisibleRows) {
|
|
1072
|
+
state.treeScrollOffset = selectedLineIdx - treeVisibleRows + 1;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// Clamp scroll offset
|
|
1076
|
+
const maxOffset = Math.max(0, allLeftLines.length - treeVisibleRows);
|
|
1077
|
+
if (state.treeScrollOffset > maxOffset) state.treeScrollOffset = maxOffset;
|
|
1078
|
+
if (state.treeScrollOffset < 0) state.treeScrollOffset = 0;
|
|
1079
|
+
|
|
1080
|
+
const leftLines = allLeftLines.slice(state.treeScrollOffset, state.treeScrollOffset + treeVisibleRows);
|
|
1081
|
+
|
|
1082
|
+
// Add scroll indicators if tree is scrollable
|
|
1083
|
+
const canScrollUp = state.treeScrollOffset > 0;
|
|
1084
|
+
const canScrollDown = state.treeScrollOffset + treeVisibleRows < allLeftLines.length;
|
|
1085
|
+
|
|
1086
|
+
const maxRows = Math.max(leftLines.length, rightLines.length, 8);
|
|
1087
|
+
let byteOffset = 0;
|
|
1088
|
+
|
|
1089
|
+
for (let i = 0; i < maxRows; i++) {
|
|
1090
|
+
const leftItem = leftLines[i];
|
|
1091
|
+
const rightItem = rightLines[i];
|
|
723
1092
|
|
|
1093
|
+
// Left side (padded to LEFT_WIDTH)
|
|
1094
|
+
const leftText = leftItem ? (" " + leftItem.text) : "";
|
|
1095
|
+
const leftPadded = leftText.padEnd(LEFT_WIDTH);
|
|
1096
|
+
const leftStyle = styleForLeftEntry(leftItem);
|
|
724
1097
|
entries.push({
|
|
725
|
-
text:
|
|
726
|
-
properties: {
|
|
1098
|
+
text: leftPadded,
|
|
1099
|
+
properties: {
|
|
1100
|
+
type: leftItem?.type || "blank",
|
|
1101
|
+
index: leftItem?.index,
|
|
1102
|
+
path: leftItem?.path,
|
|
1103
|
+
selected: leftItem?.selected,
|
|
1104
|
+
colorValue: leftItem?.colorValue,
|
|
1105
|
+
},
|
|
1106
|
+
style: leftStyle.style,
|
|
1107
|
+
inlineOverlays: leftStyle.inlineOverlays,
|
|
1108
|
+
});
|
|
1109
|
+
byteOffset += getUtf8ByteLength(leftPadded);
|
|
1110
|
+
|
|
1111
|
+
// Divider
|
|
1112
|
+
const dividerText = "│";
|
|
1113
|
+
entries.push({ text: dividerText, properties: { type: "divider" }, style: { fg: colors.divider } });
|
|
1114
|
+
byteOffset += getUtf8ByteLength(dividerText);
|
|
1115
|
+
|
|
1116
|
+
// Right side
|
|
1117
|
+
const rightText = rightItem ? (" " + rightItem.text) : "";
|
|
1118
|
+
const rightStyle = styleForRightEntry(rightItem);
|
|
1119
|
+
entries.push({
|
|
1120
|
+
text: rightText,
|
|
1121
|
+
properties: {
|
|
1122
|
+
type: rightItem?.type || "blank",
|
|
1123
|
+
namedRow: rightItem?.namedRow,
|
|
1124
|
+
paletteRow: rightItem?.paletteRow,
|
|
1125
|
+
previewLineIdx: rightItem?.previewLineIdx,
|
|
1126
|
+
},
|
|
1127
|
+
style: rightStyle.style,
|
|
1128
|
+
inlineOverlays: rightStyle.inlineOverlays,
|
|
727
1129
|
});
|
|
1130
|
+
byteOffset += getUtf8ByteLength(rightText);
|
|
1131
|
+
|
|
1132
|
+
// Newline
|
|
1133
|
+
entries.push({ text: "\n", properties: { type: "newline" } });
|
|
1134
|
+
byteOffset += 1;
|
|
728
1135
|
}
|
|
729
1136
|
|
|
1137
|
+
// Status bar (full width)
|
|
1138
|
+
entries.push({
|
|
1139
|
+
text: "─".repeat(100) + "\n",
|
|
1140
|
+
properties: { type: "status-separator" },
|
|
1141
|
+
style: { fg: colors.divider },
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// Context-sensitive key hints
|
|
1145
|
+
let hints: string;
|
|
1146
|
+
const scrollHint = canScrollUp || canScrollDown
|
|
1147
|
+
? ` [${canScrollUp ? "▲" : " "}${canScrollDown ? "▼" : " "}]`
|
|
1148
|
+
: "";
|
|
1149
|
+
if (state.focusPanel === "tree") {
|
|
1150
|
+
hints = " ↑↓ Navigate Tab Switch Panel Enter Edit /Filter Ctrl+S Save Esc Close" + scrollHint;
|
|
1151
|
+
} else {
|
|
1152
|
+
hints = " ↑↓←→ Navigate Tab Switch Panel Enter Apply Esc Back to Tree" + scrollHint;
|
|
1153
|
+
}
|
|
1154
|
+
entries.push({
|
|
1155
|
+
text: hints + "\n",
|
|
1156
|
+
properties: { type: "status-bar" },
|
|
1157
|
+
style: { fg: colors.footer },
|
|
1158
|
+
});
|
|
1159
|
+
|
|
730
1160
|
return entries;
|
|
731
1161
|
}
|
|
732
1162
|
|
|
733
1163
|
/**
|
|
734
1164
|
* Helper to add a colored overlay (foreground color)
|
|
735
|
-
* addOverlay signature: (bufferId, namespace, start, end, r, g, b, underline, bold, italic, bg_r, bg_g, bg_b, extend_to_line_end)
|
|
736
1165
|
*/
|
|
737
1166
|
function addColorOverlay(
|
|
738
1167
|
bufferId: number,
|
|
@@ -753,7 +1182,7 @@ function addBackgroundHighlight(
|
|
|
753
1182
|
end: number,
|
|
754
1183
|
bgColor: RGB
|
|
755
1184
|
): void {
|
|
756
|
-
editor.addOverlay(bufferId, "theme-
|
|
1185
|
+
editor.addOverlay(bufferId, "theme-sel", start, end, { bg: bgColor });
|
|
757
1186
|
}
|
|
758
1187
|
|
|
759
1188
|
/**
|
|
@@ -773,96 +1202,110 @@ function isSpecialColor(value: ColorValue): boolean {
|
|
|
773
1202
|
}
|
|
774
1203
|
|
|
775
1204
|
/**
|
|
776
|
-
* Apply
|
|
1205
|
+
* Apply selection-only highlighting (cheap — just the selected row background
|
|
1206
|
+
* and picker focus highlights). Only touches the "theme-selection" namespace.
|
|
777
1207
|
*/
|
|
778
|
-
function
|
|
1208
|
+
function applySelectionHighlighting(cachedEntries?: TextPropertyEntry[]): void {
|
|
779
1209
|
if (state.bufferId === null) return;
|
|
780
1210
|
|
|
781
1211
|
const bufferId = state.bufferId;
|
|
782
|
-
editor.clearNamespace(bufferId, "theme");
|
|
783
|
-
editor.clearNamespace(bufferId, "theme-selection");
|
|
1212
|
+
editor.clearNamespace(bufferId, "theme-sel");
|
|
784
1213
|
|
|
785
|
-
const entries = buildDisplayEntries();
|
|
1214
|
+
const entries = cachedEntries || buildDisplayEntries();
|
|
786
1215
|
let byteOffset = 0;
|
|
787
1216
|
|
|
788
|
-
// Get current field at cursor to highlight it
|
|
789
|
-
const currentField = getFieldAtCursor();
|
|
790
|
-
const currentFieldPath = currentField?.path;
|
|
791
|
-
|
|
792
1217
|
for (const entry of entries) {
|
|
793
1218
|
const text = entry.text;
|
|
794
1219
|
const textLen = getUtf8ByteLength(text);
|
|
795
1220
|
const props = entry.properties as Record<string, unknown>;
|
|
796
1221
|
const entryType = props.type as string;
|
|
797
|
-
const entryPath = props.path as string | undefined;
|
|
798
|
-
|
|
799
|
-
// Add selection highlight for current field/section
|
|
800
|
-
if (currentFieldPath && entryPath === currentFieldPath && (entryType === "field" || entryType === "section")) {
|
|
801
|
-
addBackgroundHighlight(bufferId, byteOffset, byteOffset + textLen, colors.selectionBg);
|
|
802
|
-
}
|
|
803
1222
|
|
|
804
|
-
if (entryType === "
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1223
|
+
if (entryType === "tree-section" || entryType === "tree-field") {
|
|
1224
|
+
if (props.selected as boolean) {
|
|
1225
|
+
// For tree-field entries, split the highlight around the swatch (██) bytes
|
|
1226
|
+
// so the inline swatch color overlay (fg+bg) isn't overridden by selection bg
|
|
1227
|
+
const swatchIdx = text.indexOf("██");
|
|
1228
|
+
if (entryType === "tree-field" && swatchIdx >= 0) {
|
|
1229
|
+
const swatchByteStart = byteOffset + getUtf8ByteLength(text.substring(0, swatchIdx));
|
|
1230
|
+
const swatchByteEnd = swatchByteStart + getUtf8ByteLength("██");
|
|
1231
|
+
addBackgroundHighlight(bufferId, byteOffset, swatchByteStart, colors.selectionBg);
|
|
1232
|
+
addBackgroundHighlight(bufferId, swatchByteEnd, byteOffset + textLen, colors.selectionBg);
|
|
1233
|
+
} else {
|
|
1234
|
+
addBackgroundHighlight(bufferId, byteOffset, byteOffset + textLen, colors.selectionBg);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
} else if (entryType === "picker-hex") {
|
|
1238
|
+
if (state.focusPanel === "picker" && state.pickerFocus.type === "hex-input") {
|
|
1239
|
+
addBackgroundHighlight(bufferId, byteOffset, byteOffset + textLen, colors.pickerFocusBg);
|
|
1240
|
+
}
|
|
1241
|
+
} else if (entryType === "picker-named-row") {
|
|
1242
|
+
if (state.focusPanel === "picker" && state.pickerFocus.type === "named-colors") {
|
|
1243
|
+
const namedRow = props.namedRow as number;
|
|
1244
|
+
const gridRow = NAMED_COLOR_GRID[namedRow];
|
|
1245
|
+
if (gridRow) {
|
|
1246
|
+
let pos = byteOffset + getUtf8ByteLength(" ");
|
|
1247
|
+
for (let col = 0; col < gridRow.length; col++) {
|
|
1248
|
+
const item = gridRow[col];
|
|
1249
|
+
const cellText = " " + item.display.padEnd(8);
|
|
1250
|
+
const cellLen = getUtf8ByteLength(cellText);
|
|
1251
|
+
const flatIdx = namedRow * NAMED_COLORS_PER_ROW + col;
|
|
1252
|
+
if (state.pickerFocus.index === flatIdx) {
|
|
1253
|
+
editor.addOverlay(bufferId, "theme-sel", pos, pos + cellLen, { bg: colors.pickerFocusBg });
|
|
1254
|
+
}
|
|
1255
|
+
pos += cellLen;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
} else if (entryType === "picker-palette-row") {
|
|
1260
|
+
if (state.focusPanel === "picker" && state.pickerFocus.type === "palette") {
|
|
1261
|
+
const paletteRow = props.paletteRow as number;
|
|
1262
|
+
const palette = getExtendedPalette();
|
|
1263
|
+
const rowColors = palette[paletteRow];
|
|
1264
|
+
if (rowColors) {
|
|
1265
|
+
let pos = byteOffset + getUtf8ByteLength(" ");
|
|
1266
|
+
for (let col = 0; col < PALETTE_COLS; col++) {
|
|
1267
|
+
const prefix = col === 0 ? " " : " ";
|
|
1268
|
+
pos += getUtf8ByteLength(prefix);
|
|
1269
|
+
const swatchLen = getUtf8ByteLength("██");
|
|
1270
|
+
if (state.pickerFocus.row === paletteRow && state.pickerFocus.col === col) {
|
|
1271
|
+
editor.addOverlay(bufferId, "theme-sel", pos, pos + swatchLen, {
|
|
1272
|
+
bg: [255, 255, 255],
|
|
1273
|
+
fg: rowColors[col],
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
pos += swatchLen;
|
|
1277
|
+
}
|
|
834
1278
|
}
|
|
835
|
-
|
|
836
|
-
// Value (hex code) - custom color (green)
|
|
837
|
-
// Format: ": X #RRGGBB" - value starts after "X " (X + 2 spaces)
|
|
838
|
-
const valueStart = nameEnd + getUtf8ByteLength(": X ");
|
|
839
|
-
addColorOverlay(bufferId, valueStart, byteOffset + textLen, colors.customValue);
|
|
840
1279
|
}
|
|
841
|
-
} else if (entryType === "separator" || entryType === "footer") {
|
|
842
|
-
addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.footer);
|
|
843
1280
|
}
|
|
844
1281
|
|
|
845
1282
|
byteOffset += textLen;
|
|
846
1283
|
}
|
|
1284
|
+
|
|
847
1285
|
}
|
|
848
1286
|
|
|
1287
|
+
// Guard to suppress cursor_moved handler during programmatic updates
|
|
1288
|
+
let isUpdatingDisplay = false;
|
|
1289
|
+
|
|
849
1290
|
/**
|
|
850
|
-
*
|
|
1291
|
+
* Full display update — rebuilds content and all overlays.
|
|
1292
|
+
* Use for structural changes (open, section toggle, color edit, filter).
|
|
851
1293
|
*/
|
|
852
1294
|
function updateDisplay(): void {
|
|
853
1295
|
if (state.bufferId === null) return;
|
|
854
|
-
|
|
855
|
-
// Save current field path before updating
|
|
856
|
-
const currentPath = getCurrentFieldPath();
|
|
1296
|
+
isUpdatingDisplay = true;
|
|
857
1297
|
|
|
858
1298
|
const entries = buildDisplayEntries();
|
|
1299
|
+
|
|
1300
|
+
// Clear selection overlays BEFORE replacing content to prevent stale
|
|
1301
|
+
// theme-sel markers from having wrong positions after the buffer replace
|
|
1302
|
+
editor.clearNamespace(state.bufferId, "theme-sel");
|
|
1303
|
+
|
|
859
1304
|
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
860
|
-
applyHighlighting();
|
|
861
1305
|
|
|
862
|
-
//
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
1306
|
+
// Selection highlights use a separate namespace via addOverlay (dynamic, position-dependent)
|
|
1307
|
+
applySelectionHighlighting(entries);
|
|
1308
|
+
isUpdatingDisplay = false;
|
|
866
1309
|
}
|
|
867
1310
|
|
|
868
1311
|
// =============================================================================
|
|
@@ -870,19 +1313,12 @@ function updateDisplay(): void {
|
|
|
870
1313
|
// =============================================================================
|
|
871
1314
|
|
|
872
1315
|
/**
|
|
873
|
-
* Get field at cursor position
|
|
1316
|
+
* Get field at cursor position (uses state.selectedIndex)
|
|
874
1317
|
*/
|
|
875
1318
|
function getFieldAtCursor(): ThemeField | null {
|
|
876
|
-
if (state.
|
|
877
|
-
|
|
878
|
-
const props = editor.getTextPropertiesAtCursor(state.bufferId);
|
|
879
|
-
if (props.length > 0 && typeof props[0].index === "number") {
|
|
880
|
-
const index = props[0].index as number;
|
|
881
|
-
if (index >= 0 && index < state.visibleFields.length) {
|
|
882
|
-
return state.visibleFields[index];
|
|
883
|
-
}
|
|
1319
|
+
if (state.selectedIndex >= 0 && state.selectedIndex < state.visibleFields.length) {
|
|
1320
|
+
return state.visibleFields[state.selectedIndex];
|
|
884
1321
|
}
|
|
885
|
-
|
|
886
1322
|
return null;
|
|
887
1323
|
}
|
|
888
1324
|
|
|
@@ -1019,7 +1455,7 @@ function findMatchingColor(input: string): string | null {
|
|
|
1019
1455
|
/**
|
|
1020
1456
|
* Handle color prompt confirmation
|
|
1021
1457
|
*/
|
|
1022
|
-
|
|
1458
|
+
function onThemeColorPromptConfirmed(args: {
|
|
1023
1459
|
prompt_type: string;
|
|
1024
1460
|
selected_index: number | null;
|
|
1025
1461
|
input: string;
|
|
@@ -1040,7 +1476,7 @@ globalThis.onThemeColorPromptConfirmed = function(args: {
|
|
|
1040
1476
|
const entries = buildDisplayEntries();
|
|
1041
1477
|
if (state.bufferId !== null) {
|
|
1042
1478
|
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1043
|
-
|
|
1479
|
+
applySelectionHighlighting(entries);
|
|
1044
1480
|
}
|
|
1045
1481
|
moveCursorToField(path);
|
|
1046
1482
|
editor.setStatus(editor.t("status.updated", { path }));
|
|
@@ -1074,12 +1510,13 @@ globalThis.onThemeColorPromptConfirmed = function(args: {
|
|
|
1074
1510
|
}
|
|
1075
1511
|
|
|
1076
1512
|
return true;
|
|
1077
|
-
}
|
|
1513
|
+
}
|
|
1514
|
+
registerHandler("onThemeColorPromptConfirmed", onThemeColorPromptConfirmed);
|
|
1078
1515
|
|
|
1079
1516
|
/**
|
|
1080
1517
|
* Handle open theme prompt (both builtin and user themes)
|
|
1081
1518
|
*/
|
|
1082
|
-
|
|
1519
|
+
async function onThemeOpenPromptConfirmed(args: {
|
|
1083
1520
|
prompt_type: string;
|
|
1084
1521
|
selected_index: number | null;
|
|
1085
1522
|
input: string;
|
|
@@ -1103,45 +1540,29 @@ globalThis.onThemeOpenPromptConfirmed = async function(args: {
|
|
|
1103
1540
|
isBuiltin = state.builtinThemes.includes(value);
|
|
1104
1541
|
}
|
|
1105
1542
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
editor.setStatus(editor.t("status.opened_builtin", { name: themeName }));
|
|
1118
|
-
} else {
|
|
1119
|
-
editor.setStatus(editor.t("status.load_failed", { name: themeName }));
|
|
1120
|
-
}
|
|
1543
|
+
const themeData = loadThemeFile(themeName);
|
|
1544
|
+
if (themeData) {
|
|
1545
|
+
state.themeData = deepClone(themeData);
|
|
1546
|
+
state.originalThemeData = deepClone(themeData);
|
|
1547
|
+
state.themeName = themeName;
|
|
1548
|
+
state.themePath = null;
|
|
1549
|
+
state.isBuiltin = isBuiltin;
|
|
1550
|
+
state.hasChanges = false;
|
|
1551
|
+
updateDisplay();
|
|
1552
|
+
const statusKey = isBuiltin ? "status.opened_builtin" : "status.loaded";
|
|
1553
|
+
editor.setStatus(editor.t(statusKey, { name: themeName }));
|
|
1121
1554
|
} else {
|
|
1122
|
-
|
|
1123
|
-
const result = await loadUserThemeFile(themeName);
|
|
1124
|
-
if (result) {
|
|
1125
|
-
state.themeData = deepClone(result.data);
|
|
1126
|
-
state.originalThemeData = deepClone(result.data);
|
|
1127
|
-
state.themeName = themeName;
|
|
1128
|
-
state.themePath = result.path;
|
|
1129
|
-
state.isBuiltin = false;
|
|
1130
|
-
state.hasChanges = false;
|
|
1131
|
-
updateDisplay();
|
|
1132
|
-
editor.setStatus(editor.t("status.loaded", { name: themeName }));
|
|
1133
|
-
} else {
|
|
1134
|
-
editor.setStatus(editor.t("status.load_failed", { name: themeName }));
|
|
1135
|
-
}
|
|
1555
|
+
editor.setStatus(editor.t("status.load_failed", { name: themeName }));
|
|
1136
1556
|
}
|
|
1137
1557
|
|
|
1138
1558
|
return true;
|
|
1139
|
-
}
|
|
1559
|
+
}
|
|
1560
|
+
registerHandler("onThemeOpenPromptConfirmed", onThemeOpenPromptConfirmed);
|
|
1140
1561
|
|
|
1141
1562
|
/**
|
|
1142
1563
|
* Handle save as prompt
|
|
1143
1564
|
*/
|
|
1144
|
-
|
|
1565
|
+
async function onThemeSaveAsPromptConfirmed(args: {
|
|
1145
1566
|
prompt_type: string;
|
|
1146
1567
|
selected_index: number | null;
|
|
1147
1568
|
input: string;
|
|
@@ -1150,11 +1571,15 @@ globalThis.onThemeSaveAsPromptConfirmed = async function(args: {
|
|
|
1150
1571
|
|
|
1151
1572
|
const name = args.input.trim();
|
|
1152
1573
|
if (name) {
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1155
|
-
|
|
1574
|
+
// Reject names that match a built-in theme
|
|
1575
|
+
if (state.builtinThemes.includes(name)) {
|
|
1576
|
+
editor.setStatus(editor.t("error.name_is_builtin", { name }));
|
|
1577
|
+
theme_editor_save_as();
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1156
1580
|
|
|
1157
|
-
if
|
|
1581
|
+
// Check if a user theme file already exists with this name
|
|
1582
|
+
if (editor.themeFileExists(name)) {
|
|
1158
1583
|
// Store pending save name for overwrite confirmation
|
|
1159
1584
|
state.pendingSaveName = name;
|
|
1160
1585
|
editor.startPrompt(editor.t("prompt.overwrite_confirm", { name }), "theme-overwrite-confirm");
|
|
@@ -1176,26 +1601,30 @@ globalThis.onThemeSaveAsPromptConfirmed = async function(args: {
|
|
|
1176
1601
|
}
|
|
1177
1602
|
|
|
1178
1603
|
return true;
|
|
1179
|
-
}
|
|
1604
|
+
}
|
|
1605
|
+
registerHandler("onThemeSaveAsPromptConfirmed", onThemeSaveAsPromptConfirmed);
|
|
1180
1606
|
|
|
1181
1607
|
/**
|
|
1182
1608
|
* Handle prompt cancellation
|
|
1183
1609
|
*/
|
|
1184
|
-
|
|
1610
|
+
function onThemePromptCancelled(args: { prompt_type: string }) : boolean {
|
|
1185
1611
|
if (!args.prompt_type.startsWith("theme-")) return true;
|
|
1186
1612
|
|
|
1187
|
-
// Clear saved
|
|
1613
|
+
// Clear saved state on cancellation
|
|
1188
1614
|
state.savedCursorPath = null;
|
|
1189
1615
|
state.pendingSaveName = null;
|
|
1616
|
+
state.closeAfterSave = false;
|
|
1617
|
+
state.filterActive = false;
|
|
1190
1618
|
|
|
1191
|
-
editor.
|
|
1619
|
+
editor.debug(editor.t("status.cancelled"));
|
|
1192
1620
|
return true;
|
|
1193
|
-
}
|
|
1621
|
+
}
|
|
1622
|
+
registerHandler("onThemePromptCancelled", onThemePromptCancelled);
|
|
1194
1623
|
|
|
1195
1624
|
/**
|
|
1196
1625
|
* Handle initial theme selection prompt (when opening editor)
|
|
1197
1626
|
*/
|
|
1198
|
-
|
|
1627
|
+
async function onThemeSelectInitialPromptConfirmed(args: {
|
|
1199
1628
|
prompt_type: string;
|
|
1200
1629
|
selected_index: number | null;
|
|
1201
1630
|
input: string;
|
|
@@ -1224,46 +1653,19 @@ globalThis.onThemeSelectInitialPromptConfirmed = async function(args: {
|
|
|
1224
1653
|
isBuiltin = state.builtinThemes.includes(value);
|
|
1225
1654
|
}
|
|
1226
1655
|
|
|
1227
|
-
editor.
|
|
1656
|
+
editor.debug(editor.t("status.loading"));
|
|
1228
1657
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
state.isBuiltin = true;
|
|
1238
|
-
state.hasChanges = false;
|
|
1239
|
-
} else {
|
|
1240
|
-
// Fallback to default theme if load failed
|
|
1241
|
-
state.themeData = createDefaultTheme();
|
|
1242
|
-
state.originalThemeData = deepClone(state.themeData);
|
|
1243
|
-
state.themeName = themeName;
|
|
1244
|
-
state.themePath = null;
|
|
1245
|
-
state.isBuiltin = true;
|
|
1246
|
-
state.hasChanges = false;
|
|
1247
|
-
}
|
|
1658
|
+
const themeData = loadThemeFile(themeName);
|
|
1659
|
+
if (themeData) {
|
|
1660
|
+
state.themeData = deepClone(themeData);
|
|
1661
|
+
state.originalThemeData = deepClone(themeData);
|
|
1662
|
+
state.themeName = themeName;
|
|
1663
|
+
state.themePath = null;
|
|
1664
|
+
state.isBuiltin = isBuiltin;
|
|
1665
|
+
state.hasChanges = false;
|
|
1248
1666
|
} else {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (result) {
|
|
1252
|
-
state.themeData = deepClone(result.data);
|
|
1253
|
-
state.originalThemeData = deepClone(result.data);
|
|
1254
|
-
state.themeName = themeName;
|
|
1255
|
-
state.themePath = result.path;
|
|
1256
|
-
state.isBuiltin = false;
|
|
1257
|
-
state.hasChanges = false;
|
|
1258
|
-
} else {
|
|
1259
|
-
// Fallback to default theme if load failed
|
|
1260
|
-
state.themeData = createDefaultTheme();
|
|
1261
|
-
state.originalThemeData = deepClone(state.themeData);
|
|
1262
|
-
state.themeName = themeName;
|
|
1263
|
-
state.themePath = null;
|
|
1264
|
-
state.isBuiltin = false;
|
|
1265
|
-
state.hasChanges = false;
|
|
1266
|
-
}
|
|
1667
|
+
editor.setStatus(`Failed to load theme '${themeName}'`);
|
|
1668
|
+
return true;
|
|
1267
1669
|
}
|
|
1268
1670
|
|
|
1269
1671
|
// Now open the editor with loaded theme
|
|
@@ -1272,7 +1674,8 @@ globalThis.onThemeSelectInitialPromptConfirmed = async function(args: {
|
|
|
1272
1674
|
editor.debug(`[theme_editor] doOpenThemeEditor() completed`);
|
|
1273
1675
|
|
|
1274
1676
|
return true;
|
|
1275
|
-
}
|
|
1677
|
+
}
|
|
1678
|
+
registerHandler("onThemeSelectInitialPromptConfirmed", onThemeSelectInitialPromptConfirmed);
|
|
1276
1679
|
|
|
1277
1680
|
// Register prompt handlers
|
|
1278
1681
|
editor.on("prompt_confirmed", "onThemeSelectInitialPromptConfirmed");
|
|
@@ -1294,28 +1697,32 @@ editor.on("prompt_cancelled", "onThemePromptCancelled");
|
|
|
1294
1697
|
* @param restorePath - Optional field path to restore cursor to after save
|
|
1295
1698
|
*/
|
|
1296
1699
|
async function saveTheme(name?: string, restorePath?: string | null): Promise<boolean> {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
// Ensure themes directory exists
|
|
1301
|
-
if (!editor.fileExists(userThemesDir)) {
|
|
1302
|
-
try {
|
|
1303
|
-
// Create directory via shell command
|
|
1304
|
-
await editor.spawnProcess("mkdir", ["-p", userThemesDir]);
|
|
1305
|
-
} catch (e) {
|
|
1306
|
-
editor.setStatus(editor.t("status.mkdir_failed", { error: String(e) }));
|
|
1307
|
-
return false;
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
const themePath = editor.pathJoin(userThemesDir, `${themeName}.json`);
|
|
1700
|
+
// Normalize theme name: lowercase, replace underscores/spaces with hyphens
|
|
1701
|
+
// (must match Rust's normalize_theme_name so config name matches filename)
|
|
1702
|
+
const themeName = (name || state.themeName).toLowerCase().replace(/[_ ]/g, "-");
|
|
1312
1703
|
|
|
1313
1704
|
try {
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1705
|
+
// Build a complete theme object from all known fields.
|
|
1706
|
+
// This ensures we always write every field, even if state.themeData
|
|
1707
|
+
// is missing some (e.g. package theme that failed to load fully).
|
|
1708
|
+
const completeTheme: Record<string, unknown> = { name: themeName };
|
|
1709
|
+
const sections = getThemeSections();
|
|
1710
|
+
for (const section of sections) {
|
|
1711
|
+
const sectionData: Record<string, unknown> = {};
|
|
1712
|
+
for (const field of section.fields) {
|
|
1713
|
+
const path = `${section.name}.${field.key}`;
|
|
1714
|
+
const value = getNestedValue(state.themeData, path);
|
|
1715
|
+
if (value !== undefined) {
|
|
1716
|
+
sectionData[field.key] = value;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
completeTheme[section.name] = sectionData;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const content = JSON.stringify(completeTheme, null, 2);
|
|
1723
|
+
const savedPath = editor.saveThemeFile(themeName, content);
|
|
1317
1724
|
|
|
1318
|
-
state.themePath =
|
|
1725
|
+
state.themePath = savedPath;
|
|
1319
1726
|
state.themeName = themeName;
|
|
1320
1727
|
state.isBuiltin = false; // After saving, it's now a user theme
|
|
1321
1728
|
state.originalThemeData = deepClone(state.themeData);
|
|
@@ -1325,7 +1732,7 @@ async function saveTheme(name?: string, restorePath?: string | null): Promise<bo
|
|
|
1325
1732
|
const entries = buildDisplayEntries();
|
|
1326
1733
|
if (state.bufferId !== null) {
|
|
1327
1734
|
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1328
|
-
|
|
1735
|
+
applySelectionHighlighting(entries);
|
|
1329
1736
|
}
|
|
1330
1737
|
|
|
1331
1738
|
// Restore cursor position if provided
|
|
@@ -1333,12 +1740,18 @@ async function saveTheme(name?: string, restorePath?: string | null): Promise<bo
|
|
|
1333
1740
|
moveCursorToField(restorePath);
|
|
1334
1741
|
}
|
|
1335
1742
|
|
|
1336
|
-
// Reload themes
|
|
1337
|
-
editor.
|
|
1338
|
-
editor.applyTheme(themeName);
|
|
1743
|
+
// Reload themes and apply the new/updated theme atomically
|
|
1744
|
+
editor.reloadAndApplyTheme(themeName);
|
|
1339
1745
|
editor.setStatus(editor.t("status.saved_and_applied", { name: themeName }));
|
|
1746
|
+
|
|
1747
|
+
if (state.closeAfterSave) {
|
|
1748
|
+
state.closeAfterSave = false;
|
|
1749
|
+
doCloseEditor();
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1340
1752
|
return true;
|
|
1341
1753
|
} catch (e) {
|
|
1754
|
+
state.closeAfterSave = false;
|
|
1342
1755
|
editor.setStatus(editor.t("status.save_failed", { error: String(e) }));
|
|
1343
1756
|
return false;
|
|
1344
1757
|
}
|
|
@@ -1419,28 +1832,102 @@ function createDefaultTheme(): Record<string, unknown> {
|
|
|
1419
1832
|
// Cursor Movement Handler
|
|
1420
1833
|
// =============================================================================
|
|
1421
1834
|
|
|
1422
|
-
|
|
1835
|
+
function onThemeEditorCursorMoved(data: {
|
|
1423
1836
|
buffer_id: number;
|
|
1424
1837
|
cursor_id: number;
|
|
1425
1838
|
old_position: number;
|
|
1426
1839
|
new_position: number;
|
|
1840
|
+
text_properties?: Array<Record<string, any>>;
|
|
1427
1841
|
}): void {
|
|
1428
1842
|
if (state.bufferId === null || data.buffer_id !== state.bufferId) return;
|
|
1843
|
+
if (isUpdatingDisplay) return;
|
|
1429
1844
|
|
|
1430
|
-
|
|
1845
|
+
const props = data.text_properties || [];
|
|
1846
|
+
if (props.length === 0) return;
|
|
1431
1847
|
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1848
|
+
const entryType = props[0].type as string | undefined;
|
|
1849
|
+
|
|
1850
|
+
// Tree field/section click — update selection and refresh display
|
|
1851
|
+
if ((entryType === "tree-field" || entryType === "tree-section") && typeof props[0].index === "number") {
|
|
1852
|
+
const index = props[0].index as number;
|
|
1853
|
+
if (index >= 0 && index < state.visibleFields.length) {
|
|
1854
|
+
state.selectedIndex = index;
|
|
1855
|
+
state.focusPanel = "tree";
|
|
1856
|
+
// Click on section header always toggles expand/collapse
|
|
1857
|
+
if (entryType === "tree-section") {
|
|
1858
|
+
theme_editor_toggle_section();
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
updateDisplay();
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1435
1864
|
}
|
|
1436
|
-
|
|
1865
|
+
|
|
1866
|
+
// Picker named color click — find which column via inline overlay properties
|
|
1867
|
+
if (entryType === "picker-named-row" && typeof props[0].namedRow === "number") {
|
|
1868
|
+
const namedRow = props[0].namedRow as number;
|
|
1869
|
+
// Look for namedCol property from inline overlay
|
|
1870
|
+
const colProp = props.find(p => typeof p.namedCol === "number");
|
|
1871
|
+
const clickedCol = colProp ? (colProp.namedCol as number) : 0;
|
|
1872
|
+
state.focusPanel = "picker";
|
|
1873
|
+
state.pickerFocus = { type: "named-colors", index: namedRow * NAMED_COLORS_PER_ROW + clickedCol };
|
|
1874
|
+
applyPickerColor();
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// Picker palette click — find which column via inline overlay properties
|
|
1879
|
+
if (entryType === "picker-palette-row" && typeof props[0].paletteRow === "number") {
|
|
1880
|
+
const paletteRow = props[0].paletteRow as number;
|
|
1881
|
+
// Look for paletteCol property from inline overlay
|
|
1882
|
+
const colProp = props.find(p => typeof p.paletteCol === "number");
|
|
1883
|
+
const clickedCol = colProp ? (colProp.paletteCol as number) : 0;
|
|
1884
|
+
state.focusPanel = "picker";
|
|
1885
|
+
state.pickerFocus = { type: "palette", row: paletteRow, col: clickedCol };
|
|
1886
|
+
applyPickerColor();
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Picker hex click — open hex editing prompt
|
|
1891
|
+
if (entryType === "picker-hex") {
|
|
1892
|
+
state.focusPanel = "picker";
|
|
1893
|
+
state.pickerFocus = { type: "hex-input" };
|
|
1894
|
+
applyPickerColor();
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
applySelectionHighlighting();
|
|
1899
|
+
}
|
|
1900
|
+
registerHandler("onThemeEditorCursorMoved", onThemeEditorCursorMoved);
|
|
1437
1901
|
|
|
1438
1902
|
editor.on("cursor_moved", "onThemeEditorCursorMoved");
|
|
1439
1903
|
|
|
1904
|
+
function onThemeEditorResize(data: { width: number; height: number }): void {
|
|
1905
|
+
if (state.bufferId === null) return;
|
|
1906
|
+
state.viewportHeight = data.height;
|
|
1907
|
+
state.viewportWidth = data.width;
|
|
1908
|
+
updateDisplay();
|
|
1909
|
+
}
|
|
1910
|
+
registerHandler("onThemeEditorResize", onThemeEditorResize);
|
|
1911
|
+
editor.on("resize", "onThemeEditorResize");
|
|
1912
|
+
|
|
1913
|
+
function onThemeEditorMouseScroll(data: { buffer_id: number; delta: number; col: number; row: number }): void {
|
|
1914
|
+
if (state.bufferId === null || data.buffer_id !== state.bufferId) return;
|
|
1915
|
+
|
|
1916
|
+
// Only scroll the tree when mouse is over the left panel area (col < LEFT_WIDTH)
|
|
1917
|
+
if (data.col >= LEFT_WIDTH) return;
|
|
1918
|
+
|
|
1919
|
+
// delta > 0 = scroll down, delta < 0 = scroll up
|
|
1920
|
+
const scrollAmount = data.delta > 0 ? 3 : -3;
|
|
1921
|
+
state.treeScrollOffset = Math.max(0, state.treeScrollOffset + scrollAmount);
|
|
1922
|
+
updateDisplay();
|
|
1923
|
+
}
|
|
1924
|
+
registerHandler("onThemeEditorMouseScroll", onThemeEditorMouseScroll);
|
|
1925
|
+
editor.on("mouse_scroll", "onThemeEditorMouseScroll");
|
|
1926
|
+
|
|
1440
1927
|
/**
|
|
1441
1928
|
* Handle buffer_closed event to reset state when buffer is closed by any means
|
|
1442
1929
|
*/
|
|
1443
|
-
|
|
1930
|
+
function onThemeEditorBufferClosed(data: {
|
|
1444
1931
|
buffer_id: number;
|
|
1445
1932
|
}): void {
|
|
1446
1933
|
if (state.bufferId !== null && data.buffer_id === state.bufferId) {
|
|
@@ -1450,11 +1937,70 @@ globalThis.onThemeEditorBufferClosed = function(data: {
|
|
|
1450
1937
|
state.themeData = {};
|
|
1451
1938
|
state.originalThemeData = {};
|
|
1452
1939
|
state.hasChanges = false;
|
|
1940
|
+
state.focusPanel = "tree";
|
|
1941
|
+
state.pickerFocus = { type: "hex-input" };
|
|
1942
|
+
state.filterText = "";
|
|
1943
|
+
state.filterActive = false;
|
|
1944
|
+
state.selectedIndex = 0;
|
|
1945
|
+
state.treeScrollOffset = 0;
|
|
1453
1946
|
}
|
|
1454
|
-
}
|
|
1947
|
+
}
|
|
1948
|
+
registerHandler("onThemeEditorBufferClosed", onThemeEditorBufferClosed);
|
|
1455
1949
|
|
|
1456
1950
|
editor.on("buffer_closed", "onThemeEditorBufferClosed");
|
|
1457
1951
|
|
|
1952
|
+
/**
|
|
1953
|
+
* Handle theme_inspect_key hook: open the theme editor at a specific key
|
|
1954
|
+
*/
|
|
1955
|
+
async function onThemeInspectKey(data: {
|
|
1956
|
+
theme_name: string;
|
|
1957
|
+
key: string;
|
|
1958
|
+
}): Promise<void> {
|
|
1959
|
+
// If already open, focus and navigate to the key
|
|
1960
|
+
if (isThemeEditorOpen()) {
|
|
1961
|
+
if (state.bufferId !== null) {
|
|
1962
|
+
editor.showBuffer(state.bufferId);
|
|
1963
|
+
}
|
|
1964
|
+
const section = data.key.split(".")[0];
|
|
1965
|
+
if (!state.expandedSections.has(section)) {
|
|
1966
|
+
state.expandedSections.add(section);
|
|
1967
|
+
}
|
|
1968
|
+
moveCursorToField(data.key);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Save context
|
|
1973
|
+
state.sourceSplitId = editor.getActiveSplitId();
|
|
1974
|
+
state.sourceBufferId = editor.getActiveBufferId();
|
|
1975
|
+
state.builtinThemes = await loadBuiltinThemes();
|
|
1976
|
+
|
|
1977
|
+
// Auto-load the current theme (builtin or user)
|
|
1978
|
+
const isBuiltin = state.builtinThemes.includes(data.theme_name);
|
|
1979
|
+
const themeData = loadThemeFile(data.theme_name);
|
|
1980
|
+
if (themeData) {
|
|
1981
|
+
state.themeData = deepClone(themeData);
|
|
1982
|
+
state.originalThemeData = deepClone(themeData);
|
|
1983
|
+
state.themeName = data.theme_name;
|
|
1984
|
+
state.themePath = null;
|
|
1985
|
+
state.isBuiltin = isBuiltin;
|
|
1986
|
+
state.hasChanges = false;
|
|
1987
|
+
} else {
|
|
1988
|
+
editor.setStatus(`Failed to load theme '${data.theme_name}'`);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// Expand the target section
|
|
1993
|
+
const section = data.key.split(".")[0];
|
|
1994
|
+
state.expandedSections.add(section);
|
|
1995
|
+
|
|
1996
|
+
// Open editor and navigate
|
|
1997
|
+
await doOpenThemeEditor();
|
|
1998
|
+
moveCursorToField(data.key);
|
|
1999
|
+
}
|
|
2000
|
+
registerHandler("onThemeInspectKey", onThemeInspectKey);
|
|
2001
|
+
|
|
2002
|
+
editor.on("theme_inspect_key", "onThemeInspectKey");
|
|
2003
|
+
|
|
1458
2004
|
// =============================================================================
|
|
1459
2005
|
// Smart Navigation - Skip Non-Selectable Lines
|
|
1460
2006
|
// =============================================================================
|
|
@@ -1480,23 +2026,13 @@ function getSelectableEntries(): SelectableEntry[] {
|
|
|
1480
2026
|
const entryType = props.type as string;
|
|
1481
2027
|
const path = (props.path as string) || "";
|
|
1482
2028
|
|
|
1483
|
-
// Only fields and sections are selectable (they have index property)
|
|
1484
|
-
if ((entryType === "field" || entryType === "section") && typeof props.index === "number") {
|
|
1485
|
-
// For fields, calculate position at the color value (after "FieldName: X ")
|
|
1486
|
-
let valueByteOffset = byteOffset;
|
|
1487
|
-
if (entryType === "field") {
|
|
1488
|
-
const colonIdx = entry.text.indexOf(":");
|
|
1489
|
-
if (colonIdx >= 0) {
|
|
1490
|
-
// Position at the hex value, after ": X " (colon + space + X + 2 spaces = 5 chars)
|
|
1491
|
-
valueByteOffset = byteOffset + getUtf8ByteLength(entry.text.substring(0, colonIdx + 5));
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
|
|
2029
|
+
// Only tree fields and sections are selectable (they have index property)
|
|
2030
|
+
if ((entryType === "tree-field" || entryType === "tree-section") && typeof props.index === "number") {
|
|
1495
2031
|
selectableEntries.push({
|
|
1496
2032
|
byteOffset,
|
|
1497
|
-
valueByteOffset,
|
|
2033
|
+
valueByteOffset: byteOffset,
|
|
1498
2034
|
index: props.index as number,
|
|
1499
|
-
isSection: entryType === "section",
|
|
2035
|
+
isSection: entryType === "tree-section",
|
|
1500
2036
|
path,
|
|
1501
2037
|
});
|
|
1502
2038
|
}
|
|
@@ -1539,118 +2075,263 @@ function getCurrentFieldPath(): string | null {
|
|
|
1539
2075
|
function moveCursorToField(path: string): void {
|
|
1540
2076
|
if (state.bufferId === null) return;
|
|
1541
2077
|
|
|
1542
|
-
|
|
1543
|
-
for (
|
|
1544
|
-
if (
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
2078
|
+
// Find the field by path in visibleFields and update selectedIndex
|
|
2079
|
+
for (let i = 0; i < state.visibleFields.length; i++) {
|
|
2080
|
+
if (state.visibleFields[i].path === path) {
|
|
2081
|
+
state.selectedIndex = i;
|
|
2082
|
+
updateDisplay();
|
|
1548
2083
|
return;
|
|
1549
2084
|
}
|
|
1550
2085
|
}
|
|
1551
2086
|
}
|
|
1552
2087
|
|
|
1553
2088
|
/**
|
|
1554
|
-
* Navigate
|
|
2089
|
+
* Navigate up - context-dependent on focus panel
|
|
1555
2090
|
*/
|
|
1556
|
-
|
|
2091
|
+
function theme_editor_nav_down() : void {
|
|
1557
2092
|
if (state.bufferId === null) return;
|
|
1558
2093
|
|
|
1559
|
-
|
|
1560
|
-
|
|
2094
|
+
if (state.focusPanel === "tree") {
|
|
2095
|
+
if (state.selectedIndex < state.visibleFields.length - 1) {
|
|
2096
|
+
state.selectedIndex++;
|
|
2097
|
+
updateDisplay();
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
navigatePickerVertical(1);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
registerHandler("theme_editor_nav_down", theme_editor_nav_down);
|
|
1561
2104
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
2105
|
+
function theme_editor_nav_up() : void {
|
|
2106
|
+
if (state.bufferId === null) return;
|
|
2107
|
+
|
|
2108
|
+
if (state.focusPanel === "tree") {
|
|
2109
|
+
if (state.selectedIndex > 0) {
|
|
2110
|
+
state.selectedIndex--;
|
|
2111
|
+
updateDisplay();
|
|
1569
2112
|
}
|
|
2113
|
+
} else {
|
|
2114
|
+
navigatePickerVertical(-1);
|
|
1570
2115
|
}
|
|
2116
|
+
}
|
|
2117
|
+
registerHandler("theme_editor_nav_up", theme_editor_nav_up);
|
|
1571
2118
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2119
|
+
/**
|
|
2120
|
+
* Navigate left/right - for picker grid navigation
|
|
2121
|
+
*/
|
|
2122
|
+
function theme_editor_nav_left() : void {
|
|
2123
|
+
if (state.focusPanel === "picker") {
|
|
2124
|
+
navigatePickerHorizontal(-1);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
registerHandler("theme_editor_nav_left", theme_editor_nav_left);
|
|
2128
|
+
|
|
2129
|
+
function theme_editor_nav_right() : void {
|
|
2130
|
+
if (state.focusPanel === "picker") {
|
|
2131
|
+
navigatePickerHorizontal(1);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
registerHandler("theme_editor_nav_right", theme_editor_nav_right);
|
|
1575
2135
|
|
|
1576
2136
|
/**
|
|
1577
|
-
*
|
|
2137
|
+
* Tab - switch focus between panels
|
|
1578
2138
|
*/
|
|
1579
|
-
|
|
1580
|
-
if (state.
|
|
2139
|
+
function theme_editor_focus_tab() : void {
|
|
2140
|
+
if (state.focusPanel === "tree") {
|
|
2141
|
+
state.focusPanel = "picker";
|
|
2142
|
+
state.pickerFocus = { type: "hex-input" };
|
|
2143
|
+
} else {
|
|
2144
|
+
state.focusPanel = "tree";
|
|
2145
|
+
}
|
|
2146
|
+
updateDisplay();
|
|
2147
|
+
}
|
|
2148
|
+
registerHandler("theme_editor_focus_tab", theme_editor_focus_tab);
|
|
1581
2149
|
|
|
1582
|
-
|
|
1583
|
-
|
|
2150
|
+
/**
|
|
2151
|
+
* Shift-Tab - reverse switch focus
|
|
2152
|
+
*/
|
|
2153
|
+
function theme_editor_focus_shift_tab() : void {
|
|
2154
|
+
if (state.focusPanel === "picker") {
|
|
2155
|
+
state.focusPanel = "tree";
|
|
2156
|
+
} else {
|
|
2157
|
+
state.focusPanel = "picker";
|
|
2158
|
+
state.pickerFocus = { type: "hex-input" };
|
|
2159
|
+
}
|
|
2160
|
+
updateDisplay();
|
|
2161
|
+
}
|
|
2162
|
+
registerHandler("theme_editor_focus_shift_tab", theme_editor_focus_shift_tab);
|
|
1584
2163
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
2164
|
+
/**
|
|
2165
|
+
* Enter key - context-dependent action
|
|
2166
|
+
*/
|
|
2167
|
+
function theme_editor_enter() : void {
|
|
2168
|
+
if (state.focusPanel === "tree") {
|
|
2169
|
+
const field = getFieldAtCursor();
|
|
2170
|
+
if (!field) return;
|
|
2171
|
+
if (field.isSection) {
|
|
2172
|
+
theme_editor_toggle_section();
|
|
2173
|
+
} else {
|
|
2174
|
+
editColorField(field);
|
|
1593
2175
|
}
|
|
2176
|
+
} else {
|
|
2177
|
+
applyPickerColor();
|
|
1594
2178
|
}
|
|
2179
|
+
}
|
|
2180
|
+
registerHandler("theme_editor_enter", theme_editor_enter);
|
|
1595
2181
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2182
|
+
/**
|
|
2183
|
+
* Escape - context-dependent
|
|
2184
|
+
*/
|
|
2185
|
+
function theme_editor_escape() : void {
|
|
2186
|
+
if (state.focusPanel === "picker") {
|
|
2187
|
+
state.focusPanel = "tree";
|
|
2188
|
+
updateDisplay();
|
|
2189
|
+
} else if (state.filterText) {
|
|
2190
|
+
state.filterText = "";
|
|
2191
|
+
state.filterActive = false;
|
|
2192
|
+
updateDisplay();
|
|
2193
|
+
} else {
|
|
2194
|
+
theme_editor_close();
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
registerHandler("theme_editor_escape", theme_editor_escape);
|
|
1599
2198
|
|
|
1600
2199
|
/**
|
|
1601
|
-
*
|
|
2200
|
+
* / key - activate filter
|
|
1602
2201
|
*/
|
|
1603
|
-
|
|
1604
|
-
|
|
2202
|
+
function theme_editor_filter() : void {
|
|
2203
|
+
state.filterActive = true;
|
|
2204
|
+
editor.startPromptWithInitial(
|
|
2205
|
+
"Filter fields:",
|
|
2206
|
+
"theme-filter",
|
|
2207
|
+
state.filterText
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
registerHandler("theme_editor_filter", theme_editor_filter);
|
|
2211
|
+
|
|
2212
|
+
/**
|
|
2213
|
+
* Handle filter prompt confirmation
|
|
2214
|
+
*/
|
|
2215
|
+
function onThemeFilterPromptConfirmed(args: {
|
|
2216
|
+
prompt_type: string;
|
|
2217
|
+
selected_index: number | null;
|
|
2218
|
+
input: string;
|
|
2219
|
+
}): boolean {
|
|
2220
|
+
if (args.prompt_type !== "theme-filter") return true;
|
|
1605
2221
|
|
|
1606
|
-
|
|
1607
|
-
|
|
2222
|
+
state.filterText = args.input.trim();
|
|
2223
|
+
state.filterActive = false;
|
|
2224
|
+
state.selectedIndex = 0;
|
|
2225
|
+
state.treeScrollOffset = 0;
|
|
2226
|
+
updateDisplay();
|
|
2227
|
+
return true;
|
|
2228
|
+
}
|
|
2229
|
+
registerHandler("onThemeFilterPromptConfirmed", onThemeFilterPromptConfirmed);
|
|
1608
2230
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2231
|
+
editor.on("prompt_confirmed", "onThemeFilterPromptConfirmed");
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Navigate picker vertically (between sections: hex, named-colors, palette)
|
|
2235
|
+
*/
|
|
2236
|
+
function navigatePickerVertical(dir: number): void {
|
|
2237
|
+
const pf = state.pickerFocus;
|
|
2238
|
+
if (dir > 0) {
|
|
2239
|
+
if (pf.type === "hex-input") {
|
|
2240
|
+
state.pickerFocus = { type: "named-colors", index: 0 };
|
|
2241
|
+
} else if (pf.type === "named-colors") {
|
|
2242
|
+
const row = Math.floor(pf.index / NAMED_COLORS_PER_ROW);
|
|
2243
|
+
const col = pf.index % NAMED_COLORS_PER_ROW;
|
|
2244
|
+
if (row < NAMED_COLOR_GRID.length - 1) {
|
|
2245
|
+
state.pickerFocus = { type: "named-colors", index: (row + 1) * NAMED_COLORS_PER_ROW + col };
|
|
2246
|
+
} else {
|
|
2247
|
+
state.pickerFocus = { type: "palette", row: 0, col: Math.min(col, PALETTE_COLS - 1) };
|
|
2248
|
+
}
|
|
2249
|
+
} else if (pf.type === "palette") {
|
|
2250
|
+
if (pf.row < PALETTE_ROWS - 1) {
|
|
2251
|
+
state.pickerFocus = { type: "palette", row: pf.row + 1, col: pf.col };
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
} else {
|
|
2255
|
+
if (pf.type === "palette") {
|
|
2256
|
+
if (pf.row > 0) {
|
|
2257
|
+
state.pickerFocus = { type: "palette", row: pf.row - 1, col: pf.col };
|
|
2258
|
+
} else {
|
|
2259
|
+
const col = Math.min(pf.col, NAMED_COLORS_PER_ROW - 1);
|
|
2260
|
+
state.pickerFocus = { type: "named-colors", index: (NAMED_COLOR_GRID.length - 1) * NAMED_COLORS_PER_ROW + col };
|
|
2261
|
+
}
|
|
2262
|
+
} else if (pf.type === "named-colors") {
|
|
2263
|
+
const row = Math.floor(pf.index / NAMED_COLORS_PER_ROW);
|
|
2264
|
+
const col = pf.index % NAMED_COLORS_PER_ROW;
|
|
2265
|
+
if (row > 0) {
|
|
2266
|
+
state.pickerFocus = { type: "named-colors", index: (row - 1) * NAMED_COLORS_PER_ROW + col };
|
|
2267
|
+
} else {
|
|
2268
|
+
state.pickerFocus = { type: "hex-input" };
|
|
2269
|
+
}
|
|
1616
2270
|
}
|
|
1617
2271
|
}
|
|
2272
|
+
updateDisplay();
|
|
2273
|
+
}
|
|
1618
2274
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2275
|
+
/**
|
|
2276
|
+
* Navigate picker horizontally (within named-colors or palette grids)
|
|
2277
|
+
*/
|
|
2278
|
+
function navigatePickerHorizontal(dir: number): void {
|
|
2279
|
+
const pf = state.pickerFocus;
|
|
2280
|
+
if (pf.type === "named-colors") {
|
|
2281
|
+
const row = Math.floor(pf.index / NAMED_COLORS_PER_ROW);
|
|
2282
|
+
const col = pf.index % NAMED_COLORS_PER_ROW;
|
|
2283
|
+
const newCol = col + dir;
|
|
2284
|
+
if (newCol >= 0 && newCol < NAMED_COLORS_PER_ROW) {
|
|
2285
|
+
const totalItems = NAMED_COLOR_GRID.length * NAMED_COLORS_PER_ROW;
|
|
2286
|
+
const newIdx = row * NAMED_COLORS_PER_ROW + newCol;
|
|
2287
|
+
if (newIdx < totalItems) {
|
|
2288
|
+
state.pickerFocus = { type: "named-colors", index: newIdx };
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
} else if (pf.type === "palette") {
|
|
2292
|
+
const newCol = pf.col + dir;
|
|
2293
|
+
if (newCol >= 0 && newCol < PALETTE_COLS) {
|
|
2294
|
+
state.pickerFocus = { type: "palette", row: pf.row, col: newCol };
|
|
2295
|
+
}
|
|
1624
2296
|
}
|
|
1625
|
-
|
|
2297
|
+
updateDisplay();
|
|
2298
|
+
}
|
|
1626
2299
|
|
|
1627
2300
|
/**
|
|
1628
|
-
*
|
|
2301
|
+
* Apply the currently focused picker color to the selected field
|
|
1629
2302
|
*/
|
|
1630
|
-
|
|
1631
|
-
|
|
2303
|
+
function applyPickerColor(): void {
|
|
2304
|
+
const field = getFieldAtCursor();
|
|
2305
|
+
if (!field || field.isSection) return;
|
|
1632
2306
|
|
|
1633
|
-
const
|
|
1634
|
-
|
|
2307
|
+
const pf = state.pickerFocus;
|
|
2308
|
+
let newColor: ColorValue | null = null;
|
|
1635
2309
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2310
|
+
if (pf.type === "hex-input") {
|
|
2311
|
+
editColorField(field);
|
|
2312
|
+
return;
|
|
2313
|
+
} else if (pf.type === "named-colors") {
|
|
2314
|
+
const row = Math.floor(pf.index / NAMED_COLORS_PER_ROW);
|
|
2315
|
+
const col = pf.index % NAMED_COLORS_PER_ROW;
|
|
2316
|
+
if (row < NAMED_COLOR_GRID.length && col < NAMED_COLOR_GRID[row].length) {
|
|
2317
|
+
const item = NAMED_COLOR_GRID[row][col];
|
|
2318
|
+
newColor = item.value;
|
|
2319
|
+
}
|
|
2320
|
+
} else if (pf.type === "palette") {
|
|
2321
|
+
const palette = getExtendedPalette();
|
|
2322
|
+
if (pf.row < PALETTE_ROWS && pf.col < PALETTE_COLS) {
|
|
2323
|
+
newColor = palette[pf.row][pf.col];
|
|
1644
2324
|
}
|
|
1645
2325
|
}
|
|
1646
2326
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
editor.
|
|
2327
|
+
if (newColor !== null) {
|
|
2328
|
+
setNestedValue(state.themeData, field.path, newColor);
|
|
2329
|
+
state.hasChanges = !deepEqual(state.themeData, state.originalThemeData);
|
|
2330
|
+
updateDisplay();
|
|
2331
|
+
editor.setStatus(editor.t("status.updated", { path: field.path }));
|
|
1652
2332
|
}
|
|
1653
|
-
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
1654
2335
|
|
|
1655
2336
|
// =============================================================================
|
|
1656
2337
|
// Public Commands
|
|
@@ -1659,7 +2340,7 @@ globalThis.theme_editor_nav_prev_section = function(): void {
|
|
|
1659
2340
|
/**
|
|
1660
2341
|
* Open the theme editor - prompts user to select theme first
|
|
1661
2342
|
*/
|
|
1662
|
-
|
|
2343
|
+
async function open_theme_editor() : Promise<void> {
|
|
1663
2344
|
editor.debug("[theme_editor] open_theme_editor called");
|
|
1664
2345
|
if (isThemeEditorOpen()) {
|
|
1665
2346
|
editor.debug("[theme_editor] already open, focusing");
|
|
@@ -1667,7 +2348,7 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1667
2348
|
if (state.splitId !== null) {
|
|
1668
2349
|
editor.focusSplit(state.splitId);
|
|
1669
2350
|
}
|
|
1670
|
-
editor.
|
|
2351
|
+
editor.debug(editor.t("status.already_open"));
|
|
1671
2352
|
return;
|
|
1672
2353
|
}
|
|
1673
2354
|
|
|
@@ -1690,15 +2371,23 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1690
2371
|
|
|
1691
2372
|
const suggestions: PromptSuggestion[] = [];
|
|
1692
2373
|
|
|
1693
|
-
// Add user themes first
|
|
1694
|
-
const
|
|
1695
|
-
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
2374
|
+
// Add user themes first (from themes directory)
|
|
2375
|
+
const userThemesDir = editor.getThemesDir();
|
|
2376
|
+
try {
|
|
2377
|
+
const entries = editor.readDir(userThemesDir);
|
|
2378
|
+
for (const e of entries) {
|
|
2379
|
+
if (e.is_file && e.name.endsWith(".json")) {
|
|
2380
|
+
const name = e.name.replace(".json", "");
|
|
2381
|
+
const isCurrent = name === currentThemeName;
|
|
2382
|
+
suggestions.push({
|
|
2383
|
+
text: name,
|
|
2384
|
+
description: isCurrent ? editor.t("suggestion.user_theme_current") : editor.t("suggestion.user_theme"),
|
|
2385
|
+
value: `user:${name}`,
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
} catch {
|
|
2390
|
+
// No user themes directory
|
|
1702
2391
|
}
|
|
1703
2392
|
|
|
1704
2393
|
// Add built-in themes
|
|
@@ -1713,20 +2402,29 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1713
2402
|
|
|
1714
2403
|
// Sort suggestions to put current theme first
|
|
1715
2404
|
suggestions.sort((a, b) => {
|
|
1716
|
-
const aIsCurrent = a.description.includes("current");
|
|
1717
|
-
const bIsCurrent = b.description.includes("current");
|
|
2405
|
+
const aIsCurrent = (a.description ?? "").includes("current");
|
|
2406
|
+
const bIsCurrent = (b.description ?? "").includes("current");
|
|
1718
2407
|
if (aIsCurrent && !bIsCurrent) return -1;
|
|
1719
2408
|
if (!aIsCurrent && bIsCurrent) return 1;
|
|
1720
2409
|
return 0;
|
|
1721
2410
|
});
|
|
1722
2411
|
|
|
1723
2412
|
editor.setPromptSuggestions(suggestions);
|
|
1724
|
-
}
|
|
2413
|
+
}
|
|
2414
|
+
registerHandler("open_theme_editor", open_theme_editor);
|
|
1725
2415
|
|
|
1726
2416
|
/**
|
|
1727
2417
|
* Actually open the theme editor with loaded theme data
|
|
1728
2418
|
*/
|
|
1729
2419
|
async function doOpenThemeEditor(): Promise<void> {
|
|
2420
|
+
// Initialize viewport dimensions
|
|
2421
|
+
const vp = editor.getViewport();
|
|
2422
|
+
if (vp) {
|
|
2423
|
+
state.viewportHeight = vp.height;
|
|
2424
|
+
state.viewportWidth = vp.width;
|
|
2425
|
+
}
|
|
2426
|
+
state.treeScrollOffset = 0;
|
|
2427
|
+
|
|
1730
2428
|
editor.debug("[theme_editor] doOpenThemeEditor: building display entries");
|
|
1731
2429
|
// Build initial entries
|
|
1732
2430
|
const entries = buildDisplayEntries();
|
|
@@ -1740,7 +2438,7 @@ async function doOpenThemeEditor(): Promise<void> {
|
|
|
1740
2438
|
readOnly: true,
|
|
1741
2439
|
entries: entries,
|
|
1742
2440
|
showLineNumbers: false,
|
|
1743
|
-
showCursors:
|
|
2441
|
+
showCursors: false,
|
|
1744
2442
|
editingDisabled: true,
|
|
1745
2443
|
});
|
|
1746
2444
|
const bufferId = result.bufferId;
|
|
@@ -1752,11 +2450,14 @@ async function doOpenThemeEditor(): Promise<void> {
|
|
|
1752
2450
|
state.bufferId = bufferId;
|
|
1753
2451
|
state.splitId = null;
|
|
1754
2452
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
2453
|
+
// Disable line wrapping — our layout is fixed-width
|
|
2454
|
+
editor.setLineWrap(bufferId, null, false);
|
|
2455
|
+
|
|
2456
|
+
editor.debug(`[theme_editor] doOpenThemeEditor: calling applySelectionHighlighting...`);
|
|
2457
|
+
applySelectionHighlighting();
|
|
2458
|
+
editor.debug(`[theme_editor] doOpenThemeEditor: applySelectionHighlighting completed`);
|
|
1758
2459
|
editor.debug(`[theme_editor] doOpenThemeEditor: calling setStatus...`);
|
|
1759
|
-
editor.
|
|
2460
|
+
editor.debug(editor.t("status.ready"));
|
|
1760
2461
|
editor.debug(`[theme_editor] doOpenThemeEditor: completed successfully`);
|
|
1761
2462
|
} else {
|
|
1762
2463
|
editor.setStatus(editor.t("status.open_failed"));
|
|
@@ -1766,22 +2467,24 @@ async function doOpenThemeEditor(): Promise<void> {
|
|
|
1766
2467
|
/**
|
|
1767
2468
|
* Close the theme editor
|
|
1768
2469
|
*/
|
|
1769
|
-
|
|
2470
|
+
function theme_editor_close() : void {
|
|
1770
2471
|
if (!isThemeEditorOpen()) return;
|
|
1771
2472
|
|
|
1772
2473
|
if (state.hasChanges) {
|
|
1773
2474
|
// Show confirmation prompt before closing with unsaved changes
|
|
1774
2475
|
editor.startPrompt(editor.t("prompt.discard_confirm"), "theme-discard-confirm");
|
|
1775
2476
|
const suggestions: PromptSuggestion[] = [
|
|
1776
|
-
{ text: editor.t("prompt.discard_yes"), description: "", value: "discard" },
|
|
1777
2477
|
{ text: editor.t("prompt.discard_no"), description: "", value: "keep" },
|
|
2478
|
+
{ text: editor.t("prompt.discard_save"), description: "", value: "save" },
|
|
2479
|
+
{ text: editor.t("prompt.discard_yes"), description: "", value: "discard" },
|
|
1778
2480
|
];
|
|
1779
2481
|
editor.setPromptSuggestions(suggestions);
|
|
1780
2482
|
return;
|
|
1781
2483
|
}
|
|
1782
2484
|
|
|
1783
2485
|
doCloseEditor();
|
|
1784
|
-
}
|
|
2486
|
+
}
|
|
2487
|
+
registerHandler("theme_editor_close", theme_editor_close);
|
|
1785
2488
|
|
|
1786
2489
|
/**
|
|
1787
2490
|
* Actually close the editor (called after confirmation or when no changes)
|
|
@@ -1798,14 +2501,19 @@ function doCloseEditor(): void {
|
|
|
1798
2501
|
state.themeData = {};
|
|
1799
2502
|
state.originalThemeData = {};
|
|
1800
2503
|
state.hasChanges = false;
|
|
2504
|
+
state.focusPanel = "tree";
|
|
2505
|
+
state.pickerFocus = { type: "hex-input" };
|
|
2506
|
+
state.filterText = "";
|
|
2507
|
+
state.filterActive = false;
|
|
2508
|
+
state.selectedIndex = 0;
|
|
1801
2509
|
|
|
1802
|
-
editor.
|
|
2510
|
+
editor.debug(editor.t("status.closed"));
|
|
1803
2511
|
}
|
|
1804
2512
|
|
|
1805
2513
|
/**
|
|
1806
2514
|
* Handle discard confirmation prompt
|
|
1807
2515
|
*/
|
|
1808
|
-
|
|
2516
|
+
function onThemeDiscardPromptConfirmed(args: {
|
|
1809
2517
|
prompt_type: string;
|
|
1810
2518
|
selected_index: number | null;
|
|
1811
2519
|
input: string;
|
|
@@ -1813,41 +2521,46 @@ globalThis.onThemeDiscardPromptConfirmed = function(args: {
|
|
|
1813
2521
|
if (args.prompt_type !== "theme-discard-confirm") return true;
|
|
1814
2522
|
|
|
1815
2523
|
const response = args.input.trim().toLowerCase();
|
|
1816
|
-
if (response === "discard" || args.selected_index ===
|
|
2524
|
+
if (response === "discard" || args.selected_index === 2) {
|
|
1817
2525
|
editor.setStatus(editor.t("status.unsaved_discarded"));
|
|
1818
2526
|
doCloseEditor();
|
|
2527
|
+
} else if (response === "save" || args.selected_index === 1) {
|
|
2528
|
+
state.closeAfterSave = true;
|
|
2529
|
+
theme_editor_save();
|
|
1819
2530
|
} else {
|
|
1820
|
-
editor.
|
|
2531
|
+
editor.debug(editor.t("status.cancelled"));
|
|
1821
2532
|
}
|
|
1822
2533
|
|
|
1823
2534
|
return false;
|
|
1824
|
-
}
|
|
2535
|
+
}
|
|
2536
|
+
registerHandler("onThemeDiscardPromptConfirmed", onThemeDiscardPromptConfirmed);
|
|
1825
2537
|
|
|
1826
2538
|
/**
|
|
1827
2539
|
* Edit color at cursor
|
|
1828
2540
|
*/
|
|
1829
|
-
|
|
2541
|
+
function theme_editor_edit_color() : void {
|
|
1830
2542
|
const field = getFieldAtCursor();
|
|
1831
2543
|
if (!field) {
|
|
1832
|
-
editor.
|
|
2544
|
+
editor.debug(editor.t("status.no_field"));
|
|
1833
2545
|
return;
|
|
1834
2546
|
}
|
|
1835
2547
|
|
|
1836
2548
|
if (field.isSection) {
|
|
1837
|
-
|
|
2549
|
+
theme_editor_toggle_section();
|
|
1838
2550
|
return;
|
|
1839
2551
|
}
|
|
1840
2552
|
|
|
1841
2553
|
editColorField(field);
|
|
1842
|
-
}
|
|
2554
|
+
}
|
|
2555
|
+
registerHandler("theme_editor_edit_color", theme_editor_edit_color);
|
|
1843
2556
|
|
|
1844
2557
|
/**
|
|
1845
2558
|
* Toggle section expansion
|
|
1846
2559
|
*/
|
|
1847
|
-
|
|
2560
|
+
function theme_editor_toggle_section() : void {
|
|
1848
2561
|
const field = getFieldAtCursor();
|
|
1849
2562
|
if (!field || !field.isSection) {
|
|
1850
|
-
editor.
|
|
2563
|
+
editor.debug(editor.t("status.not_section"));
|
|
1851
2564
|
return;
|
|
1852
2565
|
}
|
|
1853
2566
|
|
|
@@ -1858,24 +2571,33 @@ globalThis.theme_editor_toggle_section = function(): void {
|
|
|
1858
2571
|
}
|
|
1859
2572
|
|
|
1860
2573
|
updateDisplay();
|
|
1861
|
-
}
|
|
2574
|
+
}
|
|
2575
|
+
registerHandler("theme_editor_toggle_section", theme_editor_toggle_section);
|
|
1862
2576
|
|
|
1863
2577
|
/**
|
|
1864
2578
|
* Open a theme (builtin or user) for editing
|
|
1865
2579
|
*/
|
|
1866
|
-
|
|
2580
|
+
function theme_editor_open() : void {
|
|
1867
2581
|
editor.startPrompt(editor.t("prompt.open_theme"), "theme-open");
|
|
1868
2582
|
|
|
1869
2583
|
const suggestions: PromptSuggestion[] = [];
|
|
1870
2584
|
|
|
1871
|
-
// Add user themes first
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2585
|
+
// Add user themes first (from themes directory)
|
|
2586
|
+
const userThemesDir = editor.getThemesDir();
|
|
2587
|
+
try {
|
|
2588
|
+
const entries = editor.readDir(userThemesDir);
|
|
2589
|
+
for (const e of entries) {
|
|
2590
|
+
if (e.is_file && e.name.endsWith(".json")) {
|
|
2591
|
+
const name = e.name.replace(".json", "");
|
|
2592
|
+
suggestions.push({
|
|
2593
|
+
text: name,
|
|
2594
|
+
description: editor.t("suggestion.user_theme"),
|
|
2595
|
+
value: `user:${name}`,
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
} catch {
|
|
2600
|
+
// No user themes directory
|
|
1879
2601
|
}
|
|
1880
2602
|
|
|
1881
2603
|
// Add built-in themes
|
|
@@ -1888,38 +2610,38 @@ globalThis.theme_editor_open = function(): void {
|
|
|
1888
2610
|
}
|
|
1889
2611
|
|
|
1890
2612
|
editor.setPromptSuggestions(suggestions);
|
|
1891
|
-
}
|
|
2613
|
+
}
|
|
2614
|
+
registerHandler("theme_editor_open", theme_editor_open);
|
|
1892
2615
|
|
|
1893
2616
|
/**
|
|
1894
2617
|
* Save theme
|
|
1895
2618
|
*/
|
|
1896
|
-
|
|
2619
|
+
async function theme_editor_save() : Promise<void> {
|
|
1897
2620
|
// Save cursor path for restoration after save
|
|
1898
2621
|
state.savedCursorPath = getCurrentFieldPath();
|
|
1899
2622
|
|
|
1900
2623
|
// Built-in themes require Save As
|
|
1901
2624
|
if (state.isBuiltin) {
|
|
1902
2625
|
editor.setStatus(editor.t("status.builtin_requires_save_as"));
|
|
1903
|
-
|
|
2626
|
+
theme_editor_save_as();
|
|
1904
2627
|
return;
|
|
1905
2628
|
}
|
|
1906
2629
|
|
|
1907
2630
|
// If theme has never been saved (no path), trigger "Save As" instead
|
|
1908
2631
|
if (!state.themePath) {
|
|
1909
|
-
|
|
2632
|
+
theme_editor_save_as();
|
|
1910
2633
|
return;
|
|
1911
2634
|
}
|
|
1912
2635
|
|
|
1913
2636
|
if (!state.hasChanges) {
|
|
1914
|
-
editor.
|
|
2637
|
+
editor.debug(editor.t("status.no_changes"));
|
|
1915
2638
|
return;
|
|
1916
2639
|
}
|
|
1917
2640
|
|
|
1918
2641
|
// Check for name collision if name has changed since last save
|
|
1919
|
-
const
|
|
1920
|
-
const targetPath = editor.pathJoin(userThemesDir, `${state.themeName}.json`);
|
|
2642
|
+
const expectedPath = editor.pathJoin(editor.getThemesDir(), `${state.themeName}.json`);
|
|
1921
2643
|
|
|
1922
|
-
if (state.themePath !==
|
|
2644
|
+
if (state.themePath !== expectedPath && editor.themeFileExists(state.themeName)) {
|
|
1923
2645
|
// File exists with this name - ask for confirmation
|
|
1924
2646
|
editor.startPrompt(editor.t("prompt.overwrite_confirm", { name: state.themeName }), "theme-overwrite-confirm");
|
|
1925
2647
|
const suggestions: PromptSuggestion[] = [
|
|
@@ -1931,12 +2653,13 @@ globalThis.theme_editor_save = async function(): Promise<void> {
|
|
|
1931
2653
|
}
|
|
1932
2654
|
|
|
1933
2655
|
await saveTheme(undefined, state.savedCursorPath);
|
|
1934
|
-
}
|
|
2656
|
+
}
|
|
2657
|
+
registerHandler("theme_editor_save", theme_editor_save);
|
|
1935
2658
|
|
|
1936
2659
|
/**
|
|
1937
2660
|
* Handle overwrite confirmation prompt
|
|
1938
2661
|
*/
|
|
1939
|
-
|
|
2662
|
+
async function onThemeOverwritePromptConfirmed(args: {
|
|
1940
2663
|
prompt_type: string;
|
|
1941
2664
|
selected_index: number | null;
|
|
1942
2665
|
input: string;
|
|
@@ -1956,16 +2679,18 @@ globalThis.onThemeOverwritePromptConfirmed = async function(args: {
|
|
|
1956
2679
|
} else {
|
|
1957
2680
|
state.pendingSaveName = null;
|
|
1958
2681
|
state.savedCursorPath = null;
|
|
1959
|
-
|
|
2682
|
+
state.closeAfterSave = false;
|
|
2683
|
+
editor.debug(editor.t("status.cancelled"));
|
|
1960
2684
|
}
|
|
1961
2685
|
|
|
1962
2686
|
return false;
|
|
1963
|
-
}
|
|
2687
|
+
}
|
|
2688
|
+
registerHandler("onThemeOverwritePromptConfirmed", onThemeOverwritePromptConfirmed);
|
|
1964
2689
|
|
|
1965
2690
|
/**
|
|
1966
2691
|
* Save theme as (new name)
|
|
1967
2692
|
*/
|
|
1968
|
-
|
|
2693
|
+
function theme_editor_save_as() : void {
|
|
1969
2694
|
// Save cursor path for restoration after save (if not already saved by theme_editor_save)
|
|
1970
2695
|
if (!state.savedCursorPath) {
|
|
1971
2696
|
state.savedCursorPath = getCurrentFieldPath();
|
|
@@ -1978,15 +2703,16 @@ globalThis.theme_editor_save_as = function(): void {
|
|
|
1978
2703
|
description: editor.t("suggestion.current"),
|
|
1979
2704
|
value: state.themeName,
|
|
1980
2705
|
}]);
|
|
1981
|
-
}
|
|
2706
|
+
}
|
|
2707
|
+
registerHandler("theme_editor_save_as", theme_editor_save_as);
|
|
1982
2708
|
|
|
1983
2709
|
/**
|
|
1984
2710
|
* Reload theme
|
|
1985
2711
|
*/
|
|
1986
|
-
|
|
2712
|
+
async function theme_editor_reload() : Promise<void> {
|
|
1987
2713
|
if (state.themePath) {
|
|
1988
2714
|
const themeName = state.themeName;
|
|
1989
|
-
const themeData =
|
|
2715
|
+
const themeData = loadThemeFile(themeName);
|
|
1990
2716
|
if (themeData) {
|
|
1991
2717
|
state.themeData = deepClone(themeData);
|
|
1992
2718
|
state.originalThemeData = deepClone(themeData);
|
|
@@ -2001,19 +2727,21 @@ globalThis.theme_editor_reload = async function(): Promise<void> {
|
|
|
2001
2727
|
updateDisplay();
|
|
2002
2728
|
editor.setStatus(editor.t("status.reset"));
|
|
2003
2729
|
}
|
|
2004
|
-
}
|
|
2730
|
+
}
|
|
2731
|
+
registerHandler("theme_editor_reload", theme_editor_reload);
|
|
2005
2732
|
|
|
2006
2733
|
/**
|
|
2007
2734
|
* Show help
|
|
2008
2735
|
*/
|
|
2009
|
-
|
|
2010
|
-
editor.
|
|
2011
|
-
}
|
|
2736
|
+
function theme_editor_show_help() : void {
|
|
2737
|
+
editor.debug(editor.t("status.help"));
|
|
2738
|
+
}
|
|
2739
|
+
registerHandler("theme_editor_show_help", theme_editor_show_help);
|
|
2012
2740
|
|
|
2013
2741
|
/**
|
|
2014
2742
|
* Delete the current user theme
|
|
2015
2743
|
*/
|
|
2016
|
-
|
|
2744
|
+
function theme_editor_delete() : void {
|
|
2017
2745
|
// Can only delete saved user themes
|
|
2018
2746
|
if (!state.themePath) {
|
|
2019
2747
|
editor.setStatus(editor.t("status.cannot_delete_unsaved"));
|
|
@@ -2027,12 +2755,13 @@ globalThis.theme_editor_delete = function(): void {
|
|
|
2027
2755
|
{ text: editor.t("prompt.delete_no"), description: "", value: "cancel" },
|
|
2028
2756
|
];
|
|
2029
2757
|
editor.setPromptSuggestions(suggestions);
|
|
2030
|
-
}
|
|
2758
|
+
}
|
|
2759
|
+
registerHandler("theme_editor_delete", theme_editor_delete);
|
|
2031
2760
|
|
|
2032
2761
|
/**
|
|
2033
2762
|
* Handle delete confirmation prompt
|
|
2034
2763
|
*/
|
|
2035
|
-
|
|
2764
|
+
async function onThemeDeletePromptConfirmed(args: {
|
|
2036
2765
|
prompt_type: string;
|
|
2037
2766
|
selected_index: number | null;
|
|
2038
2767
|
input: string;
|
|
@@ -2061,11 +2790,12 @@ globalThis.onThemeDeletePromptConfirmed = async function(args: {
|
|
|
2061
2790
|
}
|
|
2062
2791
|
}
|
|
2063
2792
|
} else {
|
|
2064
|
-
editor.
|
|
2793
|
+
editor.debug(editor.t("status.cancelled"));
|
|
2065
2794
|
}
|
|
2066
2795
|
|
|
2067
2796
|
return true;
|
|
2068
|
-
}
|
|
2797
|
+
}
|
|
2798
|
+
registerHandler("onThemeDeletePromptConfirmed", onThemeDeletePromptConfirmed);
|
|
2069
2799
|
|
|
2070
2800
|
// =============================================================================
|
|
2071
2801
|
// Command Registration
|
|
@@ -2091,8 +2821,13 @@ editor.registerCommand("%cmd.show_help", "%cmd.show_help_desc", "theme_editor_sh
|
|
|
2091
2821
|
editor.registerCommand("%cmd.delete_theme", "%cmd.delete_theme_desc", "theme_editor_delete", "theme-editor");
|
|
2092
2822
|
editor.registerCommand("%cmd.nav_up", "%cmd.nav_up_desc", "theme_editor_nav_up", "theme-editor");
|
|
2093
2823
|
editor.registerCommand("%cmd.nav_down", "%cmd.nav_down_desc", "theme_editor_nav_down", "theme-editor");
|
|
2094
|
-
editor.registerCommand("%cmd.
|
|
2095
|
-
editor.registerCommand("%cmd.
|
|
2824
|
+
editor.registerCommand("%cmd.nav_left", "%cmd.nav_left_desc", "theme_editor_nav_left", "theme-editor");
|
|
2825
|
+
editor.registerCommand("%cmd.nav_right", "%cmd.nav_right_desc", "theme_editor_nav_right", "theme-editor");
|
|
2826
|
+
editor.registerCommand("%cmd.focus_tab", "%cmd.focus_tab_desc", "theme_editor_focus_tab", "theme-editor");
|
|
2827
|
+
editor.registerCommand("%cmd.focus_shift_tab", "%cmd.focus_shift_tab_desc", "theme_editor_focus_shift_tab", "theme-editor");
|
|
2828
|
+
editor.registerCommand("%cmd.enter", "%cmd.enter_desc", "theme_editor_enter", "theme-editor");
|
|
2829
|
+
editor.registerCommand("%cmd.escape", "%cmd.escape_desc", "theme_editor_escape", "theme-editor");
|
|
2830
|
+
editor.registerCommand("%cmd.filter", "%cmd.filter_desc", "theme_editor_filter", "theme-editor");
|
|
2096
2831
|
|
|
2097
2832
|
// =============================================================================
|
|
2098
2833
|
// Plugin Initialization
|