@fresh-editor/fresh-editor 0.2.21 → 0.2.23
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 +86 -1
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +197 -393
- package/plugins/audit_mode.ts +1747 -841
- package/plugins/config-schema.json +161 -35
- package/plugins/git_explorer.ts +7 -7
- package/plugins/lib/fresh.d.ts +67 -2
- package/plugins/lib/virtual-buffer-factory.ts +8 -0
- package/plugins/pkg.ts +151 -397
- package/plugins/schemas/package.schema.json +8 -0
- package/plugins/theme_editor.i18n.json +238 -14
- package/plugins/theme_editor.ts +299 -204
- package/themes/dark.json +5 -3
- package/themes/dracula.json +7 -2
- package/themes/high-contrast.json +6 -4
- package/themes/light.json +7 -2
- package/themes/nord.json +14 -9
- package/themes/nostalgia.json +1 -1
- package/themes/solarized-dark.json +16 -11
package/plugins/theme_editor.ts
CHANGED
|
@@ -62,7 +62,8 @@ type ColorValue = RGB | string;
|
|
|
62
62
|
// =============================================================================
|
|
63
63
|
|
|
64
64
|
const LEFT_WIDTH = 38;
|
|
65
|
-
const
|
|
65
|
+
const RIGHT_WIDTH_CONST = 61;
|
|
66
|
+
function RIGHT_WIDTH(): number { return RIGHT_WIDTH_CONST; }
|
|
66
67
|
|
|
67
68
|
type PickerFocusTarget =
|
|
68
69
|
| { type: "hex-input" }
|
|
@@ -333,8 +334,10 @@ interface ThemeEditorState {
|
|
|
333
334
|
themeData: Record<string, unknown>;
|
|
334
335
|
/** Original theme data (for change detection) */
|
|
335
336
|
originalThemeData: Record<string, unknown>;
|
|
336
|
-
/** Theme name */
|
|
337
|
+
/** Theme display name */
|
|
337
338
|
themeName: string;
|
|
339
|
+
/** Theme registry key (for lookups) */
|
|
340
|
+
themeKey: string;
|
|
338
341
|
/** Theme file path (null for new themes) */
|
|
339
342
|
themePath: string | null;
|
|
340
343
|
/** Expanded sections */
|
|
@@ -345,8 +348,10 @@ interface ThemeEditorState {
|
|
|
345
348
|
selectedIndex: number;
|
|
346
349
|
/** Whether there are unsaved changes */
|
|
347
350
|
hasChanges: boolean;
|
|
348
|
-
/**
|
|
349
|
-
|
|
351
|
+
/** All themes from registry: key → {name, pack} */
|
|
352
|
+
themeRegistry: Map<string, {name: string; pack: string}>;
|
|
353
|
+
/** Keys of builtin themes (empty pack) */
|
|
354
|
+
builtinKeys: Set<string>;
|
|
350
355
|
/** Pending save name for overwrite confirmation */
|
|
351
356
|
pendingSaveName: string | null;
|
|
352
357
|
/** Whether current theme is a built-in (requires Save As) */
|
|
@@ -371,6 +376,10 @@ interface ThemeEditorState {
|
|
|
371
376
|
viewportHeight: number;
|
|
372
377
|
/** Cached viewport width */
|
|
373
378
|
viewportWidth: number;
|
|
379
|
+
/** Buffer group ID (when using buffer groups) */
|
|
380
|
+
groupId: number | null;
|
|
381
|
+
/** Panel buffer IDs keyed by panel name */
|
|
382
|
+
panelBuffers: Record<string, number>;
|
|
374
383
|
}
|
|
375
384
|
|
|
376
385
|
/**
|
|
@@ -411,13 +420,15 @@ const state: ThemeEditorState = {
|
|
|
411
420
|
sourceBufferId: null,
|
|
412
421
|
themeData: {},
|
|
413
422
|
originalThemeData: {},
|
|
414
|
-
themeName: "
|
|
423
|
+
themeName: "",
|
|
424
|
+
themeKey: "",
|
|
415
425
|
themePath: null,
|
|
416
426
|
expandedSections: new Set(["editor", "syntax"]),
|
|
417
427
|
visibleFields: [],
|
|
418
428
|
selectedIndex: 0,
|
|
419
429
|
hasChanges: false,
|
|
420
|
-
|
|
430
|
+
themeRegistry: new Map(),
|
|
431
|
+
builtinKeys: new Set(),
|
|
421
432
|
pendingSaveName: null,
|
|
422
433
|
isBuiltin: false,
|
|
423
434
|
savedCursorPath: null,
|
|
@@ -430,30 +441,55 @@ const state: ThemeEditorState = {
|
|
|
430
441
|
treeScrollOffset: 0,
|
|
431
442
|
viewportHeight: 40,
|
|
432
443
|
viewportWidth: 120,
|
|
444
|
+
groupId: null,
|
|
445
|
+
panelBuffers: {},
|
|
433
446
|
};
|
|
434
447
|
|
|
435
448
|
// =============================================================================
|
|
436
449
|
// Color Definitions for UI
|
|
437
450
|
// =============================================================================
|
|
438
451
|
|
|
452
|
+
/**
|
|
453
|
+
* UI palette for the theme editor's own chrome.
|
|
454
|
+
*
|
|
455
|
+
* Each role maps to a theme-key path (e.g. `"syntax.keyword"`). The plugin
|
|
456
|
+
* passes these strings straight through to the core as
|
|
457
|
+
* `OverlayColorSpec::ThemeKey`, and the core's renderer resolves them
|
|
458
|
+
* against the *currently-active* theme on every frame
|
|
459
|
+
* (see `OverlayFace::from_options` → `OverlayFace::ThemedStyle`, and the
|
|
460
|
+
* render-time lookup in `split_rendering.rs`). That means the theme editor
|
|
461
|
+
* inherits the host theme's look and automatically picks up theme switches
|
|
462
|
+
* without the plugin having to rebuild its overlays or even be notified —
|
|
463
|
+
* the `resolve_theme_key` lookup runs with the new theme on the next render.
|
|
464
|
+
*
|
|
465
|
+
* Important: we only use keys that the theme defines as readable on
|
|
466
|
+
* `editor.bg` (since that's the background the theme editor draws over).
|
|
467
|
+
* That rules out `ui.menu_*`, `ui.tab_*`, etc. — those are designed for
|
|
468
|
+
* their own bg pairs (e.g. `ui.menu_active_fg` on `ui.menu_active_bg`) and
|
|
469
|
+
* will clash or go invisible when drawn on `editor.bg` (notoriously in
|
|
470
|
+
* high-contrast, where `ui.menu_active_fg` is pure black). So we only pull
|
|
471
|
+
* from `editor.*` and `syntax.*`, and lean on bold + distinct syntax roles
|
|
472
|
+
* to give each UI element its own visual identity.
|
|
473
|
+
*
|
|
474
|
+
* We don't need a client-side fallback chain: the core's `Theme` struct has
|
|
475
|
+
* serde defaults for every field, so `resolve_theme_key` always returns a
|
|
476
|
+
* value for any key listed here — a stub theme file can omit them and the
|
|
477
|
+
* defaults still apply.
|
|
478
|
+
*/
|
|
439
479
|
const colors = {
|
|
440
|
-
sectionHeader:
|
|
441
|
-
fieldName:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
pickerFocusBg: [40, 60, 100] as RGB, // Picker focused item bg
|
|
454
|
-
filterText: [200, 200, 100] as RGB, // Filter input text
|
|
455
|
-
previewBg: [25, 25, 30] as RGB, // Preview background
|
|
456
|
-
};
|
|
480
|
+
sectionHeader: "syntax.keyword",
|
|
481
|
+
fieldName: "editor.fg",
|
|
482
|
+
customValue: "syntax.string",
|
|
483
|
+
description: "syntax.comment",
|
|
484
|
+
footer: "editor.line_number_fg",
|
|
485
|
+
selectionBg: "editor.selection_bg",
|
|
486
|
+
divider: "editor.line_number_fg",
|
|
487
|
+
header: "syntax.keyword",
|
|
488
|
+
pickerLabel: "editor.fg",
|
|
489
|
+
pickerValue: "syntax.constant",
|
|
490
|
+
pickerFocusBg: "editor.selection_bg",
|
|
491
|
+
filterText: "syntax.function",
|
|
492
|
+
} as const satisfies Record<string, OverlayColorSpec>;
|
|
457
493
|
|
|
458
494
|
// =============================================================================
|
|
459
495
|
// Keyboard Shortcuts (defined once, used in mode and i18n)
|
|
@@ -669,33 +705,50 @@ function findThemesDir(): string {
|
|
|
669
705
|
/**
|
|
670
706
|
* Load list of available built-in themes
|
|
671
707
|
*/
|
|
672
|
-
|
|
708
|
+
/**
|
|
709
|
+
* Load all themes from the registry, returning a map of key → display name.
|
|
710
|
+
*
|
|
711
|
+
* The registry is keyed by unique keys (repo URLs, file:// paths, or bare
|
|
712
|
+
* names for builtins). Each value contains a `name` field (display name)
|
|
713
|
+
* and `_key`/`_pack` metadata.
|
|
714
|
+
*/
|
|
715
|
+
/**
|
|
716
|
+
* Load theme registry and populate state.themeRegistry + state.builtinKeys.
|
|
717
|
+
*/
|
|
718
|
+
async function loadThemeRegistry(): Promise<void> {
|
|
673
719
|
try {
|
|
674
|
-
editor.debug("[theme_editor]
|
|
720
|
+
editor.debug("[theme_editor] loadThemeRegistry: calling editor.getBuiltinThemes()");
|
|
675
721
|
const rawThemes = editor.getBuiltinThemes();
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
722
|
+
const themes = typeof rawThemes === "string"
|
|
723
|
+
? JSON.parse(rawThemes) as Record<string, Record<string, unknown>>
|
|
724
|
+
: rawThemes as Record<string, Record<string, unknown>>;
|
|
725
|
+
state.themeRegistry = new Map();
|
|
726
|
+
state.builtinKeys = new Set();
|
|
727
|
+
for (const [key, data] of Object.entries(themes)) {
|
|
728
|
+
const name = (data?.name as string) || key;
|
|
729
|
+
const pack = (data?._pack as string) || "";
|
|
730
|
+
state.themeRegistry.set(key, {name, pack});
|
|
731
|
+
// Builtin themes have an empty pack; user themes start with "user"
|
|
732
|
+
if (!pack || (!pack.startsWith("user") && !pack.startsWith("pkg"))) {
|
|
733
|
+
state.builtinKeys.add(key);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
editor.debug(`[theme_editor] loadThemeRegistry: loaded ${state.themeRegistry.size} themes (${state.builtinKeys.size} builtin)`);
|
|
683
737
|
} catch (e) {
|
|
684
|
-
editor.debug(`[theme_editor] Failed to load
|
|
738
|
+
editor.debug(`[theme_editor] Failed to load theme registry: ${e}`);
|
|
685
739
|
throw e;
|
|
686
740
|
}
|
|
687
741
|
}
|
|
688
742
|
|
|
689
743
|
/**
|
|
690
|
-
* Load theme data by
|
|
691
|
-
* Works for all theme types (builtin, user, package) — no file I/O needed.
|
|
744
|
+
* Load theme data by key from the in-memory theme registry.
|
|
692
745
|
*/
|
|
693
|
-
function loadThemeFile(
|
|
746
|
+
function loadThemeFile(key: string): Record<string, unknown> | null {
|
|
694
747
|
try {
|
|
695
|
-
const data = editor.getThemeData(
|
|
748
|
+
const data = editor.getThemeData(key);
|
|
696
749
|
return data as Record<string, unknown> | null;
|
|
697
750
|
} catch (e) {
|
|
698
|
-
editor.debug(`[theme_editor] Failed to load theme data for '${
|
|
751
|
+
editor.debug(`[theme_editor] Failed to load theme data for '${key}': ${e}`);
|
|
699
752
|
return null;
|
|
700
753
|
}
|
|
701
754
|
}
|
|
@@ -793,8 +846,8 @@ function buildTreeLines(): TreeLine[] {
|
|
|
793
846
|
type: "header",
|
|
794
847
|
});
|
|
795
848
|
|
|
796
|
-
// Separator
|
|
797
|
-
lines.push({ text: "─".repeat(
|
|
849
|
+
// Separator (adapt to panel width)
|
|
850
|
+
lines.push({ text: "─".repeat(Math.max(10, LEFT_WIDTH - 2)), type: "separator" });
|
|
798
851
|
|
|
799
852
|
// Filter
|
|
800
853
|
if (state.filterText) {
|
|
@@ -802,7 +855,7 @@ function buildTreeLines(): TreeLine[] {
|
|
|
802
855
|
text: `Filter: [${state.filterText}]`,
|
|
803
856
|
type: "filter",
|
|
804
857
|
});
|
|
805
|
-
lines.push({ text: "─".repeat(
|
|
858
|
+
lines.push({ text: "─".repeat(Math.max(10, LEFT_WIDTH - 2)), type: "separator" });
|
|
806
859
|
}
|
|
807
860
|
|
|
808
861
|
// Build visible fields
|
|
@@ -829,11 +882,15 @@ function buildTreeLines(): TreeLine[] {
|
|
|
829
882
|
});
|
|
830
883
|
} else {
|
|
831
884
|
const sel = isSelected && state.focusPanel === "tree" ? "▸" : " ";
|
|
832
|
-
|
|
885
|
+
// Adapt name/value truncation to panel width
|
|
886
|
+
// Layout: " ▸ name.padEnd(nameW) ██ value" = 6 + nameW + 3 + valueW
|
|
887
|
+
const nameW = Math.max(8, LEFT_WIDTH - 18);
|
|
888
|
+
const valueW = Math.max(5, LEFT_WIDTH - nameW - 9);
|
|
889
|
+
const name = field.def.key.length > nameW ? field.def.key.slice(0, nameW - 1) + "…" : field.def.key;
|
|
833
890
|
const colorStr = formatColorValue(field.value);
|
|
834
|
-
const valueStr = colorStr.length >
|
|
891
|
+
const valueStr = colorStr.length > valueW ? colorStr.slice(0, valueW - 1) + "…" : colorStr;
|
|
835
892
|
lines.push({
|
|
836
|
-
text: ` ${sel} ${name.padEnd(
|
|
893
|
+
text: ` ${sel} ${name.padEnd(nameW)} ██ ${valueStr}`,
|
|
837
894
|
type: "tree-field",
|
|
838
895
|
index: i,
|
|
839
896
|
path: field.path,
|
|
@@ -870,7 +927,7 @@ function buildPickerLines(): PickerLine[] {
|
|
|
870
927
|
} else {
|
|
871
928
|
lines.push({ text: "No field selected", type: "picker-title" });
|
|
872
929
|
}
|
|
873
|
-
lines.push({ text: "─".repeat(RIGHT_WIDTH - 2), type: "picker-separator" });
|
|
930
|
+
lines.push({ text: "─".repeat(RIGHT_WIDTH() - 2), type: "picker-separator" });
|
|
874
931
|
lines.push({ text: "Select a color field to edit", type: "picker-desc" });
|
|
875
932
|
return lines;
|
|
876
933
|
}
|
|
@@ -878,7 +935,7 @@ function buildPickerLines(): PickerLine[] {
|
|
|
878
935
|
// Field title
|
|
879
936
|
lines.push({ text: `${field.path} - ${field.def.displayName}`, type: "picker-title" });
|
|
880
937
|
lines.push({ text: `"${field.def.description}"`, type: "picker-desc" });
|
|
881
|
-
lines.push({ text: "─".repeat(RIGHT_WIDTH - 2), type: "picker-separator" });
|
|
938
|
+
lines.push({ text: "─".repeat(RIGHT_WIDTH() - 2), type: "picker-separator" });
|
|
882
939
|
|
|
883
940
|
// Color value display
|
|
884
941
|
const isNamed = typeof field.value === "string" && NAMED_COLORS[field.value] !== undefined;
|
|
@@ -919,7 +976,7 @@ function buildPickerLines(): PickerLine[] {
|
|
|
919
976
|
lines.push({ text: rowText, type: "picker-palette-row", paletteRow: row });
|
|
920
977
|
}
|
|
921
978
|
|
|
922
|
-
lines.push({ text: "─".repeat(RIGHT_WIDTH - 2), type: "picker-separator" });
|
|
979
|
+
lines.push({ text: "─".repeat(RIGHT_WIDTH() - 2), type: "picker-separator" });
|
|
923
980
|
|
|
924
981
|
// Preview section
|
|
925
982
|
lines.push({ text: "Preview:", type: "picker-label" });
|
|
@@ -1218,7 +1275,7 @@ function addBackgroundHighlight(
|
|
|
1218
1275
|
bufferId: number,
|
|
1219
1276
|
start: number,
|
|
1220
1277
|
end: number,
|
|
1221
|
-
bgColor:
|
|
1278
|
+
bgColor: OverlayColorSpec
|
|
1222
1279
|
): void {
|
|
1223
1280
|
editor.addOverlay(bufferId, "theme-sel", start, end, { bg: bgColor });
|
|
1224
1281
|
}
|
|
@@ -1329,10 +1386,99 @@ let isUpdatingDisplay = false;
|
|
|
1329
1386
|
* Full display update — rebuilds content and all overlays.
|
|
1330
1387
|
* Use for structural changes (open, section toggle, color edit, filter).
|
|
1331
1388
|
*/
|
|
1389
|
+
// --- Buffer Group panel content builders ---
|
|
1390
|
+
|
|
1391
|
+
function buildTreePanelEntries(): TextPropertyEntry[] {
|
|
1392
|
+
const entries: TextPropertyEntry[] = [];
|
|
1393
|
+
const allLeftLines = buildTreeLines();
|
|
1394
|
+
for (const item of allLeftLines) {
|
|
1395
|
+
const leftStyle = styleForLeftEntry(item);
|
|
1396
|
+
entries.push({
|
|
1397
|
+
text: " " + item.text + "\n",
|
|
1398
|
+
properties: {
|
|
1399
|
+
type: item.type,
|
|
1400
|
+
index: item.index,
|
|
1401
|
+
path: item.path,
|
|
1402
|
+
selected: item.selected,
|
|
1403
|
+
colorValue: item.colorValue,
|
|
1404
|
+
},
|
|
1405
|
+
style: leftStyle.style,
|
|
1406
|
+
inlineOverlays: leftStyle.inlineOverlays,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
return entries;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function buildPickerPanelEntries(): TextPropertyEntry[] {
|
|
1413
|
+
const entries: TextPropertyEntry[] = [];
|
|
1414
|
+
const rightLines = buildPickerLines();
|
|
1415
|
+
for (const item of rightLines) {
|
|
1416
|
+
const rightStyle = styleForRightEntry(item);
|
|
1417
|
+
entries.push({
|
|
1418
|
+
text: " " + item.text + "\n",
|
|
1419
|
+
properties: {
|
|
1420
|
+
type: item.type,
|
|
1421
|
+
namedRow: item.namedRow,
|
|
1422
|
+
paletteRow: item.paletteRow,
|
|
1423
|
+
previewLineIdx: item.previewLineIdx,
|
|
1424
|
+
},
|
|
1425
|
+
style: rightStyle.style,
|
|
1426
|
+
inlineOverlays: rightStyle.inlineOverlays,
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
return entries;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function buildFooterPanelEntries(): TextPropertyEntry[] {
|
|
1433
|
+
const hintText = " ↑↓ Navigate Tab Switch Panel Enter Edit /Filter Ctrl+S Save Esc Close";
|
|
1434
|
+
return [{ text: hintText + "\n", style: { fg: colors.header } }];
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1332
1437
|
function updateDisplay(): void {
|
|
1333
|
-
if (state.bufferId === null) return;
|
|
1334
1438
|
isUpdatingDisplay = true;
|
|
1335
1439
|
|
|
1440
|
+
// Always refresh viewport dimensions
|
|
1441
|
+
const viewport = editor.getViewport();
|
|
1442
|
+
if (viewport) {
|
|
1443
|
+
state.viewportHeight = viewport.height;
|
|
1444
|
+
state.viewportWidth = viewport.width;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Buffer group mode: write to each panel separately
|
|
1448
|
+
if (state.groupId !== null) {
|
|
1449
|
+
editor.setPanelContent(state.groupId, "tree", buildTreePanelEntries());
|
|
1450
|
+
editor.setPanelContent(state.groupId, "picker", buildPickerPanelEntries());
|
|
1451
|
+
editor.setPanelContent(state.groupId, "footer", buildFooterPanelEntries());
|
|
1452
|
+
|
|
1453
|
+
// Keep the selected tree row in view. The plugin's `selectedIndex`
|
|
1454
|
+
// navigation doesn't move the buffer cursor, so without this the
|
|
1455
|
+
// core-driven panel viewport would stay at the top even after many
|
|
1456
|
+
// Down-arrow presses, causing the `▸` marker to scroll off-screen
|
|
1457
|
+
// for sections with many fields.
|
|
1458
|
+
const treeBufferId = state.panelBuffers["tree"];
|
|
1459
|
+
if (typeof treeBufferId === "number") {
|
|
1460
|
+
const treeLines = buildTreeLines();
|
|
1461
|
+
let selectedLine = -1;
|
|
1462
|
+
for (let i = 0; i < treeLines.length; i++) {
|
|
1463
|
+
if (treeLines[i].index === state.selectedIndex && treeLines[i].selected) {
|
|
1464
|
+
selectedLine = i;
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (selectedLine >= 0) {
|
|
1469
|
+
editor.scrollBufferToLine(treeBufferId, selectedLine);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
isUpdatingDisplay = false;
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (state.bufferId === null) {
|
|
1478
|
+
isUpdatingDisplay = false;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1336
1482
|
const entries = buildDisplayEntries();
|
|
1337
1483
|
|
|
1338
1484
|
// Clear selection overlays BEFORE replacing content to prevent stale
|
|
@@ -1509,11 +1655,7 @@ function onThemeColorPromptConfirmed(args: {
|
|
|
1509
1655
|
setNestedValue(state.themeData, path, result.value);
|
|
1510
1656
|
state.hasChanges = !deepEqual(state.themeData, state.originalThemeData);
|
|
1511
1657
|
|
|
1512
|
-
|
|
1513
|
-
if (state.bufferId !== null) {
|
|
1514
|
-
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1515
|
-
applySelectionHighlighting(entries);
|
|
1516
|
-
}
|
|
1658
|
+
updateDisplay();
|
|
1517
1659
|
moveCursorToField(path);
|
|
1518
1660
|
editor.setStatus(editor.t("status.updated", { path }));
|
|
1519
1661
|
} else {
|
|
@@ -1559,28 +1701,17 @@ async function onThemeOpenPromptConfirmed(args: {
|
|
|
1559
1701
|
}): Promise<boolean> {
|
|
1560
1702
|
if (args.prompt_type !== "theme-open") return true;
|
|
1561
1703
|
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
let themeName = value;
|
|
1704
|
+
const key = args.input.trim();
|
|
1705
|
+
const isBuiltin = state.builtinKeys.has(key);
|
|
1706
|
+
const entry = state.themeRegistry.get(key);
|
|
1707
|
+
const themeName = entry?.name || key;
|
|
1567
1708
|
|
|
1568
|
-
|
|
1569
|
-
themeName = value.slice(5);
|
|
1570
|
-
isBuiltin = false;
|
|
1571
|
-
} else if (value.startsWith("builtin:")) {
|
|
1572
|
-
themeName = value.slice(8);
|
|
1573
|
-
isBuiltin = true;
|
|
1574
|
-
} else {
|
|
1575
|
-
// Fallback: check if it's a builtin theme
|
|
1576
|
-
isBuiltin = state.builtinThemes.includes(value);
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
const themeData = loadThemeFile(themeName);
|
|
1709
|
+
const themeData = loadThemeFile(key);
|
|
1580
1710
|
if (themeData) {
|
|
1581
1711
|
state.themeData = deepClone(themeData);
|
|
1582
1712
|
state.originalThemeData = deepClone(themeData);
|
|
1583
1713
|
state.themeName = themeName;
|
|
1714
|
+
state.themeKey = key;
|
|
1584
1715
|
state.themePath = null;
|
|
1585
1716
|
state.isBuiltin = isBuiltin;
|
|
1586
1717
|
state.hasChanges = false;
|
|
@@ -1623,7 +1754,7 @@ async function onThemeSaveAsPromptConfirmed(args: {
|
|
|
1623
1754
|
state.saveAsPreFilled = false;
|
|
1624
1755
|
|
|
1625
1756
|
// Reject names that match a built-in theme
|
|
1626
|
-
if (state.
|
|
1757
|
+
if (state.builtinKeys.has(name)) {
|
|
1627
1758
|
editor.startPromptWithInitial(editor.t("prompt.save_as_builtin_error"), "theme-save-as", name);
|
|
1628
1759
|
editor.setPromptSuggestions([{
|
|
1629
1760
|
text: state.themeName,
|
|
@@ -1693,30 +1824,19 @@ async function onThemeSelectInitialPromptConfirmed(args: {
|
|
|
1693
1824
|
}
|
|
1694
1825
|
editor.debug(`[theme_editor] prompt_type matched, processing selection...`);
|
|
1695
1826
|
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
let themeName = value;
|
|
1701
|
-
|
|
1702
|
-
if (value.startsWith("user:")) {
|
|
1703
|
-
themeName = value.slice(5);
|
|
1704
|
-
isBuiltin = false;
|
|
1705
|
-
} else if (value.startsWith("builtin:")) {
|
|
1706
|
-
themeName = value.slice(8);
|
|
1707
|
-
isBuiltin = true;
|
|
1708
|
-
} else {
|
|
1709
|
-
// Fallback: check if it's a builtin theme
|
|
1710
|
-
isBuiltin = state.builtinThemes.includes(value);
|
|
1711
|
-
}
|
|
1827
|
+
const key = args.input.trim();
|
|
1828
|
+
const isBuiltin = state.builtinKeys.has(key);
|
|
1829
|
+
const entry = state.themeRegistry.get(key);
|
|
1830
|
+
const themeName = entry?.name || key;
|
|
1712
1831
|
|
|
1713
1832
|
editor.debug(editor.t("status.loading"));
|
|
1714
1833
|
|
|
1715
|
-
const themeData = loadThemeFile(
|
|
1834
|
+
const themeData = loadThemeFile(key);
|
|
1716
1835
|
if (themeData) {
|
|
1717
1836
|
state.themeData = deepClone(themeData);
|
|
1718
1837
|
state.originalThemeData = deepClone(themeData);
|
|
1719
1838
|
state.themeName = themeName;
|
|
1839
|
+
state.themeKey = key;
|
|
1720
1840
|
state.themePath = null;
|
|
1721
1841
|
state.isBuiltin = isBuiltin;
|
|
1722
1842
|
state.hasChanges = false;
|
|
@@ -1758,6 +1878,11 @@ async function saveTheme(name?: string, restorePath?: string | null): Promise<bo
|
|
|
1758
1878
|
// (must match Rust's normalize_theme_name so config name matches filename)
|
|
1759
1879
|
const themeName = (name || state.themeName).toLowerCase().replace(/[_ ]/g, "-");
|
|
1760
1880
|
|
|
1881
|
+
if (!themeName) {
|
|
1882
|
+
editor.setStatus(editor.t("status.save_failed", { error: "No theme name" }));
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1761
1886
|
try {
|
|
1762
1887
|
// Build a complete theme object from all known fields.
|
|
1763
1888
|
// This ensures we always write every field, even if state.themeData
|
|
@@ -1781,16 +1906,13 @@ async function saveTheme(name?: string, restorePath?: string | null): Promise<bo
|
|
|
1781
1906
|
|
|
1782
1907
|
state.themePath = savedPath;
|
|
1783
1908
|
state.themeName = themeName;
|
|
1909
|
+
state.themeKey = `file://${savedPath}`;
|
|
1784
1910
|
state.isBuiltin = false; // After saving, it's now a user theme
|
|
1785
1911
|
state.originalThemeData = deepClone(state.themeData);
|
|
1786
1912
|
state.hasChanges = false;
|
|
1787
1913
|
|
|
1788
1914
|
// Update display
|
|
1789
|
-
|
|
1790
|
-
if (state.bufferId !== null) {
|
|
1791
|
-
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1792
|
-
applySelectionHighlighting(entries);
|
|
1793
|
-
}
|
|
1915
|
+
updateDisplay();
|
|
1794
1916
|
|
|
1795
1917
|
// Restore cursor position if provided
|
|
1796
1918
|
if (restorePath) {
|
|
@@ -1896,7 +2018,17 @@ function onThemeEditorCursorMoved(data: {
|
|
|
1896
2018
|
new_position: number;
|
|
1897
2019
|
text_properties?: Array<Record<string, any>>;
|
|
1898
2020
|
}): void {
|
|
1899
|
-
if (state.bufferId === null
|
|
2021
|
+
if (state.bufferId === null) return;
|
|
2022
|
+
// Accept cursor_moved events for any of the buffer group's panels
|
|
2023
|
+
// (tree, picker, footer). With buffer groups each panel is its own
|
|
2024
|
+
// buffer, so clicks in the picker fire cursor_moved for the picker
|
|
2025
|
+
// buffer — not the tree buffer (state.bufferId). We must handle
|
|
2026
|
+
// events for all of them so picker clicks (named colors, palette,
|
|
2027
|
+
// hex) still update selection/colors.
|
|
2028
|
+
const groupBufferIds = Object.values(state.panelBuffers || {});
|
|
2029
|
+
const isGroupBuffer =
|
|
2030
|
+
data.buffer_id === state.bufferId || groupBufferIds.includes(data.buffer_id);
|
|
2031
|
+
if (!isGroupBuffer) return;
|
|
1900
2032
|
if (isUpdatingDisplay) return;
|
|
1901
2033
|
|
|
1902
2034
|
const props = data.text_properties || [];
|
|
@@ -1967,20 +2099,6 @@ function onThemeEditorResize(data: { width: number; height: number }): void {
|
|
|
1967
2099
|
registerHandler("onThemeEditorResize", onThemeEditorResize);
|
|
1968
2100
|
editor.on("resize", "onThemeEditorResize");
|
|
1969
2101
|
|
|
1970
|
-
function onThemeEditorMouseScroll(data: { buffer_id: number; delta: number; col: number; row: number }): void {
|
|
1971
|
-
if (state.bufferId === null || data.buffer_id !== state.bufferId) return;
|
|
1972
|
-
|
|
1973
|
-
// Only scroll the tree when mouse is over the left panel area (col < LEFT_WIDTH)
|
|
1974
|
-
if (data.col >= LEFT_WIDTH) return;
|
|
1975
|
-
|
|
1976
|
-
// delta > 0 = scroll down, delta < 0 = scroll up
|
|
1977
|
-
const scrollAmount = data.delta > 0 ? 3 : -3;
|
|
1978
|
-
state.treeScrollOffset = Math.max(0, state.treeScrollOffset + scrollAmount);
|
|
1979
|
-
updateDisplay();
|
|
1980
|
-
}
|
|
1981
|
-
registerHandler("onThemeEditorMouseScroll", onThemeEditorMouseScroll);
|
|
1982
|
-
editor.on("mouse_scroll", "onThemeEditorMouseScroll");
|
|
1983
|
-
|
|
1984
2102
|
/**
|
|
1985
2103
|
* Handle buffer_closed event to reset state when buffer is closed by any means
|
|
1986
2104
|
*/
|
|
@@ -2029,20 +2147,24 @@ async function onThemeInspectKey(data: {
|
|
|
2029
2147
|
// Save context
|
|
2030
2148
|
state.sourceSplitId = editor.getActiveSplitId();
|
|
2031
2149
|
state.sourceBufferId = editor.getActiveBufferId();
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
// Auto-load the current theme (
|
|
2035
|
-
const
|
|
2036
|
-
const
|
|
2150
|
+
await loadThemeRegistry();
|
|
2151
|
+
|
|
2152
|
+
// Auto-load the current theme (data.theme_name is the config key)
|
|
2153
|
+
const themeKey = data.theme_name;
|
|
2154
|
+
const isBuiltin = state.builtinKeys.has(themeKey);
|
|
2155
|
+
const entry = state.themeRegistry.get(themeKey);
|
|
2156
|
+
const themeName = entry?.name || themeKey;
|
|
2157
|
+
const themeData = loadThemeFile(themeKey);
|
|
2037
2158
|
if (themeData) {
|
|
2038
2159
|
state.themeData = deepClone(themeData);
|
|
2039
2160
|
state.originalThemeData = deepClone(themeData);
|
|
2040
|
-
state.themeName =
|
|
2161
|
+
state.themeName = themeName;
|
|
2162
|
+
state.themeKey = themeKey;
|
|
2041
2163
|
state.themePath = null;
|
|
2042
2164
|
state.isBuiltin = isBuiltin;
|
|
2043
2165
|
state.hasChanges = false;
|
|
2044
2166
|
} else {
|
|
2045
|
-
editor.setStatus(`Failed to load theme '${
|
|
2167
|
+
editor.setStatus(`Failed to load theme '${themeName}'`);
|
|
2046
2168
|
return;
|
|
2047
2169
|
}
|
|
2048
2170
|
|
|
@@ -2444,46 +2566,35 @@ async function open_theme_editor() : Promise<void> {
|
|
|
2444
2566
|
|
|
2445
2567
|
editor.debug("[theme_editor] loading builtin themes...");
|
|
2446
2568
|
// Load available themes
|
|
2447
|
-
|
|
2448
|
-
editor.debug(`[theme_editor] loaded ${state.
|
|
2569
|
+
await loadThemeRegistry();
|
|
2570
|
+
editor.debug(`[theme_editor] loaded ${state.themeRegistry.size} themes (${state.builtinKeys.size} builtin)`);
|
|
2449
2571
|
|
|
2450
|
-
// Get current theme
|
|
2572
|
+
// Get current theme key from config
|
|
2451
2573
|
const config = editor.getConfig() as Record<string, unknown>;
|
|
2452
|
-
const
|
|
2574
|
+
const currentThemeKey = (config?.theme as string) || "dark";
|
|
2453
2575
|
|
|
2454
2576
|
// Prompt user to select which theme to edit
|
|
2455
2577
|
editor.startPrompt(editor.t("prompt.select_theme_to_edit"), "theme-select-initial");
|
|
2456
2578
|
|
|
2457
2579
|
const suggestions: PromptSuggestion[] = [];
|
|
2458
2580
|
|
|
2459
|
-
//
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2581
|
+
// Build suggestions from theme registry (user themes first, then builtins)
|
|
2582
|
+
const userSuggestions: PromptSuggestion[] = [];
|
|
2583
|
+
const builtinSuggestions: PromptSuggestion[] = [];
|
|
2584
|
+
for (const [key, {name}] of state.themeRegistry) {
|
|
2585
|
+
const isCurrent = key === currentThemeKey || name === currentThemeKey;
|
|
2586
|
+
const isBuiltin = state.builtinKeys.has(key);
|
|
2587
|
+
const desc = isBuiltin
|
|
2588
|
+
? (isCurrent ? editor.t("suggestion.builtin_theme_current") : editor.t("suggestion.builtin_theme"))
|
|
2589
|
+
: (isCurrent ? editor.t("suggestion.user_theme_current") : editor.t("suggestion.user_theme"));
|
|
2590
|
+
const suggestion = { text: name, description: desc, value: key };
|
|
2591
|
+
if (isBuiltin) {
|
|
2592
|
+
builtinSuggestions.push(suggestion);
|
|
2593
|
+
} else {
|
|
2594
|
+
userSuggestions.push(suggestion);
|
|
2473
2595
|
}
|
|
2474
|
-
} catch {
|
|
2475
|
-
// No user themes directory
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
// Add built-in themes
|
|
2479
|
-
for (const name of state.builtinThemes) {
|
|
2480
|
-
const isCurrent = name === currentThemeName;
|
|
2481
|
-
suggestions.push({
|
|
2482
|
-
text: name,
|
|
2483
|
-
description: isCurrent ? editor.t("suggestion.builtin_theme_current") : editor.t("suggestion.builtin_theme"),
|
|
2484
|
-
value: `builtin:${name}`,
|
|
2485
|
-
});
|
|
2486
2596
|
}
|
|
2597
|
+
suggestions.push(...userSuggestions, ...builtinSuggestions);
|
|
2487
2598
|
|
|
2488
2599
|
// Sort suggestions to put current theme first
|
|
2489
2600
|
suggestions.sort((a, b) => {
|
|
@@ -2510,38 +2621,30 @@ async function doOpenThemeEditor(): Promise<void> {
|
|
|
2510
2621
|
}
|
|
2511
2622
|
state.treeScrollOffset = 0;
|
|
2512
2623
|
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
showCursors: false,
|
|
2527
|
-
editingDisabled: true,
|
|
2624
|
+
// Create buffer group with layout: horizontal split (tree | picker) + footer
|
|
2625
|
+
const layout = JSON.stringify({
|
|
2626
|
+
type: "split",
|
|
2627
|
+
direction: "v",
|
|
2628
|
+
ratio: 0.95,
|
|
2629
|
+
first: {
|
|
2630
|
+
type: "split",
|
|
2631
|
+
direction: "h",
|
|
2632
|
+
ratio: 0.38,
|
|
2633
|
+
first: { type: "scrollable", id: "tree" },
|
|
2634
|
+
second: { type: "scrollable", id: "picker" },
|
|
2635
|
+
},
|
|
2636
|
+
second: { type: "fixed", id: "footer", height: 1 },
|
|
2528
2637
|
});
|
|
2529
|
-
const bufferId = result.bufferId;
|
|
2530
|
-
editor.debug(`[theme_editor] doOpenThemeEditor: createVirtualBuffer returned bufferId=${bufferId}`);
|
|
2531
|
-
editor.debug(`[theme_editor] doOpenThemeEditor: checking if bufferId !== null...`);
|
|
2532
2638
|
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
// Disable line wrapping — our layout is fixed-width
|
|
2539
|
-
editor.setLineWrap(bufferId, null, false);
|
|
2639
|
+
const groupResult = await editor.createBufferGroup("*Theme Editor*", "theme-editor", layout);
|
|
2640
|
+
state.groupId = groupResult.groupId;
|
|
2641
|
+
state.panelBuffers = groupResult.panels;
|
|
2642
|
+
state.bufferId = groupResult.panels["tree"]; // representative buffer
|
|
2643
|
+
state.splitId = null;
|
|
2540
2644
|
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
editor.debug(`[theme_editor] doOpenThemeEditor: calling setStatus...`);
|
|
2645
|
+
if (state.bufferId !== null) {
|
|
2646
|
+
// Set initial content for all panels
|
|
2647
|
+
updateDisplay();
|
|
2545
2648
|
editor.debug(editor.t("status.ready"));
|
|
2546
2649
|
editor.debug(`[theme_editor] doOpenThemeEditor: completed successfully`);
|
|
2547
2650
|
} else {
|
|
@@ -2575,8 +2678,12 @@ registerHandler("theme_editor_close", theme_editor_close);
|
|
|
2575
2678
|
* Actually close the editor (called after confirmation or when no changes)
|
|
2576
2679
|
*/
|
|
2577
2680
|
function doCloseEditor(): void {
|
|
2578
|
-
// Close the buffer (
|
|
2579
|
-
if (state.
|
|
2681
|
+
// Close the buffer group (or fall back to single buffer close)
|
|
2682
|
+
if (state.groupId !== null) {
|
|
2683
|
+
editor.closeBufferGroup(state.groupId);
|
|
2684
|
+
state.groupId = null;
|
|
2685
|
+
state.panelBuffers = {};
|
|
2686
|
+
} else if (state.bufferId !== null) {
|
|
2580
2687
|
editor.closeBuffer(state.bufferId);
|
|
2581
2688
|
}
|
|
2582
2689
|
|
|
@@ -2667,32 +2774,20 @@ function theme_editor_open() : void {
|
|
|
2667
2774
|
|
|
2668
2775
|
const suggestions: PromptSuggestion[] = [];
|
|
2669
2776
|
|
|
2670
|
-
//
|
|
2671
|
-
const
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
});
|
|
2682
|
-
}
|
|
2777
|
+
// Build suggestions from theme registry (user themes first, then builtins)
|
|
2778
|
+
const userSuggestions: PromptSuggestion[] = [];
|
|
2779
|
+
const builtinSuggestions: PromptSuggestion[] = [];
|
|
2780
|
+
for (const [key, {name}] of state.themeRegistry) {
|
|
2781
|
+
const isBuiltin = state.builtinKeys.has(key);
|
|
2782
|
+
const desc = isBuiltin ? editor.t("suggestion.builtin_theme") : editor.t("suggestion.user_theme");
|
|
2783
|
+
const suggestion = { text: name, description: desc, value: key };
|
|
2784
|
+
if (isBuiltin) {
|
|
2785
|
+
builtinSuggestions.push(suggestion);
|
|
2786
|
+
} else {
|
|
2787
|
+
userSuggestions.push(suggestion);
|
|
2683
2788
|
}
|
|
2684
|
-
} catch {
|
|
2685
|
-
// No user themes directory
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// Add built-in themes
|
|
2689
|
-
for (const name of state.builtinThemes) {
|
|
2690
|
-
suggestions.push({
|
|
2691
|
-
text: name,
|
|
2692
|
-
description: editor.t("suggestion.builtin_theme"),
|
|
2693
|
-
value: `builtin:${name}`,
|
|
2694
|
-
});
|
|
2695
2789
|
}
|
|
2790
|
+
suggestions.push(...userSuggestions, ...builtinSuggestions);
|
|
2696
2791
|
|
|
2697
2792
|
editor.setPromptSuggestions(suggestions);
|
|
2698
2793
|
}
|
|
@@ -2799,8 +2894,7 @@ registerHandler("theme_editor_save_as", theme_editor_save_as);
|
|
|
2799
2894
|
*/
|
|
2800
2895
|
async function theme_editor_reload() : Promise<void> {
|
|
2801
2896
|
if (state.themePath) {
|
|
2802
|
-
const
|
|
2803
|
-
const themeData = loadThemeFile(themeName);
|
|
2897
|
+
const themeData = loadThemeFile(state.themeKey);
|
|
2804
2898
|
if (themeData) {
|
|
2805
2899
|
state.themeData = deepClone(themeData);
|
|
2806
2900
|
state.originalThemeData = deepClone(themeData);
|
|
@@ -2867,7 +2961,8 @@ async function onThemeDeletePromptConfirmed(args: {
|
|
|
2867
2961
|
// Reset to default theme
|
|
2868
2962
|
state.themeData = createDefaultTheme();
|
|
2869
2963
|
state.originalThemeData = deepClone(state.themeData);
|
|
2870
|
-
state.themeName = "
|
|
2964
|
+
state.themeName = "";
|
|
2965
|
+
state.themeKey = "";
|
|
2871
2966
|
state.themePath = null;
|
|
2872
2967
|
state.hasChanges = false;
|
|
2873
2968
|
updateDisplay();
|