envprism 0.3.0 → 0.3.1

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.
@@ -0,0 +1,1965 @@
1
+ import { d as resolveThemeHex, f as findKvEntry, g as DEFAULT_CONFIG, h as truncate, l as DEFAULT_THEME_HEX, m as matchesFilter, p as formatValue, t as buildMatrix, u as resolveHeuristics } from "./matrix-CG1Msljr.mjs";
2
+ import { n as serializeEnv, t as rebuildKvLine } from "./serialize-DWF9VxrS.mjs";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join } from "pathe";
5
+ import { BoxRenderable, RGBA, ScrollBoxRenderable, TextRenderable, createCliRenderer } from "@opentui/core";
6
+ //#region src/tui/grouping.ts
7
+ var SECTION_COLLAPSE_KEY = "__other__";
8
+ function prefixSection(key) {
9
+ const idx = key.indexOf("_");
10
+ if (idx <= 0) return void 0;
11
+ return key.slice(0, idx);
12
+ }
13
+ /**
14
+ * Sort by first-underscore-prefix while preserving the relative order each
15
+ * prefix first appeared in. Keys without an underscore land in a trailing
16
+ * "Other" group keeping their authored order.
17
+ */
18
+ function groupByPrefix(keys) {
19
+ const groups = /* @__PURE__ */ new Map();
20
+ const order = [];
21
+ const OTHER = SECTION_COLLAPSE_KEY;
22
+ for (const k of keys) {
23
+ const p = prefixSection(k) ?? OTHER;
24
+ let bucket = groups.get(p);
25
+ if (!bucket) {
26
+ bucket = [];
27
+ groups.set(p, bucket);
28
+ if (p !== OTHER) order.push(p);
29
+ }
30
+ bucket.push(k);
31
+ }
32
+ if (groups.has(OTHER)) order.push(OTHER);
33
+ return order.flatMap((p) => groups.get(p));
34
+ }
35
+ /**
36
+ * Move row focus by `delta` while skipping section dividers that are not
37
+ * collapsed. The user only needs to land on a divider when its section is
38
+ * folded — that's the only context in which 'c' on the divider does work
39
+ * the focused-key path doesn't already cover.
40
+ */
41
+ function stepRow(state, delta) {
42
+ const items = state.visibleItems;
43
+ if (items.length === 0) return 0;
44
+ const canFocus = (i) => {
45
+ const it = items[i];
46
+ if (!it) return false;
47
+ if (it.kind === "key") return true;
48
+ return state.collapsed.has(it.ref);
49
+ };
50
+ let i = state.rowIdx + delta;
51
+ while (i >= 0 && i < items.length) {
52
+ if (canFocus(i)) return i;
53
+ i += delta;
54
+ }
55
+ return state.rowIdx;
56
+ }
57
+ function orderedKeys(matrix, state, sectionOf) {
58
+ const filtered = matrix.keys.filter((k) => {
59
+ if (!matchesFilter(k, state.filter)) return false;
60
+ if (state.driftOnly && !keyDrifts(matrix, k)) return false;
61
+ return true;
62
+ });
63
+ return state.grouping === "prefix" ? groupByPrefix(filtered) : filtered;
64
+ }
65
+ function sectionMetadata(matrix, sectionOf, state) {
66
+ const out = /* @__PURE__ */ new Map();
67
+ for (const key of orderedKeys(matrix, state, sectionOf)) {
68
+ const k = sectionOf(key) ?? "__other__";
69
+ const bucket = out.get(k) ?? {
70
+ drift: 0,
71
+ missing: 0,
72
+ total: 0
73
+ };
74
+ bucket.total += 1;
75
+ let drifts = false;
76
+ let missing = false;
77
+ for (const file of matrix.files) {
78
+ if (file === matrix.base) continue;
79
+ const s = matrix.cell(key, file).state;
80
+ if (s === "missing") missing = true;
81
+ if (s === "differs" || s === "missing" || s === "extra") drifts = true;
82
+ }
83
+ if (drifts) bucket.drift += 1;
84
+ if (missing) bucket.missing += 1;
85
+ out.set(k, bucket);
86
+ }
87
+ return out;
88
+ }
89
+ function keyDrifts(matrix, key) {
90
+ for (const file of matrix.files) {
91
+ if (file === matrix.base) continue;
92
+ const s = matrix.cell(key, file).state;
93
+ if (s === "differs" || s === "missing" || s === "extra") return true;
94
+ }
95
+ return false;
96
+ }
97
+ //#endregion
98
+ //#region src/tui/envfile.ts
99
+ function isValidEnvFileName(name) {
100
+ if (!name.startsWith(".env")) return false;
101
+ if (name.includes("/") || name.includes("\\")) return false;
102
+ if (name.endsWith(".swp") || name.endsWith("~") || name.endsWith(".bak")) return false;
103
+ return true;
104
+ }
105
+ function createEmptyEnvFile(path) {
106
+ return {
107
+ path,
108
+ entries: [{
109
+ kind: "comment",
110
+ raw: `# ${basename(path)}`
111
+ }],
112
+ trailingNewline: true
113
+ };
114
+ }
115
+ function appendKv(file, key, value) {
116
+ const entry = {
117
+ kind: "kv",
118
+ key,
119
+ rawValue: "",
120
+ value,
121
+ quoting: "none",
122
+ exportPrefix: false,
123
+ inlineComment: "",
124
+ raw: ""
125
+ };
126
+ rebuildKvLine(entry);
127
+ file.entries.push(entry);
128
+ file.trailingNewline = true;
129
+ return entry;
130
+ }
131
+ //#endregion
132
+ //#region src/tui/state/visible.ts
133
+ function cellKey(key, file) {
134
+ return `${key}|${file.path}`;
135
+ }
136
+ function markModified(ctx, key, file) {
137
+ ctx.state.modified.add(cellKey(key, file));
138
+ }
139
+ function pushUndo(ctx, entry) {
140
+ ctx.state.undo.push(entry);
141
+ if (ctx.state.undo.length > ctx.config.tui.undoLimit) ctx.state.undo.shift();
142
+ }
143
+ function focusedKey(ctx) {
144
+ const item = ctx.state.visibleItems[ctx.state.rowIdx];
145
+ return item && item.kind === "key" ? item.ref : null;
146
+ }
147
+ function focusKey(ctx, key) {
148
+ const idx = ctx.state.visibleKeys.indexOf(key);
149
+ if (idx >= 0) ctx.state.rowIdx = idx;
150
+ }
151
+ function recomputeVisibleKeys(ctx) {
152
+ const { state } = ctx;
153
+ const visibleKeys = [];
154
+ const items = [];
155
+ const orderedAll = orderedKeys(ctx.matrix, state, ctx.sectionOf);
156
+ const seen = /* @__PURE__ */ new Set();
157
+ const focusedRef = state.visibleItems[state.rowIdx]?.ref;
158
+ for (const k of orderedAll) {
159
+ if (!matchesFilter(k, state.filter)) continue;
160
+ if (state.driftOnly && !keyDrifts(ctx.matrix, k)) continue;
161
+ const secKey = ctx.sectionOf(k) ?? "__other__";
162
+ if (!seen.has(secKey)) {
163
+ seen.add(secKey);
164
+ items.push({
165
+ kind: "divider",
166
+ ref: secKey
167
+ });
168
+ }
169
+ if (state.collapsed.has(secKey)) continue;
170
+ items.push({
171
+ kind: "key",
172
+ ref: k
173
+ });
174
+ visibleKeys.push(k);
175
+ }
176
+ state.visibleKeys = visibleKeys;
177
+ state.visibleItems = items;
178
+ if (focusedRef) {
179
+ const i = items.findIndex((it) => it.ref === focusedRef);
180
+ if (i >= 0) state.rowIdx = i;
181
+ }
182
+ if (state.rowIdx >= items.length) state.rowIdx = Math.max(0, items.length - 1);
183
+ if (items[state.rowIdx]?.kind === "divider" && !state.collapsed.has(items[state.rowIdx].ref)) {
184
+ const next = stepRow(state, 1);
185
+ const prev = stepRow(state, -1);
186
+ state.rowIdx = next !== state.rowIdx ? next : prev;
187
+ }
188
+ }
189
+ function rebuildMatrix(ctx) {
190
+ const { state } = ctx;
191
+ const enabledList = ctx.allFiles.filter((f) => state.enabled.has(f));
192
+ if (!state.enabled.has(ctx.currentBase)) {
193
+ const next = enabledList[0];
194
+ if (next) ctx.currentBase = next;
195
+ }
196
+ ctx.matrix = buildMatrix(enabledList, ctx.currentBase);
197
+ if (state.colIdx >= ctx.matrix.files.length) state.colIdx = Math.max(0, ctx.matrix.files.length - 1);
198
+ if (state.sidebarIdx >= ctx.allFiles.length) state.sidebarIdx = Math.max(0, ctx.allFiles.length - 1);
199
+ recomputeVisibleKeys(ctx);
200
+ }
201
+ //#endregion
202
+ //#region src/tui/actions/batch.ts
203
+ function applyToAllFiles(ctx, key, value) {
204
+ const { state } = ctx;
205
+ let touched = 0;
206
+ for (const file of ctx.matrix.files) {
207
+ const existing = findKvEntry(file, key);
208
+ if (existing) {
209
+ if (existing.value === value) continue;
210
+ pushUndo(ctx, {
211
+ kind: "edit",
212
+ file,
213
+ entry: existing,
214
+ prevValue: existing.value,
215
+ prevRaw: existing.raw
216
+ });
217
+ existing.value = value;
218
+ rebuildKvLine(existing);
219
+ } else pushUndo(ctx, {
220
+ kind: "add-kv",
221
+ file,
222
+ entry: appendKv(file, key, value)
223
+ });
224
+ state.dirty.add(file);
225
+ markModified(ctx, key, file);
226
+ touched++;
227
+ }
228
+ return touched;
229
+ }
230
+ function syncToAll(ctx) {
231
+ const { state } = ctx;
232
+ const key = focusedKey(ctx);
233
+ const file = ctx.matrix.files[state.colIdx];
234
+ if (!key || !file) {
235
+ state.message = "Move onto a variable row to sync.";
236
+ ctx.refresh();
237
+ return;
238
+ }
239
+ const entry = findKvEntry(file, key);
240
+ if (!entry) {
241
+ state.message = `${key} has no value in ${basename(file.path)} to sync.`;
242
+ ctx.refresh();
243
+ return;
244
+ }
245
+ const touched = applyToAllFiles(ctx, key, entry.value);
246
+ rebuildMatrix(ctx);
247
+ state.message = touched > 0 ? `Synced ${key} to ${touched} file(s). Ctrl-S to save.` : `${key} is already in sync.`;
248
+ ctx.refresh();
249
+ }
250
+ function undo(ctx) {
251
+ const { state } = ctx;
252
+ const last = state.undo.pop();
253
+ if (!last) {
254
+ state.message = "Nothing to undo.";
255
+ ctx.refresh();
256
+ return;
257
+ }
258
+ switch (last.kind) {
259
+ case "edit":
260
+ last.entry.value = last.prevValue;
261
+ last.entry.raw = last.prevRaw;
262
+ state.dirty.add(last.file);
263
+ state.message = `Undid edit on ${last.entry.key} in ${basename(last.file.path)}.`;
264
+ break;
265
+ case "add-kv": {
266
+ const i = last.file.entries.indexOf(last.entry);
267
+ if (i >= 0) last.file.entries.splice(i, 1);
268
+ state.dirty.add(last.file);
269
+ state.message = `Undid add of ${last.entry.key} in ${basename(last.file.path)}.`;
270
+ break;
271
+ }
272
+ case "delete-kv":
273
+ last.file.entries.splice(last.idx, 0, last.entry);
274
+ state.dirty.add(last.file);
275
+ state.message = `Undid delete of ${last.entry.key} in ${basename(last.file.path)}.`;
276
+ break;
277
+ }
278
+ rebuildMatrix(ctx);
279
+ ctx.refresh();
280
+ }
281
+ function toggleEnabled(ctx) {
282
+ const { state } = ctx;
283
+ const file = ctx.allFiles[state.sidebarIdx];
284
+ if (!file) return;
285
+ if (state.enabled.has(file)) {
286
+ if (state.enabled.size === 1) {
287
+ state.message = "At least one file must stay enabled.";
288
+ ctx.refresh();
289
+ return;
290
+ }
291
+ state.enabled.delete(file);
292
+ state.message = `Hidden ${basename(file.path)} from the matrix.`;
293
+ } else {
294
+ state.enabled.add(file);
295
+ state.message = `Showing ${basename(file.path)} in the matrix.`;
296
+ }
297
+ rebuildMatrix(ctx);
298
+ ctx.refresh();
299
+ }
300
+ function setBase(ctx) {
301
+ const { state } = ctx;
302
+ const file = ctx.allFiles[state.sidebarIdx];
303
+ if (!file) return;
304
+ if (file === ctx.currentBase) {
305
+ state.message = `${basename(file.path)} is already the base.`;
306
+ ctx.refresh();
307
+ return;
308
+ }
309
+ const wasDisabled = !state.enabled.has(file);
310
+ if (wasDisabled) state.enabled.add(file);
311
+ ctx.currentBase = file;
312
+ rebuildMatrix(ctx);
313
+ state.message = wasDisabled ? `${basename(file.path)} is now the base (re-enabled).` : `${basename(file.path)} is now the base.`;
314
+ ctx.refresh();
315
+ }
316
+ //#endregion
317
+ //#region src/tui/actions/io.ts
318
+ async function saveDirty(ctx) {
319
+ const { state } = ctx;
320
+ if (state.dirty.size === 0) {
321
+ state.message = "Nothing to save.";
322
+ ctx.refresh();
323
+ return;
324
+ }
325
+ const count = state.dirty.size;
326
+ const errors = [];
327
+ for (const file of state.dirty) try {
328
+ await writeFile(file.path, serializeEnv(file), "utf8");
329
+ } catch (err) {
330
+ errors.push(`${basename(file.path)}: ${err.message ?? String(err)}`);
331
+ }
332
+ if (errors.length === 0) {
333
+ state.dirty.clear();
334
+ state.modified.clear();
335
+ state.message = `Saved ${count} file${count === 1 ? "" : "s"}.`;
336
+ } else state.message = `Save failed: ${errors.join("; ")}`;
337
+ ctx.refresh();
338
+ }
339
+ //#endregion
340
+ //#region src/tui/types.ts
341
+ var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
342
+ //#endregion
343
+ //#region src/tui/actions/prompt.ts
344
+ function openPrompt(ctx, prompt, value = "") {
345
+ const { state } = ctx;
346
+ state.prompt = prompt;
347
+ state.mode = "prompt";
348
+ state.message = null;
349
+ state.promptInput = value;
350
+ ctx.refresh();
351
+ }
352
+ function closePrompt(ctx, msg = null) {
353
+ const { state } = ctx;
354
+ state.prompt = null;
355
+ state.mode = "browse";
356
+ state.promptInput = "";
357
+ state.message = msg;
358
+ ctx.refresh();
359
+ }
360
+ function cancelPrompt(ctx) {
361
+ closePrompt(ctx, "Cancelled.");
362
+ }
363
+ function startEdit(ctx) {
364
+ const { state } = ctx;
365
+ const key = focusedKey(ctx);
366
+ const file = ctx.matrix.files[state.colIdx];
367
+ if (!key || !file) {
368
+ state.message = "Move onto a variable row to edit.";
369
+ ctx.refresh();
370
+ return;
371
+ }
372
+ const entry = findKvEntry(file, key);
373
+ openPrompt(ctx, {
374
+ kind: "edit",
375
+ key,
376
+ file
377
+ }, entry?.value ?? "");
378
+ }
379
+ function startAdd(ctx) {
380
+ const file = ctx.matrix.files[ctx.state.colIdx];
381
+ if (!file) return;
382
+ openPrompt(ctx, {
383
+ kind: "add-key",
384
+ file
385
+ });
386
+ }
387
+ function startNewFile(ctx) {
388
+ openPrompt(ctx, { kind: "new-file" });
389
+ }
390
+ function startDelete(ctx) {
391
+ const { state } = ctx;
392
+ const key = focusedKey(ctx);
393
+ const file = ctx.matrix.files[state.colIdx];
394
+ if (!key || !file) {
395
+ state.message = "Move onto a variable row to delete.";
396
+ ctx.refresh();
397
+ return;
398
+ }
399
+ const entry = findKvEntry(file, key);
400
+ if (!entry) {
401
+ state.message = `${key} is not present in ${basename(file.path)}.`;
402
+ ctx.refresh();
403
+ return;
404
+ }
405
+ const idx = file.entries.indexOf(entry);
406
+ if (idx >= 0) {
407
+ pushUndo(ctx, {
408
+ kind: "delete-kv",
409
+ file,
410
+ entry,
411
+ idx
412
+ });
413
+ file.entries.splice(idx, 1);
414
+ }
415
+ state.dirty.add(file);
416
+ markModified(ctx, key, file);
417
+ rebuildMatrix(ctx);
418
+ state.message = `Deleted ${key} from ${basename(file.path)}. Ctrl-S to save.`;
419
+ ctx.refresh();
420
+ }
421
+ function commitPrompt(ctx) {
422
+ const { state } = ctx;
423
+ if (!state.prompt) return;
424
+ const p = state.prompt;
425
+ const raw = state.promptInput;
426
+ if (p.kind === "edit") {
427
+ const existing = findKvEntry(p.file, p.key);
428
+ if (existing) {
429
+ pushUndo(ctx, {
430
+ kind: "edit",
431
+ file: p.file,
432
+ entry: existing,
433
+ prevValue: existing.value,
434
+ prevRaw: existing.raw
435
+ });
436
+ existing.value = raw;
437
+ rebuildKvLine(existing);
438
+ state.dirty.add(p.file);
439
+ markModified(ctx, p.key, p.file);
440
+ rebuildMatrix(ctx);
441
+ closePrompt(ctx, `Edited ${p.key} in ${basename(p.file.path)}. Ctrl-S to save.`);
442
+ } else {
443
+ const added = appendKv(p.file, p.key, raw);
444
+ pushUndo(ctx, {
445
+ kind: "add-kv",
446
+ file: p.file,
447
+ entry: added
448
+ });
449
+ state.dirty.add(p.file);
450
+ markModified(ctx, p.key, p.file);
451
+ rebuildMatrix(ctx);
452
+ closePrompt(ctx, `Added ${p.key} to ${basename(p.file.path)}. Ctrl-S to save.`);
453
+ }
454
+ return;
455
+ }
456
+ if (p.kind === "add-key") {
457
+ const key = raw.trim();
458
+ if (!KEY_RE.test(key)) {
459
+ state.message = `Invalid key "${key}". Must match ${KEY_RE.source}.`;
460
+ ctx.refresh();
461
+ return;
462
+ }
463
+ if (findKvEntry(p.file, key)) {
464
+ state.message = `${key} already exists in ${basename(p.file.path)}. Use edit instead.`;
465
+ ctx.refresh();
466
+ return;
467
+ }
468
+ openPrompt(ctx, {
469
+ kind: "add-value",
470
+ key,
471
+ file: p.file
472
+ });
473
+ return;
474
+ }
475
+ if (p.kind === "add-value") {
476
+ const added = appendKv(p.file, p.key, raw);
477
+ pushUndo(ctx, {
478
+ kind: "add-kv",
479
+ file: p.file,
480
+ entry: added
481
+ });
482
+ state.dirty.add(p.file);
483
+ markModified(ctx, p.key, p.file);
484
+ rebuildMatrix(ctx);
485
+ focusKey(ctx, p.key);
486
+ state.colIdx = ctx.matrix.files.indexOf(p.file);
487
+ closePrompt(ctx, `Added ${p.key} to ${basename(p.file.path)}. Ctrl-S to save.`);
488
+ return;
489
+ }
490
+ if (p.kind === "new-file") {
491
+ const name = raw.trim();
492
+ if (!isValidEnvFileName(name)) {
493
+ state.message = name.length === 0 ? "Filename cannot be empty." : !name.startsWith(".env") ? `Filename must start with ".env" (got "${name}").` : `"${name}" is not a valid env filename.`;
494
+ ctx.refresh();
495
+ return;
496
+ }
497
+ const newPath = join(dirname(ctx.currentBase.path), name);
498
+ if (ctx.allFiles.some((f) => f.path === newPath)) {
499
+ state.message = `${name} already exists.`;
500
+ ctx.refresh();
501
+ return;
502
+ }
503
+ const newFile = createEmptyEnvFile(newPath);
504
+ ctx.allFiles.push(newFile);
505
+ state.enabled.add(newFile);
506
+ state.dirty.add(newFile);
507
+ rebuildMatrix(ctx);
508
+ state.colIdx = ctx.matrix.files.indexOf(newFile);
509
+ closePrompt(ctx, `Created ${name}. Ctrl-S to write to disk.`);
510
+ return;
511
+ }
512
+ }
513
+ //#endregion
514
+ //#region src/tui/keys/browse.ts
515
+ function handleBrowseKey(ctx, key, cleanup) {
516
+ const { state } = ctx;
517
+ const refresh = ctx.refresh;
518
+ if (key.ctrl && key.name === "c") return cleanup();
519
+ if (key.ctrl && key.name === "s") {
520
+ state.confirmQuit = false;
521
+ saveDirty(ctx);
522
+ return;
523
+ }
524
+ if (key.ctrl && key.name === "z") {
525
+ state.confirmQuit = false;
526
+ return undo(ctx);
527
+ }
528
+ if (key.ctrl && key.name === "t") {
529
+ state.showSecrets = !state.showSecrets;
530
+ state.message = state.showSecrets ? "Showing secret values in plain text." : "Masking secret values.";
531
+ return refresh();
532
+ }
533
+ const tryQuit = () => {
534
+ if (state.dirty.size > 0 && !state.confirmQuit) {
535
+ state.confirmQuit = true;
536
+ state.message = `${state.dirty.size} unsaved file(s). Press 'q' again to quit without saving, or Ctrl-S to save first.`;
537
+ refresh();
538
+ return;
539
+ }
540
+ cleanup();
541
+ };
542
+ if (state.confirmQuit && key.name !== "q") {
543
+ state.confirmQuit = false;
544
+ state.message = null;
545
+ }
546
+ if (state.pane === "sidebar") {
547
+ switch (key.name) {
548
+ case "q": return tryQuit();
549
+ case "tab":
550
+ state.pane = "matrix";
551
+ return refresh();
552
+ case "right":
553
+ state.pane = "matrix";
554
+ return refresh();
555
+ case "up":
556
+ state.sidebarIdx = Math.max(0, state.sidebarIdx - 1);
557
+ return refresh();
558
+ case "down":
559
+ state.sidebarIdx = Math.min(ctx.allFiles.length - 1, state.sidebarIdx + 1);
560
+ return refresh();
561
+ case "space": return toggleEnabled(ctx);
562
+ case "b": return setBase(ctx);
563
+ }
564
+ if (key.sequence === " ") return toggleEnabled(ctx);
565
+ if (key.sequence === "?" || key.sequence === "ß") {
566
+ state.helpOpen = true;
567
+ return refresh();
568
+ }
569
+ return;
570
+ }
571
+ switch (key.name) {
572
+ case "q": return tryQuit();
573
+ case "tab":
574
+ state.pane = "sidebar";
575
+ return refresh();
576
+ case "up":
577
+ state.rowIdx = stepRow(state, -1);
578
+ return refresh();
579
+ case "down":
580
+ state.rowIdx = stepRow(state, 1);
581
+ return refresh();
582
+ case "left":
583
+ if (state.colIdx === 0) {
584
+ state.pane = "sidebar";
585
+ return refresh();
586
+ }
587
+ state.colIdx = Math.max(0, state.colIdx - 1);
588
+ return refresh();
589
+ case "right":
590
+ state.colIdx = Math.min(ctx.matrix.files.length - 1, state.colIdx + 1);
591
+ return refresh();
592
+ case "e":
593
+ case "return": return startEdit(ctx);
594
+ case "a": return startAdd(ctx);
595
+ case "d": return startDelete(ctx);
596
+ case "n": return startNewFile(ctx);
597
+ case "v":
598
+ state.driftOnly = !state.driftOnly;
599
+ state.message = state.driftOnly ? "Drift-only view (only keys with differences)." : "Full view.";
600
+ recomputeVisibleKeys(ctx);
601
+ return refresh();
602
+ case "c": {
603
+ if (key.shift) {
604
+ if (state.collapsed.size === 0) {
605
+ state.message = "Nothing collapsed.";
606
+ return refresh();
607
+ }
608
+ const count = state.collapsed.size;
609
+ state.collapsed.clear();
610
+ state.message = `Expanded ${count} section(s).`;
611
+ recomputeVisibleKeys(ctx);
612
+ return refresh();
613
+ }
614
+ const item = state.visibleItems[state.rowIdx];
615
+ if (!item) return;
616
+ const sectionKey = item.kind === "divider" ? item.ref : ctx.sectionOf(item.ref) ?? "__other__";
617
+ const label = sectionKey === "__other__" ? "(other)" : sectionKey;
618
+ if (state.collapsed.has(sectionKey)) {
619
+ state.collapsed.delete(sectionKey);
620
+ state.message = `Expanded "${label}".`;
621
+ } else {
622
+ state.collapsed.add(sectionKey);
623
+ state.message = `Collapsed "${label}". Press Shift-C to expand all.`;
624
+ }
625
+ recomputeVisibleKeys(ctx);
626
+ return refresh();
627
+ }
628
+ case "g":
629
+ state.grouping = state.grouping === "banner" ? "prefix" : "banner";
630
+ recomputeVisibleKeys(ctx);
631
+ state.message = state.grouping === "banner" ? "Group by comment banners." : "Group by key prefix (first underscore segment).";
632
+ return refresh();
633
+ }
634
+ if (key.sequence === "/" || key.name === "slash") {
635
+ state.mode = "filter";
636
+ state.message = null;
637
+ refresh();
638
+ return;
639
+ }
640
+ if (key.sequence === "=") return syncToAll(ctx);
641
+ if (key.sequence === "?" || key.sequence === "ß") {
642
+ state.helpOpen = true;
643
+ refresh();
644
+ }
645
+ }
646
+ //#endregion
647
+ //#region src/tui/keys/filter.ts
648
+ function handleFilterKey(ctx, key) {
649
+ const { state } = ctx;
650
+ if (key.name === "escape") {
651
+ state.filter = "";
652
+ state.mode = "browse";
653
+ recomputeVisibleKeys(ctx);
654
+ ctx.refresh();
655
+ return;
656
+ }
657
+ if (key.name === "return") {
658
+ state.mode = "browse";
659
+ ctx.refresh();
660
+ return;
661
+ }
662
+ if (key.name === "backspace") {
663
+ if (state.filter.length > 0) {
664
+ state.filter = state.filter.slice(0, -1);
665
+ recomputeVisibleKeys(ctx);
666
+ ctx.refresh();
667
+ }
668
+ return;
669
+ }
670
+ const seq = key.sequence ?? "";
671
+ if (seq.length === 1 && seq >= " " && seq !== "") {
672
+ state.filter += seq;
673
+ recomputeVisibleKeys(ctx);
674
+ ctx.refresh();
675
+ }
676
+ }
677
+ //#endregion
678
+ //#region src/tui/keys/help.ts
679
+ function handleHelpKey(ctx, key) {
680
+ if (key.name === "escape" || key.sequence === "?" || key.sequence === "ß" || key.name === "q") {
681
+ ctx.state.helpOpen = false;
682
+ ctx.refresh();
683
+ }
684
+ }
685
+ //#endregion
686
+ //#region src/tui/keys/prompt.ts
687
+ function handlePromptKey(ctx, key) {
688
+ const { state } = ctx;
689
+ if (key.name === "escape") {
690
+ cancelPrompt(ctx);
691
+ return;
692
+ }
693
+ if (key.name === "return") {
694
+ commitPrompt(ctx);
695
+ return;
696
+ }
697
+ if (key.name === "backspace") {
698
+ if (state.promptInput.length > 0) {
699
+ state.promptInput = state.promptInput.slice(0, -1);
700
+ ctx.refresh();
701
+ }
702
+ return;
703
+ }
704
+ if (key.ctrl && key.name === "t") {
705
+ state.showSecrets = !state.showSecrets;
706
+ ctx.refresh();
707
+ return;
708
+ }
709
+ if (key.ctrl && key.name === "a" && state.prompt) {
710
+ const p = state.prompt;
711
+ if (p.kind === "edit" || p.kind === "add-value") {
712
+ const touched = applyToAllFiles(ctx, p.key, state.promptInput);
713
+ rebuildMatrix(ctx);
714
+ closePrompt(ctx, touched > 0 ? `Set ${p.key} in ${touched} file(s). Ctrl-S to save.` : `${p.key} already had that value everywhere.`);
715
+ }
716
+ return;
717
+ }
718
+ const seq = key.sequence ?? "";
719
+ if (!key.ctrl && seq.length === 1 && seq >= " " && seq !== "") {
720
+ state.promptInput += seq;
721
+ ctx.refresh();
722
+ }
723
+ }
724
+ //#endregion
725
+ //#region src/tui/keys/onKey.ts
726
+ /**
727
+ * Build the global keypress handler. Dispatches by current mode; `cleanup`
728
+ * tears down the renderer and resolves the run promise (used by Ctrl-C and
729
+ * the quit-confirm path in browse mode).
730
+ */
731
+ function createOnKey(ctx, cleanup) {
732
+ return (key) => {
733
+ if (ctx.state.helpOpen) return handleHelpKey(ctx, key);
734
+ if (ctx.state.mode === "prompt") return handlePromptKey(ctx, key);
735
+ if (ctx.state.mode === "filter") return handleFilterKey(ctx, key);
736
+ handleBrowseKey(ctx, key, cleanup);
737
+ };
738
+ }
739
+ //#endregion
740
+ //#region src/tui/render/layout.ts
741
+ /**
742
+ * Build the static element tree once and return stable handles. Content is
743
+ * (re)populated later by the refreshers; this only establishes the layout
744
+ * skeleton and parent/child wiring.
745
+ */
746
+ function buildLayout(renderer, theme, layout) {
747
+ const root = new BoxRenderable(renderer, {
748
+ id: "root",
749
+ flexDirection: "column",
750
+ width: "100%",
751
+ height: "100%"
752
+ });
753
+ renderer.root.add(root);
754
+ const body = new BoxRenderable(renderer, {
755
+ id: "body",
756
+ flexDirection: "row",
757
+ flexGrow: 1
758
+ });
759
+ root.add(body);
760
+ const sidebar = new BoxRenderable(renderer, {
761
+ id: "sidebar",
762
+ border: true,
763
+ borderStyle: "rounded",
764
+ title: "",
765
+ flexDirection: "column",
766
+ width: layout.SIDEBAR_WIDTH,
767
+ flexShrink: 0,
768
+ paddingX: 1
769
+ });
770
+ body.add(sidebar);
771
+ const matrixBox = new BoxRenderable(renderer, {
772
+ id: "matrix",
773
+ border: true,
774
+ borderStyle: "rounded",
775
+ title: "",
776
+ flexDirection: "column",
777
+ flexGrow: 1,
778
+ paddingX: 1,
779
+ paddingBottom: 1
780
+ });
781
+ body.add(matrixBox);
782
+ const headerHost = new BoxRenderable(renderer, {
783
+ id: "header-host",
784
+ flexDirection: "column",
785
+ flexShrink: 0,
786
+ paddingRight: 1
787
+ });
788
+ matrixBox.add(headerHost);
789
+ const scrollBox = new ScrollBoxRenderable(renderer, {
790
+ id: "matrix-scroll",
791
+ flexGrow: 1,
792
+ scrollX: true,
793
+ scrollY: true,
794
+ viewportOptions: {
795
+ paddingRight: 1,
796
+ paddingBottom: 1
797
+ },
798
+ contentOptions: {
799
+ flexDirection: "column",
800
+ rowGap: layout.ROW_GAP
801
+ }
802
+ });
803
+ matrixBox.add(scrollBox);
804
+ const footer = new BoxRenderable(renderer, {
805
+ id: "footer",
806
+ flexDirection: "column",
807
+ flexShrink: 0,
808
+ paddingX: 1
809
+ });
810
+ root.add(footer);
811
+ const hintA = new BoxRenderable(renderer, {
812
+ id: "hint-a",
813
+ flexDirection: "row",
814
+ height: 1,
815
+ flexShrink: 0
816
+ });
817
+ footer.add(hintA);
818
+ const hintB = new BoxRenderable(renderer, {
819
+ id: "hint-b",
820
+ flexDirection: "row",
821
+ height: 1,
822
+ flexShrink: 0
823
+ });
824
+ footer.add(hintB);
825
+ const status = new TextRenderable(renderer, {
826
+ id: "status",
827
+ content: "",
828
+ fg: theme.fgDim,
829
+ wrapMode: "none",
830
+ height: 1
831
+ });
832
+ footer.add(status);
833
+ const filterBox = new BoxRenderable(renderer, {
834
+ id: "filter-box",
835
+ position: "absolute",
836
+ top: "15%",
837
+ left: "20%",
838
+ right: "20%",
839
+ height: "auto",
840
+ zIndex: 60,
841
+ border: true,
842
+ borderStyle: "rounded",
843
+ title: " Filter keys ",
844
+ paddingX: 2,
845
+ paddingY: 1,
846
+ visible: false,
847
+ backgroundColor: RGBA.fromHex("#1a1a1a"),
848
+ flexDirection: "column"
849
+ });
850
+ const filterField = new TextRenderable(renderer, {
851
+ id: "filter-field",
852
+ content: "",
853
+ fg: theme.fg,
854
+ height: 1,
855
+ wrapMode: "none"
856
+ });
857
+ const filterStatus = new TextRenderable(renderer, {
858
+ id: "filter-status",
859
+ content: "",
860
+ fg: theme.fgDim,
861
+ height: 1,
862
+ marginTop: 1,
863
+ wrapMode: "none"
864
+ });
865
+ const filterHint = new TextRenderable(renderer, {
866
+ id: "filter-hint",
867
+ content: "Enter · keep filter Esc · clear & close",
868
+ fg: theme.fg,
869
+ height: 1,
870
+ marginTop: 1,
871
+ wrapMode: "none"
872
+ });
873
+ filterBox.add(filterField);
874
+ filterBox.add(filterStatus);
875
+ filterBox.add(filterHint);
876
+ renderer.root.add(filterBox);
877
+ const promptBox = new BoxRenderable(renderer, {
878
+ id: "prompt-box",
879
+ position: "absolute",
880
+ top: "20%",
881
+ left: "15%",
882
+ right: "15%",
883
+ height: "auto",
884
+ zIndex: 50,
885
+ border: true,
886
+ borderStyle: "rounded",
887
+ title: "",
888
+ paddingX: 2,
889
+ paddingY: 1,
890
+ visible: false,
891
+ backgroundColor: RGBA.fromHex("#1a1a1a"),
892
+ flexDirection: "column"
893
+ });
894
+ const promptBody = new BoxRenderable(renderer, {
895
+ id: "prompt-body",
896
+ flexDirection: "column",
897
+ flexGrow: 1
898
+ });
899
+ const promptHint = new TextRenderable(renderer, {
900
+ id: "prompt-hint",
901
+ content: "",
902
+ fg: theme.fg,
903
+ height: 1,
904
+ marginTop: 2,
905
+ wrapMode: "none"
906
+ });
907
+ promptBox.add(promptBody);
908
+ promptBox.add(promptHint);
909
+ renderer.root.add(promptBox);
910
+ const dimOverlay = new BoxRenderable(renderer, {
911
+ id: "dim-overlay",
912
+ position: "absolute",
913
+ top: 0,
914
+ left: 0,
915
+ right: 0,
916
+ bottom: 0,
917
+ zIndex: 40,
918
+ backgroundColor: RGBA.fromHex("#000000"),
919
+ opacity: .6,
920
+ visible: false
921
+ });
922
+ renderer.root.add(dimOverlay);
923
+ const helpBox = new BoxRenderable(renderer, {
924
+ id: "help-overlay",
925
+ position: "absolute",
926
+ top: 2,
927
+ bottom: 2,
928
+ left: "8%",
929
+ right: "8%",
930
+ zIndex: 100,
931
+ border: true,
932
+ borderStyle: "rounded",
933
+ title: " Keybindings — press ? or Esc to close ",
934
+ paddingX: 2,
935
+ paddingY: 1,
936
+ visible: false,
937
+ backgroundColor: RGBA.fromHex("#1a1a1a"),
938
+ flexDirection: "column"
939
+ });
940
+ renderer.root.add(helpBox);
941
+ return {
942
+ root,
943
+ body,
944
+ sidebar,
945
+ matrixBox,
946
+ headerHost,
947
+ scrollBox,
948
+ footer,
949
+ hintA,
950
+ hintB,
951
+ status,
952
+ filterBox,
953
+ filterField,
954
+ filterStatus,
955
+ promptBox,
956
+ promptBody,
957
+ promptHint,
958
+ helpBox,
959
+ dimOverlay
960
+ };
961
+ }
962
+ //#endregion
963
+ //#region src/tui/render/filter.ts
964
+ function refreshFilter(ctx) {
965
+ const { el: { filterBox, filterField, filterStatus }, matrix, state } = ctx;
966
+ const open = state.mode === "filter";
967
+ filterBox.visible = open;
968
+ if (!open) return;
969
+ filterField.content = `▸ ${state.filter}▏`;
970
+ const matches = state.visibleKeys.length;
971
+ const total = matrix.keys.length;
972
+ filterStatus.content = state.filter.length === 0 ? "Type to filter the keys list." : `Matching ${matches} of ${total} keys.`;
973
+ }
974
+ //#endregion
975
+ //#region src/tui/render/dom.ts
976
+ function removeAllChildren(node) {
977
+ const children = [...node.getChildren()];
978
+ for (const child of children) node.remove(child);
979
+ }
980
+ //#endregion
981
+ //#region src/tui/render/footer.ts
982
+ function bindings(specs, theme) {
983
+ const out = [];
984
+ specs.forEach((spec, i) => {
985
+ if (i > 0) out.push({
986
+ text: " · ",
987
+ fg: theme.fgDim
988
+ });
989
+ out.push({
990
+ text: "[",
991
+ fg: theme.fgDim
992
+ });
993
+ out.push({
994
+ text: spec.key,
995
+ fg: theme.fg
996
+ });
997
+ out.push({
998
+ text: "] ",
999
+ fg: theme.fgDim
1000
+ });
1001
+ out.push({
1002
+ text: spec.label,
1003
+ fg: theme.fg
1004
+ });
1005
+ });
1006
+ return out;
1007
+ }
1008
+ function renderHintBox(box, renderer, segs) {
1009
+ removeAllChildren(box);
1010
+ segs.forEach((seg, i) => {
1011
+ box.add(new TextRenderable(renderer, {
1012
+ id: `${box.id}-seg-${i}`,
1013
+ content: seg.text,
1014
+ fg: seg.fg,
1015
+ height: 1,
1016
+ wrapMode: "none"
1017
+ }));
1018
+ });
1019
+ }
1020
+ function refreshFooter(ctx) {
1021
+ const { el: { hintA, hintB, status }, renderer, state, theme } = ctx;
1022
+ const dirty = state.dirty.size;
1023
+ const dirtyTail = dirty > 0 ? [
1024
+ {
1025
+ text: " ",
1026
+ fg: theme.fgDim
1027
+ },
1028
+ {
1029
+ text: "●",
1030
+ fg: theme.modified
1031
+ },
1032
+ {
1033
+ text: ` ${dirty} unsaved`,
1034
+ fg: theme.fg
1035
+ }
1036
+ ] : [];
1037
+ if (state.mode === "filter") {
1038
+ renderHintBox(hintA, renderer, [...bindings([{
1039
+ key: "Enter",
1040
+ label: "keep filter"
1041
+ }, {
1042
+ key: "Esc",
1043
+ label: "clear"
1044
+ }], theme), ...dirtyTail]);
1045
+ renderHintBox(hintB, renderer, [{
1046
+ text: " Filter:",
1047
+ fg: theme.fgDim
1048
+ }]);
1049
+ } else if (state.mode === "prompt") {
1050
+ renderHintBox(hintA, renderer, []);
1051
+ renderHintBox(hintB, renderer, []);
1052
+ } else if (state.pane === "sidebar") {
1053
+ renderHintBox(hintA, renderer, [...bindings([
1054
+ {
1055
+ key: "↑↓",
1056
+ label: "move"
1057
+ },
1058
+ {
1059
+ key: "Space",
1060
+ label: "toggle"
1061
+ },
1062
+ {
1063
+ key: "b",
1064
+ label: "set base"
1065
+ },
1066
+ {
1067
+ key: "Tab/→",
1068
+ label: "matrix"
1069
+ },
1070
+ {
1071
+ key: "^S",
1072
+ label: "save"
1073
+ },
1074
+ {
1075
+ key: "?",
1076
+ label: "help"
1077
+ },
1078
+ {
1079
+ key: "q",
1080
+ label: "quit"
1081
+ }
1082
+ ], theme), ...dirtyTail]);
1083
+ renderHintBox(hintB, renderer, [{
1084
+ text: "Files pane",
1085
+ fg: theme.fgDim
1086
+ }]);
1087
+ } else {
1088
+ renderHintBox(hintA, renderer, [...bindings([
1089
+ {
1090
+ key: "↑↓←→",
1091
+ label: "move"
1092
+ },
1093
+ {
1094
+ key: "Tab",
1095
+ label: "files"
1096
+ },
1097
+ {
1098
+ key: "e",
1099
+ label: "edit"
1100
+ },
1101
+ {
1102
+ key: "a",
1103
+ label: "add var"
1104
+ },
1105
+ {
1106
+ key: "d",
1107
+ label: "del var"
1108
+ },
1109
+ {
1110
+ key: "n",
1111
+ label: "new file"
1112
+ },
1113
+ {
1114
+ key: "=",
1115
+ label: "sync to all"
1116
+ },
1117
+ {
1118
+ key: "c",
1119
+ label: "collapse"
1120
+ },
1121
+ {
1122
+ key: "^T",
1123
+ label: "secrets"
1124
+ },
1125
+ {
1126
+ key: "^Z",
1127
+ label: "undo"
1128
+ },
1129
+ {
1130
+ key: "^S",
1131
+ label: "save"
1132
+ },
1133
+ {
1134
+ key: "/",
1135
+ label: "filter"
1136
+ },
1137
+ {
1138
+ key: "?/ß",
1139
+ label: "help"
1140
+ },
1141
+ {
1142
+ key: "q",
1143
+ label: "quit"
1144
+ }
1145
+ ], theme), ...dirtyTail]);
1146
+ renderHintBox(hintB, renderer, [
1147
+ {
1148
+ text: "view: ",
1149
+ fg: theme.fgDim
1150
+ },
1151
+ {
1152
+ text: state.driftOnly ? "drift" : "all",
1153
+ fg: theme.fg
1154
+ },
1155
+ {
1156
+ text: " · group: ",
1157
+ fg: theme.fgDim
1158
+ },
1159
+ {
1160
+ text: state.grouping,
1161
+ fg: theme.fg
1162
+ },
1163
+ {
1164
+ text: " · secrets: ",
1165
+ fg: theme.fgDim
1166
+ },
1167
+ {
1168
+ text: state.showSecrets ? "shown" : "masked",
1169
+ fg: theme.fg
1170
+ }
1171
+ ]);
1172
+ }
1173
+ status.content = state.message ?? "";
1174
+ }
1175
+ //#endregion
1176
+ //#region src/tui/help.ts
1177
+ function buildHelpLines() {
1178
+ return [
1179
+ {
1180
+ kind: "header",
1181
+ text: "Panes"
1182
+ },
1183
+ {
1184
+ kind: "entry",
1185
+ text: " Tab Switch matrix ↔ files sidebar"
1186
+ },
1187
+ {
1188
+ kind: "entry",
1189
+ text: " ← (leftmost col) Hop from matrix into the sidebar"
1190
+ },
1191
+ { kind: "blank" },
1192
+ {
1193
+ kind: "header",
1194
+ text: "Matrix navigation"
1195
+ },
1196
+ {
1197
+ kind: "entry",
1198
+ text: " ↑ ↓ ← → Move focused cell"
1199
+ },
1200
+ {
1201
+ kind: "entry",
1202
+ text: " Mouse wheel Scroll (both axes)"
1203
+ },
1204
+ { kind: "blank" },
1205
+ {
1206
+ kind: "header",
1207
+ text: "Files sidebar"
1208
+ },
1209
+ {
1210
+ kind: "entry",
1211
+ text: " ↑ ↓ Move selection"
1212
+ },
1213
+ {
1214
+ kind: "entry",
1215
+ text: " Space Enable / disable file"
1216
+ },
1217
+ {
1218
+ kind: "entry",
1219
+ text: " b Make selected file the base"
1220
+ },
1221
+ {
1222
+ kind: "entry",
1223
+ text: " Tab / → Back to matrix"
1224
+ },
1225
+ { kind: "blank" },
1226
+ {
1227
+ kind: "header",
1228
+ text: "Editing"
1229
+ },
1230
+ {
1231
+ kind: "entry",
1232
+ text: " e / Enter Edit focused cell value"
1233
+ },
1234
+ {
1235
+ kind: "entry",
1236
+ text: " a Add a new variable here"
1237
+ },
1238
+ {
1239
+ kind: "entry",
1240
+ text: " d Delete the variable from this file"
1241
+ },
1242
+ {
1243
+ kind: "entry",
1244
+ text: " n Create a new .env* file"
1245
+ },
1246
+ {
1247
+ kind: "entry",
1248
+ text: " = Sync focused value to every file"
1249
+ },
1250
+ {
1251
+ kind: "entry",
1252
+ text: " Ctrl-A (in edit) Apply typed value to every file"
1253
+ },
1254
+ {
1255
+ kind: "entry",
1256
+ text: " Ctrl-Z Undo last edit/add/delete"
1257
+ },
1258
+ {
1259
+ kind: "entry",
1260
+ text: " Ctrl-S Write all dirty files"
1261
+ },
1262
+ {
1263
+ kind: "entry",
1264
+ text: " c Collapse / expand focused section"
1265
+ },
1266
+ {
1267
+ kind: "entry",
1268
+ text: " Shift-C Expand every collapsed section"
1269
+ },
1270
+ { kind: "blank" },
1271
+ {
1272
+ kind: "header",
1273
+ text: "View"
1274
+ },
1275
+ {
1276
+ kind: "entry",
1277
+ text: " / Filter keys"
1278
+ },
1279
+ {
1280
+ kind: "entry",
1281
+ text: " v All keys ↔ drift-only"
1282
+ },
1283
+ {
1284
+ kind: "entry",
1285
+ text: " g Group by prefix ↔ banner"
1286
+ },
1287
+ {
1288
+ kind: "entry",
1289
+ text: " Ctrl-T Show / mask secret values"
1290
+ },
1291
+ { kind: "blank" },
1292
+ {
1293
+ kind: "header",
1294
+ text: "Help & exit"
1295
+ },
1296
+ {
1297
+ kind: "entry",
1298
+ text: " ? / ß Toggle this overlay"
1299
+ },
1300
+ {
1301
+ kind: "entry",
1302
+ text: " q Quit (twice if dirty)"
1303
+ },
1304
+ {
1305
+ kind: "entry",
1306
+ text: " Ctrl-C Force quit"
1307
+ },
1308
+ { kind: "blank" },
1309
+ {
1310
+ kind: "header",
1311
+ text: "Cell icons"
1312
+ },
1313
+ {
1314
+ kind: "legend",
1315
+ symbol: "≠ value",
1316
+ color: RGBA.fromHex("#ffd866"),
1317
+ description: "value differs from base"
1318
+ },
1319
+ {
1320
+ kind: "legend",
1321
+ symbol: "✗ missing",
1322
+ color: RGBA.fromHex("#ff6b6b"),
1323
+ description: "this file has no value for the key"
1324
+ },
1325
+ {
1326
+ kind: "legend",
1327
+ symbol: "★ value",
1328
+ color: RGBA.fromHex("#ffd866"),
1329
+ description: "key is not in the base"
1330
+ },
1331
+ {
1332
+ kind: "legend",
1333
+ symbol: "•••• (N)",
1334
+ color: RGBA.fromHex("#cccccc"),
1335
+ description: "secret-suspect value masked by length"
1336
+ },
1337
+ {
1338
+ kind: "legend",
1339
+ symbol: "⚠ TODO",
1340
+ color: RGBA.fromHex("#ffd866"),
1341
+ description: "placeholder value (TODO, CHANGEME, xxx, …)"
1342
+ },
1343
+ {
1344
+ kind: "legend",
1345
+ symbol: "value ●",
1346
+ color: RGBA.fromHex("#7fce6a"),
1347
+ description: "modified in this session — Ctrl-S to persist"
1348
+ }
1349
+ ];
1350
+ }
1351
+ //#endregion
1352
+ //#region src/tui/render/builders.ts
1353
+ function buildValueCell(cell, secret, width, focused, modified, theme, isPlaceholder) {
1354
+ const bg = focused ? theme.focusBg : void 0;
1355
+ const trailing = modified ? {
1356
+ char: "●",
1357
+ fg: theme.modified
1358
+ } : void 0;
1359
+ if (cell.state === "missing") return {
1360
+ text: "missing",
1361
+ fg: theme.fgDim,
1362
+ width,
1363
+ bg,
1364
+ icon: {
1365
+ char: "✗",
1366
+ fg: theme.missing
1367
+ },
1368
+ trailing
1369
+ };
1370
+ const value = cell.value ?? "";
1371
+ if (value !== "" && isPlaceholder(value)) return {
1372
+ text: value,
1373
+ fg: theme.fg,
1374
+ width,
1375
+ bg,
1376
+ icon: {
1377
+ char: "⚠",
1378
+ fg: theme.placeholder
1379
+ },
1380
+ trailing
1381
+ };
1382
+ const isEmpty = value === "" && !secret;
1383
+ const displayText = isEmpty ? "(empty)" : formatValue(value, secret);
1384
+ const displayFg = isEmpty ? theme.fgDim : theme.fg;
1385
+ if (cell.state === "differs") return {
1386
+ text: displayText,
1387
+ fg: displayFg,
1388
+ width,
1389
+ bg,
1390
+ icon: {
1391
+ char: "≠",
1392
+ fg: theme.differs
1393
+ },
1394
+ trailing
1395
+ };
1396
+ if (cell.state === "extra") return {
1397
+ text: displayText,
1398
+ fg: displayFg,
1399
+ width,
1400
+ bg,
1401
+ icon: {
1402
+ char: "★",
1403
+ fg: theme.extra
1404
+ },
1405
+ trailing
1406
+ };
1407
+ return {
1408
+ text: displayText,
1409
+ fg: displayFg,
1410
+ width,
1411
+ bg,
1412
+ trailing
1413
+ };
1414
+ }
1415
+ function buildHelpRow(renderer, id, line, theme) {
1416
+ const row = new BoxRenderable(renderer, {
1417
+ id,
1418
+ flexDirection: "row",
1419
+ height: 1,
1420
+ flexShrink: 0
1421
+ });
1422
+ if (line.kind === "header") row.add(new TextRenderable(renderer, {
1423
+ id: `${id}-t`,
1424
+ content: line.text,
1425
+ fg: theme.fgSection,
1426
+ wrapMode: "none",
1427
+ height: 1
1428
+ }));
1429
+ else if (line.kind === "entry") row.add(new TextRenderable(renderer, {
1430
+ id: `${id}-t`,
1431
+ content: line.text,
1432
+ fg: theme.fg,
1433
+ wrapMode: "none",
1434
+ height: 1
1435
+ }));
1436
+ else if (line.kind === "legend") {
1437
+ row.add(new TextRenderable(renderer, {
1438
+ id: `${id}-sym`,
1439
+ content: ` ${line.symbol.padEnd(12)}`,
1440
+ fg: line.color,
1441
+ wrapMode: "none",
1442
+ height: 1
1443
+ }));
1444
+ row.add(new TextRenderable(renderer, {
1445
+ id: `${id}-desc`,
1446
+ content: line.description,
1447
+ fg: theme.fgDim,
1448
+ wrapMode: "none",
1449
+ height: 1
1450
+ }));
1451
+ }
1452
+ return row;
1453
+ }
1454
+ function buildSectionDivider(renderer, id, name, width, meta, theme) {
1455
+ const baseName = name ?? "(other)";
1456
+ const segs = [
1457
+ {
1458
+ text: ` ${meta.collapsed ? "▸" : "▾"} `,
1459
+ fg: theme.fgDim
1460
+ },
1461
+ {
1462
+ text: baseName,
1463
+ fg: theme.fg
1464
+ },
1465
+ {
1466
+ text: " ",
1467
+ fg: theme.fgDim
1468
+ }
1469
+ ];
1470
+ if (meta.missing > 0) {
1471
+ segs.push({
1472
+ text: "✗ ",
1473
+ fg: theme.missing
1474
+ });
1475
+ segs.push({
1476
+ text: `${meta.missing}`,
1477
+ fg: theme.missing
1478
+ });
1479
+ segs.push({
1480
+ text: " missing ",
1481
+ fg: theme.fg
1482
+ });
1483
+ }
1484
+ if (meta.drift > 0) {
1485
+ segs.push({
1486
+ text: "≠ ",
1487
+ fg: theme.differs
1488
+ });
1489
+ segs.push({
1490
+ text: `${meta.drift}`,
1491
+ fg: theme.differs
1492
+ });
1493
+ segs.push({
1494
+ text: "/",
1495
+ fg: theme.fgDim
1496
+ });
1497
+ segs.push({
1498
+ text: `${meta.total}`,
1499
+ fg: theme.differs
1500
+ });
1501
+ segs.push({
1502
+ text: " drift ",
1503
+ fg: theme.fg
1504
+ });
1505
+ }
1506
+ if (meta.missing === 0 && meta.drift === 0) {
1507
+ segs.push({
1508
+ text: `${meta.total}`,
1509
+ fg: theme.fgDim
1510
+ });
1511
+ segs.push({
1512
+ text: " keys ",
1513
+ fg: theme.fg
1514
+ });
1515
+ }
1516
+ segs.push({
1517
+ text: " ",
1518
+ fg: theme.fgDim
1519
+ });
1520
+ const labelLength = segs.reduce((sum, s) => sum + s.text.length, 0);
1521
+ const rule = "─";
1522
+ const visible = Math.max(0, width - 2);
1523
+ const beforeLen = Math.max(2, Math.floor((visible - labelLength) / 2));
1524
+ const afterLen = Math.max(0, visible - beforeLen - labelLength);
1525
+ const box = new BoxRenderable(renderer, {
1526
+ id,
1527
+ flexDirection: "row",
1528
+ flexShrink: 0,
1529
+ height: 1,
1530
+ paddingX: 1,
1531
+ ...meta.focused ? { backgroundColor: theme.focusBg } : {}
1532
+ });
1533
+ box.add(new TextRenderable(renderer, {
1534
+ id: `${id}-lead`,
1535
+ content: rule.repeat(beforeLen),
1536
+ fg: theme.fgDim,
1537
+ height: 1,
1538
+ wrapMode: "none"
1539
+ }));
1540
+ segs.forEach((seg, i) => {
1541
+ box.add(new TextRenderable(renderer, {
1542
+ id: `${id}-seg-${i}`,
1543
+ content: seg.text,
1544
+ fg: seg.fg,
1545
+ height: 1,
1546
+ wrapMode: "none"
1547
+ }));
1548
+ });
1549
+ box.add(new TextRenderable(renderer, {
1550
+ id: `${id}-trail`,
1551
+ content: rule.repeat(afterLen),
1552
+ fg: theme.fgDim,
1553
+ height: 1,
1554
+ wrapMode: "none"
1555
+ }));
1556
+ return box;
1557
+ }
1558
+ function buildRow(renderer, idPrefix, cells, padX) {
1559
+ const row = new BoxRenderable(renderer, {
1560
+ id: idPrefix,
1561
+ flexDirection: "row",
1562
+ flexShrink: 0,
1563
+ height: 1
1564
+ });
1565
+ cells.forEach((cell, i) => {
1566
+ const cellOpts = {
1567
+ id: `${idPrefix}-c${i}`,
1568
+ width: cell.width,
1569
+ height: 1,
1570
+ flexDirection: "row",
1571
+ flexShrink: 0,
1572
+ paddingX: padX
1573
+ };
1574
+ if (cell.bg) cellOpts.backgroundColor = cell.bg;
1575
+ const cellBox = new BoxRenderable(renderer, cellOpts);
1576
+ const innerWidth = Math.max(0, cell.width - padX * 2);
1577
+ const iconLen = cell.icon ? cell.icon.char.length + 1 : 0;
1578
+ const trailingLen = cell.trailing ? cell.trailing.char.length + 1 : 0;
1579
+ const textWidth = Math.max(0, innerWidth - iconLen - trailingLen);
1580
+ if (cell.icon) cellBox.add(new TextRenderable(renderer, {
1581
+ id: `${idPrefix}-c${i}-icon`,
1582
+ content: `${cell.icon.char} `,
1583
+ fg: cell.icon.fg,
1584
+ height: 1,
1585
+ wrapMode: "none"
1586
+ }));
1587
+ cellBox.add(new TextRenderable(renderer, {
1588
+ id: `${idPrefix}-c${i}-t`,
1589
+ content: truncate(cell.text, textWidth),
1590
+ fg: cell.fg,
1591
+ height: 1,
1592
+ flexGrow: 1,
1593
+ wrapMode: "none"
1594
+ }));
1595
+ if (cell.trailing) cellBox.add(new TextRenderable(renderer, {
1596
+ id: `${idPrefix}-c${i}-trail`,
1597
+ content: ` ${cell.trailing.char}`,
1598
+ fg: cell.trailing.fg,
1599
+ height: 1,
1600
+ wrapMode: "none"
1601
+ }));
1602
+ row.add(cellBox);
1603
+ });
1604
+ return row;
1605
+ }
1606
+ //#endregion
1607
+ //#region src/tui/render/help.ts
1608
+ function refreshHelp(ctx) {
1609
+ const { el: { helpBox }, renderer, state, theme } = ctx;
1610
+ helpBox.visible = state.helpOpen;
1611
+ if (!state.helpOpen) return;
1612
+ removeAllChildren(helpBox);
1613
+ const lines = buildHelpLines();
1614
+ const narrow = renderer.terminalWidth < 100;
1615
+ const short = renderer.terminalHeight < 36;
1616
+ if (narrow || short) {
1617
+ const scroll = new ScrollBoxRenderable(renderer, {
1618
+ id: "help-scroll",
1619
+ flexGrow: 1,
1620
+ scrollX: false,
1621
+ scrollY: true,
1622
+ viewportOptions: { paddingRight: 1 },
1623
+ contentOptions: { flexDirection: "column" }
1624
+ });
1625
+ helpBox.add(scroll);
1626
+ lines.forEach((line, i) => {
1627
+ scroll.content.add(buildHelpRow(renderer, `help-${i}`, line, theme));
1628
+ });
1629
+ return;
1630
+ }
1631
+ const grid = new BoxRenderable(renderer, {
1632
+ id: "help-grid",
1633
+ flexDirection: "row",
1634
+ flexGrow: 1,
1635
+ columnGap: 3
1636
+ });
1637
+ const left = new BoxRenderable(renderer, {
1638
+ id: "help-left",
1639
+ flexDirection: "column",
1640
+ flexGrow: 1,
1641
+ flexBasis: 0
1642
+ });
1643
+ const right = new BoxRenderable(renderer, {
1644
+ id: "help-right",
1645
+ flexDirection: "column",
1646
+ flexGrow: 1,
1647
+ flexBasis: 0
1648
+ });
1649
+ const half = Math.floor(lines.length / 2);
1650
+ let splitIdx = half;
1651
+ for (let i = half; i < lines.length; i++) if (lines[i]?.kind === "blank") {
1652
+ splitIdx = i + 1;
1653
+ break;
1654
+ }
1655
+ lines.slice(0, splitIdx).forEach((line, i) => left.add(buildHelpRow(renderer, `help-l-${i}`, line, theme)));
1656
+ lines.slice(splitIdx).forEach((line, i) => right.add(buildHelpRow(renderer, `help-r-${i}`, line, theme)));
1657
+ grid.add(left);
1658
+ grid.add(right);
1659
+ helpBox.add(grid);
1660
+ }
1661
+ //#endregion
1662
+ //#region src/tui/render/matrix.ts
1663
+ function matrixTitle(matrix, state) {
1664
+ const visible = state.visibleKeys.length;
1665
+ const total = matrix.keys.length;
1666
+ const parts = [`${total} keys`];
1667
+ if (state.driftOnly) parts.push(`drift ${visible}/${total}`);
1668
+ else if (state.filter && visible !== total) parts.push(`"${state.filter}" ${visible}/${total}`);
1669
+ return ` Matrix · ${parts.join(" · ")} `;
1670
+ }
1671
+ /**
1672
+ * Available width inside the matrix box (subtract sidebar, both borders and the
1673
+ * matrix's horizontal padding). If columns would shrink below VALUE_COL_MIN to
1674
+ * fit, keep them at the minimum and let the ScrollBox handle the overflow.
1675
+ */
1676
+ function computeValueColWidth(ctx) {
1677
+ const { renderer, matrix, layout } = ctx;
1678
+ const available = Math.max(0, renderer.terminalWidth - layout.SIDEBAR_WIDTH - 6 - layout.KEY_COL_WIDTH);
1679
+ const fair = matrix.files.length ? Math.floor(available / matrix.files.length) : layout.VALUE_COL_MIN;
1680
+ return Math.max(layout.VALUE_COL_MIN, fair);
1681
+ }
1682
+ function refreshMatrix(ctx) {
1683
+ const { el: { matrixBox, headerHost, scrollBox }, renderer, matrix, state, theme, layout, heuristics } = ctx;
1684
+ const sectionOf = ctx.sectionOf;
1685
+ const valueColWidth = computeValueColWidth(ctx);
1686
+ matrixBox.title = matrixTitle(matrix, state);
1687
+ removeAllChildren(headerHost);
1688
+ removeAllChildren(scrollBox.content);
1689
+ headerHost.add(buildRow(renderer, "header", [{
1690
+ text: "KEY",
1691
+ fg: theme.fgHeader,
1692
+ width: layout.KEY_COL_WIDTH
1693
+ }, ...matrix.files.map((f) => ({
1694
+ text: basename(f.path),
1695
+ fg: theme.fgHeader,
1696
+ width: valueColWidth
1697
+ }))], layout.CELL_PAD_X));
1698
+ const totalWidth = layout.KEY_COL_WIDTH + valueColWidth * matrix.files.length;
1699
+ const sectionStats = sectionMetadata(matrix, sectionOf, state);
1700
+ for (let r = 0; r < state.visibleItems.length; r++) {
1701
+ const item = state.visibleItems[r];
1702
+ if (item.kind === "divider") {
1703
+ const sectionKey = item.ref;
1704
+ const sectionName = sectionKey === "__other__" ? void 0 : sectionKey;
1705
+ const meta = sectionStats.get(sectionKey) ?? {
1706
+ drift: 0,
1707
+ missing: 0,
1708
+ total: 0
1709
+ };
1710
+ const focused = state.mode === "browse" && r === state.rowIdx;
1711
+ scrollBox.content.add(buildSectionDivider(renderer, `row-${r}`, sectionName, totalWidth, {
1712
+ ...meta,
1713
+ collapsed: state.collapsed.has(sectionKey),
1714
+ focused
1715
+ }, theme));
1716
+ continue;
1717
+ }
1718
+ const key = item.ref;
1719
+ const secret = heuristics.isSecretKey(key) && !state.showSecrets;
1720
+ const cells = [{
1721
+ text: key,
1722
+ fg: theme.fg,
1723
+ width: layout.KEY_COL_WIDTH
1724
+ }];
1725
+ for (let c = 0; c < matrix.files.length; c++) {
1726
+ const file = matrix.files[c];
1727
+ const cell = matrix.cell(key, file);
1728
+ const focused = state.mode === "browse" && r === state.rowIdx && c === state.colIdx;
1729
+ const isModified = state.modified.has(`${key}|${file.path}`);
1730
+ cells.push(buildValueCell(cell, secret, valueColWidth, focused, isModified, theme, heuristics.isPlaceholderValue));
1731
+ }
1732
+ scrollBox.content.add(buildRow(renderer, `row-${r}`, cells, layout.CELL_PAD_X));
1733
+ }
1734
+ if (state.mode === "browse" && state.visibleItems.length > 0) {
1735
+ const target = `row-${state.rowIdx}`;
1736
+ setImmediate(() => {
1737
+ try {
1738
+ scrollBox.scrollChildIntoView(target);
1739
+ } catch {}
1740
+ });
1741
+ }
1742
+ }
1743
+ //#endregion
1744
+ //#region src/tui/render/prompt.ts
1745
+ function promptLabelText(p) {
1746
+ switch (p.kind) {
1747
+ case "edit": return ` Edit ${p.key} in ${basename(p.file.path)}:`;
1748
+ case "add-key": return ` Add new key to ${basename(p.file.path)}:`;
1749
+ case "add-value": return ` Value for ${p.key} in ${basename(p.file.path)}:`;
1750
+ case "new-file": return " New env file name (e.g. .env.local):";
1751
+ }
1752
+ }
1753
+ function refreshPrompt(ctx) {
1754
+ const { el: { promptBox, promptBody, promptHint }, renderer, matrix, state, theme, heuristics } = ctx;
1755
+ const open = state.mode === "prompt" && state.prompt !== null;
1756
+ promptBox.visible = open;
1757
+ if (!open || !state.prompt) return;
1758
+ promptBox.title = promptLabelText(state.prompt);
1759
+ const p = state.prompt;
1760
+ if (p.kind === "edit" || p.kind === "add-value") promptHint.content = "Enter · confirm Ctrl-A · apply to all Ctrl-T · show/mask secrets Esc · cancel";
1761
+ else promptHint.content = "Enter · confirm Esc · cancel";
1762
+ removeAllChildren(promptBody);
1763
+ promptBody.add(new TextRenderable(renderer, {
1764
+ id: "prompt-input-text",
1765
+ content: `▸ ${state.promptInput}▏`,
1766
+ fg: theme.fg,
1767
+ height: 1,
1768
+ wrapMode: "none"
1769
+ }));
1770
+ if (state.message) promptBody.add(new TextRenderable(renderer, {
1771
+ id: "prompt-error",
1772
+ content: `! ${state.message}`,
1773
+ fg: theme.missing,
1774
+ height: 1,
1775
+ marginTop: 1,
1776
+ wrapMode: "none"
1777
+ }));
1778
+ if (p.kind === "edit" || p.kind === "add-value") {
1779
+ const secret = heuristics.isSecretKey(p.key) && !state.showSecrets;
1780
+ const nameWidth = Math.min(26, Math.max(...matrix.files.map((f) => basename(f.path).length + 2)));
1781
+ promptBody.add(new TextRenderable(renderer, {
1782
+ id: "prompt-table-header",
1783
+ content: "Current values",
1784
+ fg: theme.fgSection,
1785
+ wrapMode: "none",
1786
+ height: 1,
1787
+ marginTop: 1
1788
+ }));
1789
+ for (const file of matrix.files) {
1790
+ const isTarget = file === p.file;
1791
+ const row = new BoxRenderable(renderer, {
1792
+ id: `prompt-row-${file.path}`,
1793
+ flexDirection: "row",
1794
+ height: 1,
1795
+ flexShrink: 0
1796
+ });
1797
+ row.add(new TextRenderable(renderer, {
1798
+ id: `prompt-row-${file.path}-name`,
1799
+ content: `${isTarget ? "▸" : " "} ${basename(file.path)}`.padEnd(nameWidth),
1800
+ fg: isTarget ? theme.fgBase : theme.fgDim,
1801
+ height: 1,
1802
+ wrapMode: "none"
1803
+ }));
1804
+ const entry = findKvEntry(file, p.key);
1805
+ const current = entry ? formatValue(entry.value, secret) : "✗ missing";
1806
+ row.add(new TextRenderable(renderer, {
1807
+ id: `prompt-row-${file.path}-value`,
1808
+ content: current,
1809
+ fg: !entry ? theme.missing : isTarget ? theme.fg : theme.fgDim,
1810
+ height: 1,
1811
+ wrapMode: "none"
1812
+ }));
1813
+ promptBody.add(row);
1814
+ }
1815
+ }
1816
+ }
1817
+ //#endregion
1818
+ //#region src/tui/render/sidebar.ts
1819
+ function refreshSidebar(ctx) {
1820
+ const { el: { sidebar }, renderer, matrix, allFiles, state, theme } = ctx;
1821
+ const total = allFiles.length;
1822
+ const enabled = state.enabled.size;
1823
+ sidebar.title = state.pane === "sidebar" ? ` Files ${enabled}/${total} • focused ` : ` Files ${enabled}/${total} `;
1824
+ removeAllChildren(sidebar);
1825
+ for (let i = 0; i < allFiles.length; i++) {
1826
+ const file = allFiles[i];
1827
+ const isBase = file === matrix.base;
1828
+ const isDirty = state.dirty.has(file);
1829
+ const isEnabled = state.enabled.has(file);
1830
+ const matrixIdx = matrix.files.indexOf(file);
1831
+ const isFocusCol = isEnabled && matrixIdx === state.colIdx;
1832
+ const isPaneFocus = state.pane === "sidebar" && i === state.sidebarIdx;
1833
+ const nameFg = !isEnabled ? theme.fgDim : isBase ? theme.fgBase : theme.fg;
1834
+ const row = new BoxRenderable(renderer, {
1835
+ id: `file-${file.path}`,
1836
+ flexDirection: "row",
1837
+ height: 1,
1838
+ flexShrink: 0,
1839
+ ...isPaneFocus ? { backgroundColor: theme.focusBg } : {}
1840
+ });
1841
+ const span = (id, text, fg) => new TextRenderable(renderer, {
1842
+ id: `${row.id}-${id}`,
1843
+ content: text,
1844
+ fg,
1845
+ height: 1,
1846
+ wrapMode: "none"
1847
+ });
1848
+ row.add(span("focus", `${isPaneFocus ? "▶" : " "} `, theme.fg));
1849
+ row.add(span("dirty", `${isDirty ? "●" : " "} `, isDirty ? theme.fgDirty : theme.fgDim));
1850
+ row.add(span("base", `${isBase ? "★" : " "} `, isBase ? theme.fgBase : theme.fgDim));
1851
+ row.add(span("col", `${isFocusCol ? "▸" : " "} `, theme.fgDim));
1852
+ row.add(span("enabled", `${isEnabled ? "✓" : "☐"} `, theme.fgDim));
1853
+ row.add(span("name", basename(file.path), nameFg));
1854
+ sidebar.add(row);
1855
+ }
1856
+ }
1857
+ //#endregion
1858
+ //#region src/tui/render/index.ts
1859
+ /**
1860
+ * Full synchronous render of every region. Reads `ctx.matrix` fresh so a
1861
+ * `rebuildMatrix` that ran between scheduling and this flush is reflected.
1862
+ */
1863
+ function refreshAll(ctx) {
1864
+ const { state } = ctx;
1865
+ refreshSidebar(ctx);
1866
+ refreshMatrix(ctx);
1867
+ refreshFooter(ctx);
1868
+ refreshPrompt(ctx);
1869
+ refreshHelp(ctx);
1870
+ refreshFilter(ctx);
1871
+ ctx.el.dimOverlay.visible = state.helpOpen || state.mode === "prompt" || state.mode === "filter";
1872
+ }
1873
+ //#endregion
1874
+ //#region src/tui/theme.ts
1875
+ /** Resolve a partial hex theme into RGBA values (gaps + invalid → defaults). */
1876
+ function resolveTheme(theme = {}, warn) {
1877
+ const hex = resolveThemeHex(theme, warn);
1878
+ const out = {};
1879
+ for (const key of Object.keys(DEFAULT_THEME_HEX)) out[key] = RGBA.fromHex(hex[key]);
1880
+ return out;
1881
+ }
1882
+ resolveTheme();
1883
+ function resolveLayout(layout) {
1884
+ return {
1885
+ KEY_COL_WIDTH: layout.keyColWidth,
1886
+ VALUE_COL_MIN: layout.valueColMin,
1887
+ SIDEBAR_WIDTH: layout.sidebarWidth,
1888
+ ROW_GAP: layout.rowGap,
1889
+ CELL_PAD_X: layout.cellPadX
1890
+ };
1891
+ }
1892
+ //#endregion
1893
+ //#region src/tui/app.ts
1894
+ async function runMatrixTui(initialMatrix, config = DEFAULT_CONFIG) {
1895
+ const renderer = await createCliRenderer({ useMouse: true });
1896
+ const theme = resolveTheme(config.tui.theme);
1897
+ const layout = resolveLayout(config.tui.layout);
1898
+ const heuristics = resolveHeuristics(config);
1899
+ const allFiles = initialMatrix.files.slice();
1900
+ const hasBanners = initialMatrix.keys.some((k) => initialMatrix.sectionOf(k) !== void 0);
1901
+ const grouping = heuristics.grouping === "auto" ? hasBanners ? "banner" : "prefix" : heuristics.grouping;
1902
+ const state = {
1903
+ mode: "browse",
1904
+ filter: "",
1905
+ rowIdx: 0,
1906
+ colIdx: 0,
1907
+ prompt: null,
1908
+ dirty: /* @__PURE__ */ new Set(),
1909
+ visibleKeys: initialMatrix.keys.slice(),
1910
+ visibleItems: [],
1911
+ message: null,
1912
+ driftOnly: false,
1913
+ confirmQuit: false,
1914
+ grouping,
1915
+ helpOpen: false,
1916
+ undo: [],
1917
+ pane: "matrix",
1918
+ sidebarIdx: 0,
1919
+ enabled: new Set(allFiles),
1920
+ showSecrets: !config.tui.maskSecrets,
1921
+ collapsed: /* @__PURE__ */ new Set(),
1922
+ modified: /* @__PURE__ */ new Set(),
1923
+ promptInput: ""
1924
+ };
1925
+ const el = buildLayout(renderer, theme, layout);
1926
+ let refreshScheduled = false;
1927
+ const ctx = {
1928
+ renderer,
1929
+ state,
1930
+ allFiles,
1931
+ el,
1932
+ config,
1933
+ theme,
1934
+ layout,
1935
+ heuristics,
1936
+ matrix: initialMatrix,
1937
+ currentBase: initialMatrix.base,
1938
+ refresh: () => {
1939
+ if (refreshScheduled) return;
1940
+ refreshScheduled = true;
1941
+ queueMicrotask(() => {
1942
+ refreshScheduled = false;
1943
+ refreshAll(ctx);
1944
+ });
1945
+ },
1946
+ refreshNow: () => refreshAll(ctx),
1947
+ sectionOf: (key) => state.grouping === "banner" ? ctx.matrix.sectionOf(key) : prefixSection(key)
1948
+ };
1949
+ recomputeVisibleKeys(ctx);
1950
+ ctx.refreshNow();
1951
+ return new Promise((resolve) => {
1952
+ const cleanup = () => {
1953
+ renderer._internalKeyInput.offInternal("keypress", onKey);
1954
+ renderer.destroy?.();
1955
+ resolve();
1956
+ };
1957
+ const onKey = createOnKey(ctx, cleanup);
1958
+ renderer._internalKeyInput.onInternal("keypress", onKey);
1959
+ renderer.on("resize", ctx.refresh);
1960
+ });
1961
+ }
1962
+ //#endregion
1963
+ export { runMatrixTui };
1964
+
1965
+ //# sourceMappingURL=app-Bp7QXiLx.mjs.map