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