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.
- package/README.md +46 -43
- package/dist/bin/envprism.mjs +254 -262
- package/dist/bin/envprism.mjs.map +1 -1
- package/dist/chunks/app-Bp7QXiLx.mjs +1965 -0
- package/dist/chunks/app-Bp7QXiLx.mjs.map +1 -0
- package/dist/chunks/matrix-CG1Msljr.mjs +740 -0
- package/dist/chunks/matrix-CG1Msljr.mjs.map +1 -0
- package/dist/chunks/serialize-DWF9VxrS.mjs +59 -0
- package/dist/chunks/serialize-DWF9VxrS.mjs.map +1 -0
- package/dist/config/define.mjs +14 -5
- package/dist/config/define.mjs.map +1 -1
- package/dist/index.mjs +3 -18
- package/package.json +16 -16
- package/dist/chunks/app-DgjyG_HE.mjs +0 -1800
- package/dist/chunks/app-DgjyG_HE.mjs.map +0 -1
- package/dist/chunks/matrix-C1pErf9Z.mjs +0 -676
- package/dist/chunks/matrix-C1pErf9Z.mjs.map +0 -1
- package/dist/chunks/serialize-D_ZNIgRa.mjs +0 -41
- package/dist/chunks/serialize-D_ZNIgRa.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -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
|