mellos-mapping 0.19.0 → 0.20.2
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 +366 -56
- package/README.zh-CN.md +319 -47
- package/dist/hook-session-start.mjs +239 -0
- package/dist/mmap.mjs +338 -0
- package/dist/server.mjs +1737 -897
- package/dist/store-paths.mjs +107 -0
- package/dist/watch.mjs +1612 -902
- package/lib/domain/ops.d.ts +171 -0
- package/lib/domain/ops.js +384 -0
- package/lib/domain/types.d.ts +283 -0
- package/lib/domain/types.js +153 -0
- package/lib/render/canvas.d.ts +50 -0
- package/lib/render/canvas.js +210 -0
- package/lib/render/draw.d.ts +37 -0
- package/lib/render/draw.js +111 -0
- package/lib/render/layout.d.ts +89 -0
- package/lib/render/layout.js +200 -0
- package/lib/render/options.d.ts +39 -0
- package/lib/render/options.js +10 -0
- package/lib/render/render.d.ts +88 -0
- package/lib/render/render.js +128 -0
- package/lib/render/routing.d.ts +56 -0
- package/lib/render/routing.js +244 -0
- package/lib/render/skins.d.ts +54 -0
- package/lib/render/skins.js +99 -0
- package/lib/render/width.d.ts +24 -0
- package/lib/render/width.js +139 -0
- package/lib/render/zoom-geometry.d.ts +52 -0
- package/lib/render/zoom-geometry.js +56 -0
- package/lib/semantics/semantics.d.ts +169 -0
- package/lib/semantics/semantics.js +380 -0
- package/lib/semantics/vocabulary.d.ts +79 -0
- package/lib/semantics/vocabulary.js +112 -0
- package/lib/store/format.d.ts +67 -0
- package/lib/store/format.js +334 -0
- package/lib/store/store.d.ts +296 -0
- package/lib/store/store.js +734 -0
- package/package.json +41 -5
- package/scripts/codex-register.mjs +89 -20
- package/scripts/install-mmap-command.mjs +293 -0
- package/scripts/mmap.mjs +213 -0
- package/scripts/open-pane.mjs +115 -254
- package/scripts/pane-core.mjs +418 -0
package/dist/watch.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/watch/watch.ts
|
|
5
|
-
import { realpathSync, statSync } from "node:fs";
|
|
5
|
+
import { realpathSync, statSync as statSync2 } from "node:fs";
|
|
6
6
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
|
|
@@ -29,6 +29,12 @@ function makeNodeKind(raw) {
|
|
|
29
29
|
function makeSubmapRef(raw) {
|
|
30
30
|
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
31
31
|
}
|
|
32
|
+
var RANK_MIN = 0;
|
|
33
|
+
var RANK_MAX = 99;
|
|
34
|
+
var RANK_RULE_TEXT = `an integer in ${RANK_MIN}..${RANK_MAX}, 0 = bottom / most primitive`;
|
|
35
|
+
function makeRank(raw) {
|
|
36
|
+
return Number.isInteger(raw) && raw >= RANK_MIN && raw <= RANK_MAX ? ok(raw) : err({ kind: "invalid-rank", raw, rule: RANK_RULE_TEXT });
|
|
37
|
+
}
|
|
32
38
|
var MAP_KINDS = ["dev", "architecture", "dataflow", "behavior-tree", "sequence"];
|
|
33
39
|
function makeMapKind(raw) {
|
|
34
40
|
return MAP_KINDS.includes(raw) ? ok(raw) : err({ kind: "invalid-map-kind", raw });
|
|
@@ -42,6 +48,8 @@ function describeMapError(e) {
|
|
|
42
48
|
switch (e.kind) {
|
|
43
49
|
case "invalid-id":
|
|
44
50
|
return `invalid id "${e.raw}" (rule: ${e.rule})`;
|
|
51
|
+
case "invalid-rank":
|
|
52
|
+
return `invalid rank ${e.raw} (rule: ${e.rule})`;
|
|
45
53
|
case "invalid-status":
|
|
46
54
|
return `invalid status "${e.raw}" (expected: ${NODE_STATUSES.join(" | ")})`;
|
|
47
55
|
case "duplicate-layer":
|
|
@@ -62,6 +70,8 @@ function describeMapError(e) {
|
|
|
62
70
|
return `node "${e.id}" cannot depend on itself`;
|
|
63
71
|
case "duplicate-group":
|
|
64
72
|
return `group "${e.id}" already exists`;
|
|
73
|
+
case "id-collision":
|
|
74
|
+
return `id "${e.id}" already names a ${e.taken} on this map; nodes and groups share one id namespace (both render as boxes, so one id must mean one box) \u2014 rename "${e.id}"`;
|
|
65
75
|
case "unknown-group":
|
|
66
76
|
return `group "${e.id}" does not exist`;
|
|
67
77
|
case "invalid-map-kind":
|
|
@@ -73,9 +83,9 @@ function describeMapError(e) {
|
|
|
73
83
|
case "group-layer-mismatch":
|
|
74
84
|
return `node "${e.node}" (layer ${e.nodeLayer}) cannot join group "${e.group}" (layer ${e.groupLayer}); groups cluster nodes within one band`;
|
|
75
85
|
case "layer-not-empty":
|
|
76
|
-
return `layer "${e.id}" still holds node "${e.occupant}"; move or remove
|
|
86
|
+
return `layer "${e.id}" still holds node "${e.occupant}"; move its nodes to another band (moveNode) or remove them (removeNode) first`;
|
|
77
87
|
case "layer-holds-group":
|
|
78
|
-
return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups first`;
|
|
88
|
+
return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups (removeGroup) first`;
|
|
79
89
|
case "edge-not-downward":
|
|
80
90
|
return `edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; dependencies may only point to a lower layer`;
|
|
81
91
|
}
|
|
@@ -101,7 +111,15 @@ function checkMembership(map, node, nodeLayer, group) {
|
|
|
101
111
|
function hasEdge(map, from, to) {
|
|
102
112
|
return map.edges.some((e) => e.from === from && e.to === to);
|
|
103
113
|
}
|
|
114
|
+
function checkIdSpace(map, id, declaring) {
|
|
115
|
+
const taken = declaring === "node" ? map.groups.some((g) => g.id === id) : map.nodes.some((n) => n.id === id);
|
|
116
|
+
return taken ? { kind: "id-collision", id, taken: declaring === "node" ? "group" : "node" } : void 0;
|
|
117
|
+
}
|
|
104
118
|
function setTitle(map, title) {
|
|
119
|
+
if (title === null || title === void 0) {
|
|
120
|
+
const { title: _dropped, ...rest } = map;
|
|
121
|
+
return rest;
|
|
122
|
+
}
|
|
105
123
|
return { ...map, title };
|
|
106
124
|
}
|
|
107
125
|
function setKind(map, kind) {
|
|
@@ -122,6 +140,8 @@ function declareLayer(map, input) {
|
|
|
122
140
|
}
|
|
123
141
|
function declareGroup(map, input) {
|
|
124
142
|
if (findGroup(map, input.id)) return err({ kind: "duplicate-group", id: input.id });
|
|
143
|
+
const collision = checkIdSpace(map, input.id, "group");
|
|
144
|
+
if (collision) return err(collision);
|
|
125
145
|
if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
|
|
126
146
|
return ok({ ...map, groups: [...map.groups, { id: input.id, label: input.label, layer: input.layer }] });
|
|
127
147
|
}
|
|
@@ -139,6 +159,8 @@ function mapStatus(map) {
|
|
|
139
159
|
}
|
|
140
160
|
function declareNode(map, input) {
|
|
141
161
|
if (findNode(map, input.id)) return err({ kind: "duplicate-node", id: input.id });
|
|
162
|
+
const collision = checkIdSpace(map, input.id, "node");
|
|
163
|
+
if (collision) return err(collision);
|
|
142
164
|
if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
|
|
143
165
|
if (input.group !== void 0) {
|
|
144
166
|
const bad = checkMembership(map, input.id, input.layer, input.group);
|
|
@@ -150,6 +172,7 @@ function declareNode(map, input) {
|
|
|
150
172
|
label: input.label,
|
|
151
173
|
layer: input.layer,
|
|
152
174
|
status: input.status ?? "planned",
|
|
175
|
+
...input.evidence !== void 0 ? { evidence: input.evidence } : {},
|
|
153
176
|
...input.detail !== void 0 ? { detail: input.detail } : {},
|
|
154
177
|
...input.group !== void 0 ? { group: input.group } : {},
|
|
155
178
|
...input.kind !== void 0 ? { kind: input.kind } : {},
|
|
@@ -170,6 +193,9 @@ function linkNodes(map, from, to, label) {
|
|
|
170
193
|
if (fromRank <= toRank) return err({ kind: "edge-not-downward", from, fromRank, to, toRank });
|
|
171
194
|
return ok({ ...map, edges: [...map.edges, { from, to, ...label !== void 0 ? { label } : {} }] });
|
|
172
195
|
}
|
|
196
|
+
function resolveOptional(input, current) {
|
|
197
|
+
return input === void 0 ? current : input === null ? void 0 : input;
|
|
198
|
+
}
|
|
173
199
|
function updateNode(map, input) {
|
|
174
200
|
const node = findNode(map, input.id);
|
|
175
201
|
if (!node) return err({ kind: "unknown-node", id: input.id });
|
|
@@ -180,32 +206,91 @@ function updateNode(map, input) {
|
|
|
180
206
|
if (input.lane !== void 0 && input.lane !== null && !findLane(map, input.lane)) {
|
|
181
207
|
return err({ kind: "unknown-lane", id: input.lane });
|
|
182
208
|
}
|
|
183
|
-
const {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
const {
|
|
210
|
+
group: currentGroup,
|
|
211
|
+
kind: currentKind,
|
|
212
|
+
lane: currentLane,
|
|
213
|
+
submap: currentSubmap,
|
|
214
|
+
evidence: currentEvidence,
|
|
215
|
+
detail: currentDetail,
|
|
216
|
+
...bare
|
|
217
|
+
} = node;
|
|
218
|
+
const nextGroup = resolveOptional(input.group, currentGroup);
|
|
219
|
+
const nextKind = resolveOptional(input.kind, currentKind);
|
|
220
|
+
const nextLane = resolveOptional(input.lane, currentLane);
|
|
221
|
+
const nextSubmap = resolveOptional(input.submap, currentSubmap);
|
|
222
|
+
const nextEvidence = resolveOptional(input.evidence, currentEvidence);
|
|
223
|
+
const nextDetail = resolveOptional(input.detail, currentDetail);
|
|
188
224
|
const updated = {
|
|
189
225
|
...bare,
|
|
226
|
+
...nextEvidence !== void 0 ? { evidence: nextEvidence } : {},
|
|
227
|
+
...nextDetail !== void 0 ? { detail: nextDetail } : {},
|
|
190
228
|
...nextGroup !== void 0 ? { group: nextGroup } : {},
|
|
191
229
|
...nextKind !== void 0 ? { kind: nextKind } : {},
|
|
192
230
|
...nextLane !== void 0 ? { lane: nextLane } : {},
|
|
193
231
|
...nextSubmap !== void 0 ? { submap: nextSubmap } : {},
|
|
194
232
|
...input.status !== void 0 ? { status: input.status } : {},
|
|
195
|
-
...input.label !== void 0 ? { label: input.label } : {}
|
|
196
|
-
...input.evidence !== void 0 ? { evidence: input.evidence } : {},
|
|
197
|
-
...input.detail !== void 0 ? { detail: input.detail } : {}
|
|
233
|
+
...input.label !== void 0 ? { label: input.label } : {}
|
|
198
234
|
};
|
|
199
235
|
return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
|
|
200
236
|
}
|
|
201
237
|
|
|
202
|
-
// src/
|
|
238
|
+
// src/semantics/vocabulary.ts
|
|
239
|
+
var STATUS_GLYPHS = {
|
|
240
|
+
planned: ["\xB7", "."],
|
|
241
|
+
"in-progress": ["\u283F", "*"],
|
|
242
|
+
done: ["\u25A0", "#"],
|
|
243
|
+
regressed: ["\u2717", "X"]
|
|
244
|
+
};
|
|
245
|
+
function statusGlyph(status, unicode) {
|
|
246
|
+
const [uni, ascii] = STATUS_GLYPHS[status];
|
|
247
|
+
return unicode ? uni : ascii;
|
|
248
|
+
}
|
|
249
|
+
var UNVERIFIED_DONE_GLYPHS = ["\u25A1", "o"];
|
|
250
|
+
function unverifiedDoneGlyph(unicode) {
|
|
251
|
+
const [uni, ascii] = UNVERIFIED_DONE_GLYPHS;
|
|
252
|
+
return unicode ? uni : ascii;
|
|
253
|
+
}
|
|
254
|
+
var SPINNER_FRAMES = {
|
|
255
|
+
unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
256
|
+
ascii: ["|", "/", "-", "\\"]
|
|
257
|
+
};
|
|
258
|
+
function spinnerGlyph(frame, unicode) {
|
|
259
|
+
const frames = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
|
|
260
|
+
return frames[(frame % frames.length + frames.length) % frames.length];
|
|
261
|
+
}
|
|
262
|
+
var NODE_KIND_GLYPHS = {
|
|
263
|
+
selector: ["?", "?"],
|
|
264
|
+
sequence: ["\xBB", ">"],
|
|
265
|
+
parallel: ["\u2016", "="],
|
|
266
|
+
decorator: ["\u25CC", "o"],
|
|
267
|
+
condition: ["\u25C7", "c"],
|
|
268
|
+
action: ["\xB7", "."],
|
|
269
|
+
source: ["\u25CB", "o"],
|
|
270
|
+
transform: ["\u25D0", "%"],
|
|
271
|
+
sink: ["\u25CF", "*"],
|
|
272
|
+
service: ["\u25C6", "S"],
|
|
273
|
+
db: ["\u25A4", "D"],
|
|
274
|
+
queue: ["\u2263", "Q"],
|
|
275
|
+
ui: ["\u25A3", "U"]
|
|
276
|
+
};
|
|
277
|
+
function kindGlyph(kind, unicode) {
|
|
278
|
+
const pair = NODE_KIND_GLYPHS[kind];
|
|
279
|
+
return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/semantics/semantics.ts
|
|
203
283
|
var ZOOM_MIN = -4;
|
|
204
284
|
var ZOOM_MAX = 2;
|
|
205
285
|
var ZOOM_DEFAULT = 0;
|
|
206
286
|
function clampZoom(n) {
|
|
207
287
|
return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
|
|
208
288
|
}
|
|
289
|
+
function zoomMode(zoom) {
|
|
290
|
+
if (zoom >= 1) return "detail";
|
|
291
|
+
if (zoom <= -4) return "overview";
|
|
292
|
+
return "boxes";
|
|
293
|
+
}
|
|
209
294
|
function zoomLabel(zoom) {
|
|
210
295
|
switch (zoom) {
|
|
211
296
|
case 2:
|
|
@@ -224,9 +309,216 @@ function zoomLabel(zoom) {
|
|
|
224
309
|
return "overview";
|
|
225
310
|
}
|
|
226
311
|
}
|
|
312
|
+
function isNeutralKind(map) {
|
|
313
|
+
return map.kind !== void 0 && map.kind !== "dev";
|
|
314
|
+
}
|
|
315
|
+
function aggregateMap(map) {
|
|
316
|
+
if (map.groups.length === 0) return void 0;
|
|
317
|
+
const representative = /* @__PURE__ */ new Map();
|
|
318
|
+
for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
|
|
319
|
+
const nodes = map.groups.map((g) => {
|
|
320
|
+
const members = map.nodes.filter((n) => n.group === g.id);
|
|
321
|
+
const done = members.filter((n) => n.status === "done").length;
|
|
322
|
+
return {
|
|
323
|
+
id: g.id,
|
|
324
|
+
// neutral kinds document structure, not progress — no member counts
|
|
325
|
+
label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
|
|
326
|
+
layer: g.layer,
|
|
327
|
+
status: groupStatus(map, g.id)
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
|
|
331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
332
|
+
const edges = [];
|
|
333
|
+
for (const e of map.edges) {
|
|
334
|
+
const from = representative.get(e.from);
|
|
335
|
+
const to = representative.get(e.to);
|
|
336
|
+
if (from === to || seen.has(`${from}->${to}`)) continue;
|
|
337
|
+
seen.add(`${from}->${to}`);
|
|
338
|
+
edges.push({ from, to });
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
...map.title !== void 0 ? { title: map.title } : {},
|
|
342
|
+
...map.kind !== void 0 ? { kind: map.kind } : {},
|
|
343
|
+
layers: map.layers,
|
|
344
|
+
groups: [],
|
|
345
|
+
lanes: map.lanes,
|
|
346
|
+
nodes,
|
|
347
|
+
edges
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function focusInfo(map, focusId) {
|
|
351
|
+
const layerNameOf = (layerId) => map.layers.find((l) => l.id === layerId)?.name ?? layerId;
|
|
352
|
+
const group = map.groups.find((g) => g.id === focusId);
|
|
353
|
+
if (group) {
|
|
354
|
+
const members = map.nodes.filter((n) => n.group === group.id);
|
|
355
|
+
const memberIds = new Set(members.map((n) => n.id));
|
|
356
|
+
const rep = (id) => {
|
|
357
|
+
const n = map.nodes.find((x) => x.id === id);
|
|
358
|
+
const owner = n.group !== void 0 ? map.groups.find((g) => g.id === n.group) : void 0;
|
|
359
|
+
return owner !== void 0 ? { id: owner.id, label: owner.label, status: groupStatus(map, owner.id) } : { id: n.id, label: n.label, status: n.status };
|
|
360
|
+
};
|
|
361
|
+
const dedupe = (refs) => {
|
|
362
|
+
const seen = /* @__PURE__ */ new Set();
|
|
363
|
+
const out = [];
|
|
364
|
+
for (const r of refs) {
|
|
365
|
+
if (seen.has(r.id)) continue;
|
|
366
|
+
seen.add(r.id);
|
|
367
|
+
out.push(r);
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
};
|
|
371
|
+
return {
|
|
372
|
+
kind: "group",
|
|
373
|
+
group,
|
|
374
|
+
status: groupStatus(map, group.id),
|
|
375
|
+
layerName: layerNameOf(group.layer),
|
|
376
|
+
members,
|
|
377
|
+
uses: dedupe(
|
|
378
|
+
map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => rep(e.to))
|
|
379
|
+
),
|
|
380
|
+
usedBy: dedupe(
|
|
381
|
+
map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => rep(e.from))
|
|
382
|
+
)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const node = map.nodes.find((n) => n.id === focusId);
|
|
386
|
+
if (!node) return void 0;
|
|
387
|
+
const ref = (id, edgeLabel) => {
|
|
388
|
+
const n = map.nodes.find((x) => x.id === id);
|
|
389
|
+
return {
|
|
390
|
+
id,
|
|
391
|
+
label: n?.label ?? id,
|
|
392
|
+
status: n?.status ?? "planned",
|
|
393
|
+
...edgeLabel !== void 0 ? { edgeLabel } : {}
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
|
|
397
|
+
return {
|
|
398
|
+
kind: "node",
|
|
399
|
+
node,
|
|
400
|
+
layerName: layerNameOf(node.layer),
|
|
401
|
+
...laneLabel !== void 0 ? { laneLabel } : {},
|
|
402
|
+
uses: map.edges.filter((e) => e.from === node.id).map((e) => ref(e.to, e.label)),
|
|
403
|
+
usedBy: map.edges.filter((e) => e.to === node.id).map((e) => ref(e.from, e.label))
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function interiorPages(pages) {
|
|
407
|
+
const dives = /* @__PURE__ */ new Map();
|
|
408
|
+
const divedIntoBy = /* @__PURE__ */ new Map();
|
|
409
|
+
for (const [slug, map] of pages) {
|
|
410
|
+
const targets = /* @__PURE__ */ new Set();
|
|
411
|
+
for (const n of map?.nodes ?? []) {
|
|
412
|
+
const target = n.submap;
|
|
413
|
+
if (target === void 0 || target === slug) continue;
|
|
414
|
+
targets.add(target);
|
|
415
|
+
const sources = divedIntoBy.get(target) ?? /* @__PURE__ */ new Set();
|
|
416
|
+
sources.add(slug);
|
|
417
|
+
divedIntoBy.set(target, sources);
|
|
418
|
+
}
|
|
419
|
+
if (slug !== void 0) dives.set(slug, targets);
|
|
420
|
+
}
|
|
421
|
+
const reachableFrom = (start) => {
|
|
422
|
+
const seen = /* @__PURE__ */ new Set();
|
|
423
|
+
const pending = [start];
|
|
424
|
+
while (pending.length > 0) {
|
|
425
|
+
for (const target of dives.get(pending.pop()) ?? []) {
|
|
426
|
+
if (seen.has(target)) continue;
|
|
427
|
+
seen.add(target);
|
|
428
|
+
pending.push(target);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return seen;
|
|
432
|
+
};
|
|
433
|
+
const interior = /* @__PURE__ */ new Set();
|
|
434
|
+
for (const [target, sources] of divedIntoBy) {
|
|
435
|
+
const outward = reachableFrom(target);
|
|
436
|
+
for (const source of sources) {
|
|
437
|
+
if (source === void 0 || !outward.has(source)) {
|
|
438
|
+
interior.add(target);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return interior;
|
|
444
|
+
}
|
|
445
|
+
function diveParent(entries, pageId) {
|
|
446
|
+
for (const [key, m] of entries) {
|
|
447
|
+
const node = m?.nodes.find((n) => n.submap === pageId);
|
|
448
|
+
if (node !== void 0) return { parent: key, label: node.label };
|
|
449
|
+
}
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
function mostRecentKey(keys, mtimeOf2) {
|
|
453
|
+
let best;
|
|
454
|
+
let bestMtime = -Infinity;
|
|
455
|
+
for (const key of keys) {
|
|
456
|
+
const mtime = mtimeOf2(key);
|
|
457
|
+
if (mtime !== void 0 && mtime > bestMtime) {
|
|
458
|
+
best = key;
|
|
459
|
+
bestMtime = mtime;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return best ?? keys[0];
|
|
463
|
+
}
|
|
464
|
+
function flipForSequence(map) {
|
|
465
|
+
if (map.kind !== "sequence") return map;
|
|
466
|
+
return {
|
|
467
|
+
...map,
|
|
468
|
+
// VIOLATION: state-explicit-in-types - `-l.rank as Rank` produces a value
|
|
469
|
+
// the Rank brand promises cannot exist: mirroring 0..99 gives -99..0, and
|
|
470
|
+
// makeRank would refuse every one of them. The alternative is a second
|
|
471
|
+
// ordered-position type (an unbranded `order` field) threaded through the
|
|
472
|
+
// renderer's whole layout stage purely so this one derived map can be
|
|
473
|
+
// typed — a large change to express "these ranks are an order, not a
|
|
474
|
+
// stored value". What makes it safe is the same thing that makes it
|
|
475
|
+
// wrong: this map only ever reaches a renderer, which compares ranks and
|
|
476
|
+
// never writes them (same contract as aggregateMap).
|
|
477
|
+
layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
|
|
478
|
+
edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/render/width.ts
|
|
227
483
|
var WIDE_RANGES = [
|
|
228
484
|
[4352, 4447],
|
|
229
485
|
// Hangul Jamo
|
|
486
|
+
// Wide symbols scattered through the BMP — mostly emoji that predate the
|
|
487
|
+
// emoji planes (⌚ ⏰ ⚡ ✅ ✨ ❌ ❓ ⭐ ⬛ …).
|
|
488
|
+
[8986, 8987],
|
|
489
|
+
[9001, 9002],
|
|
490
|
+
[9193, 9196],
|
|
491
|
+
[9200, 9200],
|
|
492
|
+
[9203, 9203],
|
|
493
|
+
[9725, 9726],
|
|
494
|
+
[9748, 9749],
|
|
495
|
+
[9800, 9811],
|
|
496
|
+
[9855, 9855],
|
|
497
|
+
[9875, 9875],
|
|
498
|
+
[9889, 9889],
|
|
499
|
+
[9898, 9899],
|
|
500
|
+
[9917, 9918],
|
|
501
|
+
[9924, 9925],
|
|
502
|
+
[9934, 9934],
|
|
503
|
+
[9940, 9940],
|
|
504
|
+
[9962, 9962],
|
|
505
|
+
[9970, 9971],
|
|
506
|
+
[9973, 9973],
|
|
507
|
+
[9978, 9978],
|
|
508
|
+
[9981, 9981],
|
|
509
|
+
[9989, 9989],
|
|
510
|
+
[9994, 9995],
|
|
511
|
+
[10024, 10024],
|
|
512
|
+
[10060, 10060],
|
|
513
|
+
[10062, 10062],
|
|
514
|
+
[10067, 10069],
|
|
515
|
+
[10071, 10071],
|
|
516
|
+
[10133, 10135],
|
|
517
|
+
[10160, 10160],
|
|
518
|
+
[10175, 10175],
|
|
519
|
+
[11035, 11036],
|
|
520
|
+
[11088, 11088],
|
|
521
|
+
[11093, 11093],
|
|
230
522
|
[11904, 42191],
|
|
231
523
|
// CJK radicals .. Yi (covers CJK Unified Ideographs)
|
|
232
524
|
[43360, 43391],
|
|
@@ -239,14 +531,41 @@ var WIDE_RANGES = [
|
|
|
239
531
|
[65280, 65376],
|
|
240
532
|
// fullwidth forms
|
|
241
533
|
[65504, 65510],
|
|
534
|
+
[127744, 128591],
|
|
535
|
+
// pictographs, transport, emoticons (🚀 🎯 😀 …)
|
|
536
|
+
[128640, 128767],
|
|
537
|
+
[129280, 129535],
|
|
538
|
+
// supplemental symbols (🤖 🧱 …)
|
|
539
|
+
[129648, 129791],
|
|
540
|
+
// symbols extended-A
|
|
242
541
|
[131072, 262141]
|
|
243
542
|
// CJK extension planes
|
|
244
543
|
];
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
544
|
+
var ZERO_WIDTH_RANGES = [
|
|
545
|
+
[768, 879],
|
|
546
|
+
// combining diacritical marks (decomposed 'e' + ´)
|
|
547
|
+
[6832, 6911],
|
|
548
|
+
[7616, 7679],
|
|
549
|
+
[8203, 8207],
|
|
550
|
+
// zero-width space .. RLM, zero-width joiner among them
|
|
551
|
+
[8400, 8432],
|
|
552
|
+
// combining marks for symbols
|
|
553
|
+
[65024, 65039],
|
|
554
|
+
// variation selectors, VS16 (emoji presentation) included
|
|
555
|
+
[65056, 65071],
|
|
556
|
+
// combining half marks
|
|
557
|
+
[127995, 127999]
|
|
558
|
+
// emoji skin tone modifiers — always applied to a base
|
|
559
|
+
];
|
|
560
|
+
function inRanges(cp, ranges) {
|
|
561
|
+
for (const [lo, hi] of ranges) {
|
|
562
|
+
if (cp >= lo && cp <= hi) return true;
|
|
248
563
|
}
|
|
249
|
-
return
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
function charWidth(cp) {
|
|
567
|
+
if (inRanges(cp, ZERO_WIDTH_RANGES)) return 0;
|
|
568
|
+
return inRanges(cp, WIDE_RANGES) ? 2 : 1;
|
|
250
569
|
}
|
|
251
570
|
function displayWidth(text) {
|
|
252
571
|
let w = 0;
|
|
@@ -288,6 +607,19 @@ function wrapWidth(s, width) {
|
|
|
288
607
|
if (line !== "") lines.push(line);
|
|
289
608
|
return lines;
|
|
290
609
|
}
|
|
610
|
+
|
|
611
|
+
// src/render/canvas.ts
|
|
612
|
+
var SGR = {
|
|
613
|
+
none: "",
|
|
614
|
+
dim: "2",
|
|
615
|
+
amber: "33",
|
|
616
|
+
green: "32",
|
|
617
|
+
greenDim: "32;2",
|
|
618
|
+
// done, but nothing behind the claim: green, not fully lit
|
|
619
|
+
red: "31",
|
|
620
|
+
faint: "90"
|
|
621
|
+
};
|
|
622
|
+
var ANSI_RESET = "\x1B[0m";
|
|
291
623
|
var UP = 1;
|
|
292
624
|
var DOWN = 2;
|
|
293
625
|
var LEFT = 4;
|
|
@@ -322,15 +654,6 @@ function maskChar(mask, heavyHorizontal, unicode) {
|
|
|
322
654
|
}
|
|
323
655
|
return LIGHT_BY_MASK[mask] ?? "\u253C";
|
|
324
656
|
}
|
|
325
|
-
var SGR = {
|
|
326
|
-
none: "",
|
|
327
|
-
dim: "2",
|
|
328
|
-
amber: "33",
|
|
329
|
-
green: "32",
|
|
330
|
-
red: "31",
|
|
331
|
-
faint: "90"
|
|
332
|
-
};
|
|
333
|
-
var ANSI_RESET = "\x1B[0m";
|
|
334
657
|
var BORDER_JUNCTION = {
|
|
335
658
|
"\u2500": { down: "\u252C", up: "\u2534" },
|
|
336
659
|
"\u254C": { down: "\u252C", up: "\u2534" },
|
|
@@ -356,11 +679,17 @@ var Canvas = class {
|
|
|
356
679
|
text(x, y, s, style, bold = false) {
|
|
357
680
|
let cx = x;
|
|
358
681
|
for (const ch of s) {
|
|
682
|
+
const w = charWidth(ch.codePointAt(0));
|
|
683
|
+
if (w === 0) {
|
|
684
|
+
const base = this.cell(Math.max(0, cx - 1), y);
|
|
685
|
+
const target = base.literal === "" ? this.cell(Math.max(0, cx - 2), y) : base;
|
|
686
|
+
target.literal = (target.literal ?? "") + ch;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
359
689
|
const c = this.cell(cx, y);
|
|
360
690
|
c.literal = ch;
|
|
361
691
|
c.style = style;
|
|
362
692
|
c.bold = bold;
|
|
363
|
-
const w = charWidth(ch.codePointAt(0));
|
|
364
693
|
if (w === 2) {
|
|
365
694
|
const phantom = this.cell(cx + 1, y);
|
|
366
695
|
phantom.literal = "";
|
|
@@ -439,280 +768,69 @@ function drawPath(canvas, points, bright = false) {
|
|
|
439
768
|
}
|
|
440
769
|
}
|
|
441
770
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
function skinFor(status, unicode) {
|
|
445
|
-
const style = status === "planned" ? "dim" : status === "in-progress" ? "amber" : status === "done" ? "green" : "red";
|
|
446
|
-
if (!unicode) {
|
|
447
|
-
return status === "planned" ? { h: ".", v: ":", corners: ["+", "+", "+", "+"], style } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style };
|
|
448
|
-
}
|
|
449
|
-
switch (status) {
|
|
450
|
-
case "planned":
|
|
451
|
-
return { h: "\u254C", v: "\u254E", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
|
|
452
|
-
case "in-progress":
|
|
453
|
-
return { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
|
|
454
|
-
case "done":
|
|
455
|
-
case "regressed":
|
|
456
|
-
return { h: "\u2501", v: "\u2503", corners: ["\u250F", "\u2513", "\u2517", "\u251B"], style };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
function glyphFor(status, opts) {
|
|
460
|
-
const spinner = opts.unicode ? SPINNER_UNICODE : SPINNER_ASCII;
|
|
461
|
-
switch (status) {
|
|
462
|
-
case "planned":
|
|
463
|
-
return opts.unicode ? "\xB7" : ".";
|
|
464
|
-
case "in-progress":
|
|
465
|
-
return spinner[opts.spinnerFrame % spinner.length];
|
|
466
|
-
case "done":
|
|
467
|
-
return opts.unicode ? "\u25A0" : "#";
|
|
468
|
-
case "regressed":
|
|
469
|
-
return opts.unicode ? "\u2717" : "X";
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
var NODE_KIND_GLYPHS = {
|
|
473
|
-
selector: ["?", "?"],
|
|
474
|
-
sequence: ["\xBB", ">"],
|
|
475
|
-
parallel: ["\u2016", "="],
|
|
476
|
-
decorator: ["\u25CC", "o"],
|
|
477
|
-
condition: ["\u25C7", "c"],
|
|
478
|
-
action: ["\xB7", "."],
|
|
479
|
-
source: ["\u25CB", "o"],
|
|
480
|
-
transform: ["\u25D0", "%"],
|
|
481
|
-
sink: ["\u25CF", "*"],
|
|
482
|
-
service: ["\u25C6", "S"],
|
|
483
|
-
db: ["\u25A4", "D"],
|
|
484
|
-
queue: ["\u2263", "Q"],
|
|
485
|
-
ui: ["\u25A3", "U"]
|
|
486
|
-
};
|
|
487
|
-
function kindGlyph(kind, unicode) {
|
|
488
|
-
const pair = NODE_KIND_GLYPHS[kind];
|
|
489
|
-
return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
|
|
490
|
-
}
|
|
491
|
-
function isNeutralKind(map) {
|
|
492
|
-
return map.kind !== void 0 && map.kind !== "dev";
|
|
493
|
-
}
|
|
494
|
-
function neutralSkin(unicode) {
|
|
495
|
-
return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
|
|
496
|
-
}
|
|
771
|
+
|
|
772
|
+
// src/render/zoom-geometry.ts
|
|
497
773
|
var BOX_H = 3;
|
|
498
774
|
var BOX_GAP = 2;
|
|
499
775
|
var LEFT_MARGIN = 2;
|
|
776
|
+
var BAR_MIN_RUN = 7;
|
|
500
777
|
var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
|
|
501
778
|
var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
|
|
502
779
|
function zoomGeometry(zoom) {
|
|
780
|
+
const m = zoomMode(zoom);
|
|
781
|
+
const mode = m === "overview" ? "constellation" : m;
|
|
503
782
|
switch (zoom) {
|
|
504
783
|
case 2:
|
|
505
|
-
return { mode
|
|
784
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
|
|
506
785
|
case 1:
|
|
507
|
-
return { mode
|
|
786
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
|
|
508
787
|
case 0:
|
|
509
|
-
return { mode
|
|
788
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
510
789
|
case -1:
|
|
511
|
-
return { mode
|
|
790
|
+
return { mode, scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
512
791
|
case -2:
|
|
513
|
-
return { mode
|
|
792
|
+
return { mode, scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
|
|
514
793
|
case -3:
|
|
515
|
-
return { mode
|
|
794
|
+
return { mode, scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
516
795
|
case -4:
|
|
517
|
-
return { mode
|
|
796
|
+
return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
518
797
|
}
|
|
519
798
|
}
|
|
520
|
-
var
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
|
|
538
|
-
extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
799
|
+
var AGGREGATE_GEO = {
|
|
800
|
+
mode: "boxes",
|
|
801
|
+
scale: 1,
|
|
802
|
+
pad: 0,
|
|
803
|
+
boxGap: 1,
|
|
804
|
+
breathe: 0,
|
|
805
|
+
titleGap: 0,
|
|
806
|
+
barGap: 1,
|
|
807
|
+
bandCounts: false
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/render/routing.ts
|
|
811
|
+
function routeEdges(map, columns) {
|
|
812
|
+
const { bandIndexOf, bandBoxes, boxOf, contentWidth } = columns;
|
|
813
|
+
const pending = map.edges.map((e) => {
|
|
814
|
+
const from = boxOf.get(e.from);
|
|
815
|
+
const to = boxOf.get(e.to);
|
|
541
816
|
return {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
borderless: false,
|
|
547
|
-
extra
|
|
817
|
+
from,
|
|
818
|
+
to,
|
|
819
|
+
fromBand: bandIndexOf.get(from.node.layer),
|
|
820
|
+
toBand: bandIndexOf.get(to.node.layer)
|
|
548
821
|
};
|
|
549
|
-
}
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
borderless: false,
|
|
558
|
-
extra: []
|
|
822
|
+
});
|
|
823
|
+
const gapVerticals = Array.from(
|
|
824
|
+
{ length: Math.max(0, columns.bands.length - 1) },
|
|
825
|
+
() => /* @__PURE__ */ new Map()
|
|
826
|
+
);
|
|
827
|
+
const verticalFree = (gap, x, edge) => {
|
|
828
|
+
const owner = gapVerticals[gap]?.get(x);
|
|
829
|
+
return owner === void 0 || owner === edge;
|
|
559
830
|
};
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const built = buildCanvas(map, opts);
|
|
563
|
-
return {
|
|
564
|
-
lines: built.canvas.emit(opts, viewport),
|
|
565
|
-
contentWidth: built.canvas.width,
|
|
566
|
-
contentHeight: built.canvas.height,
|
|
567
|
-
hits: built.hits
|
|
831
|
+
const takeVertical = (gap, x, edge) => {
|
|
832
|
+
gapVerticals[gap]?.set(x, edge);
|
|
568
833
|
};
|
|
569
|
-
}
|
|
570
|
-
function aggregateMap(map) {
|
|
571
|
-
if (map.groups.length === 0) return void 0;
|
|
572
|
-
const representative = /* @__PURE__ */ new Map();
|
|
573
|
-
for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
|
|
574
|
-
const nodes = map.groups.map((g) => {
|
|
575
|
-
const members = map.nodes.filter((n) => n.group === g.id);
|
|
576
|
-
const done = members.filter((n) => n.status === "done").length;
|
|
577
|
-
return {
|
|
578
|
-
id: g.id,
|
|
579
|
-
// neutral kinds document structure, not progress — no member counts
|
|
580
|
-
label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
|
|
581
|
-
layer: g.layer,
|
|
582
|
-
status: groupStatus(map, g.id)
|
|
583
|
-
};
|
|
584
|
-
});
|
|
585
|
-
for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
|
|
586
|
-
const seen = /* @__PURE__ */ new Set();
|
|
587
|
-
const edges = [];
|
|
588
|
-
for (const e of map.edges) {
|
|
589
|
-
const from = representative.get(e.from);
|
|
590
|
-
const to = representative.get(e.to);
|
|
591
|
-
if (from === to || seen.has(`${from}->${to}`)) continue;
|
|
592
|
-
seen.add(`${from}->${to}`);
|
|
593
|
-
edges.push({ from, to });
|
|
594
|
-
}
|
|
595
|
-
return {
|
|
596
|
-
...map.title !== void 0 ? { title: map.title } : {},
|
|
597
|
-
...map.kind !== void 0 ? { kind: map.kind } : {},
|
|
598
|
-
layers: map.layers,
|
|
599
|
-
groups: [],
|
|
600
|
-
lanes: map.lanes,
|
|
601
|
-
nodes,
|
|
602
|
-
edges
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
var AGGREGATE_GEO = {
|
|
606
|
-
mode: "boxes",
|
|
607
|
-
scale: 1,
|
|
608
|
-
pad: 0,
|
|
609
|
-
boxGap: 1,
|
|
610
|
-
breathe: 0,
|
|
611
|
-
titleGap: 0,
|
|
612
|
-
barGap: 1,
|
|
613
|
-
bandCounts: false
|
|
614
|
-
};
|
|
615
|
-
function flipForSequence(map) {
|
|
616
|
-
if (map.kind !== "sequence") return map;
|
|
617
|
-
return {
|
|
618
|
-
...map,
|
|
619
|
-
layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
|
|
620
|
-
edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
function buildCanvas(map, opts) {
|
|
624
|
-
const oriented = flipForSequence(map);
|
|
625
|
-
const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
|
|
626
|
-
const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
|
|
627
|
-
return buildCanvasWith(aggregated ?? oriented, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo);
|
|
628
|
-
}
|
|
629
|
-
function buildCanvasWith(map, opts, geo) {
|
|
630
|
-
const canvas = new Canvas();
|
|
631
|
-
const neutral = isNeutralKind(map);
|
|
632
|
-
const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
|
|
633
|
-
if (bands.length === 0) {
|
|
634
|
-
canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
|
|
635
|
-
canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
|
|
636
|
-
return { canvas, hits: [] };
|
|
637
|
-
}
|
|
638
|
-
const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
|
|
639
|
-
const boxes = /* @__PURE__ */ new Map();
|
|
640
|
-
const bandBoxes = bands.map(() => []);
|
|
641
|
-
for (const node of map.nodes) {
|
|
642
|
-
const band = bandIndexOf.get(node.layer);
|
|
643
|
-
const box = { node, ...boxSpec(node, geo, opts.unicode, neutral), x: LEFT_MARGIN, y: 0 };
|
|
644
|
-
bandBoxes[band].push(box);
|
|
645
|
-
boxes.set(node.id, box);
|
|
646
|
-
}
|
|
647
|
-
const laneCount = map.lanes.length;
|
|
648
|
-
const laneX = [];
|
|
649
|
-
const laneW = [];
|
|
650
|
-
if (laneCount === 0) {
|
|
651
|
-
for (const row of bandBoxes) {
|
|
652
|
-
let x = LEFT_MARGIN;
|
|
653
|
-
for (const box of row) {
|
|
654
|
-
box.x = x;
|
|
655
|
-
x += box.w + geo.boxGap;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
} else {
|
|
659
|
-
const laneGap = geo.boxGap + 2;
|
|
660
|
-
const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
|
|
661
|
-
const regions = laneCount + 1;
|
|
662
|
-
const grouped = bandBoxes.map((row) => {
|
|
663
|
-
const cells = Array.from({ length: regions }, () => []);
|
|
664
|
-
for (const box of row) {
|
|
665
|
-
const lane = box.node.lane;
|
|
666
|
-
cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(box);
|
|
667
|
-
}
|
|
668
|
-
return cells;
|
|
669
|
-
});
|
|
670
|
-
const regionW = Array.from({ length: regions }, () => 0);
|
|
671
|
-
for (const cells of grouped) {
|
|
672
|
-
for (let i = 0; i < regions; i++) {
|
|
673
|
-
const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
|
|
674
|
-
regionW[i] = Math.max(regionW[i], rowW);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
|
|
678
|
-
let x0 = LEFT_MARGIN;
|
|
679
|
-
for (let i = 0; i < regions; i++) {
|
|
680
|
-
laneX.push(x0);
|
|
681
|
-
laneW.push(regionW[i]);
|
|
682
|
-
x0 += regionW[i] + laneGap;
|
|
683
|
-
}
|
|
684
|
-
for (const cells of grouped) {
|
|
685
|
-
for (let i = 0; i < regions; i++) {
|
|
686
|
-
let x = laneX[i];
|
|
687
|
-
for (const box of cells[i]) {
|
|
688
|
-
box.x = x;
|
|
689
|
-
x += box.w + geo.boxGap;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
const bandLabel = bands.map((l, i) => {
|
|
695
|
-
const row = bandBoxes[i];
|
|
696
|
-
const done = row.filter((b) => b.node.status === "done").length;
|
|
697
|
-
return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
|
|
698
|
-
});
|
|
699
|
-
let contentWidth = LEFT_MARGIN;
|
|
700
|
-
for (const row of bandBoxes) {
|
|
701
|
-
const last = row[row.length - 1];
|
|
702
|
-
if (last) contentWidth = Math.max(contentWidth, last.x + last.w);
|
|
703
|
-
}
|
|
704
|
-
for (let i = 0; i < laneCount; i++) contentWidth = Math.max(contentWidth, laneX[i] + laneW[i]);
|
|
705
|
-
for (const label of bandLabel) contentWidth = Math.max(contentWidth, LEFT_MARGIN + displayWidth(label) + 7);
|
|
706
|
-
const routes = map.edges.map((e) => {
|
|
707
|
-
const fromBox = boxes.get(e.from);
|
|
708
|
-
const toBox = boxes.get(e.to);
|
|
709
|
-
return {
|
|
710
|
-
fromBox,
|
|
711
|
-
toBox,
|
|
712
|
-
fromBand: bandIndexOf.get(fromBox.node.layer),
|
|
713
|
-
toBand: bandIndexOf.get(toBox.node.layer)
|
|
714
|
-
};
|
|
715
|
-
});
|
|
716
834
|
const claimedColumns = /* @__PURE__ */ new Map();
|
|
717
835
|
const isFree = (box, x) => !(claimedColumns.get(box)?.has(x) ?? false);
|
|
718
836
|
const claim = (box, x) => {
|
|
@@ -721,60 +839,65 @@ function buildCanvasWith(map, opts, geo) {
|
|
|
721
839
|
set.add(x);
|
|
722
840
|
return x;
|
|
723
841
|
};
|
|
724
|
-
const
|
|
725
|
-
for (const r of routes) {
|
|
842
|
+
for (const r of pending) {
|
|
726
843
|
if (r.toBand - r.fromBand !== 1) continue;
|
|
727
|
-
const lo = Math.max(r.
|
|
728
|
-
const hi = Math.min(r.
|
|
844
|
+
const lo = Math.max(r.from.x + 1, r.to.x + 1);
|
|
845
|
+
const hi = Math.min(r.from.x + r.from.w - 2, r.to.x + r.to.w - 2);
|
|
729
846
|
if (lo > hi) continue;
|
|
730
847
|
const mid = Math.floor((lo + hi) / 2);
|
|
731
|
-
for (let d = 0; d <= hi - lo &&
|
|
848
|
+
for (let d = 0; d <= hi - lo && r.straightX === void 0; d++) {
|
|
732
849
|
for (const x of d === 0 ? [mid] : [mid - d, mid + d]) {
|
|
733
|
-
if (x >= lo && x <= hi && isFree(r.
|
|
734
|
-
straightX
|
|
850
|
+
if (x >= lo && x <= hi && isFree(r.from, x) && isFree(r.to, x) && verticalFree(r.fromBand, x, r)) {
|
|
851
|
+
r.straightX = claim(r.to, claim(r.from, x));
|
|
852
|
+
takeVertical(r.fromBand, x, r);
|
|
735
853
|
break;
|
|
736
854
|
}
|
|
737
855
|
}
|
|
738
856
|
}
|
|
739
857
|
}
|
|
740
|
-
const bent =
|
|
858
|
+
const bent = pending.filter((r) => r.straightX === void 0);
|
|
741
859
|
const outgoing = /* @__PURE__ */ new Map();
|
|
742
860
|
const incoming = /* @__PURE__ */ new Map();
|
|
743
861
|
for (const r of bent) {
|
|
744
|
-
outgoing.set(r.
|
|
745
|
-
incoming.set(r.
|
|
862
|
+
outgoing.set(r.from, [...outgoing.get(r.from) ?? [], r]);
|
|
863
|
+
incoming.set(r.to, [...incoming.get(r.to) ?? [], r]);
|
|
746
864
|
}
|
|
747
|
-
const freeSlot = (box, k, n) => {
|
|
865
|
+
const freeSlot = (box, k, n, edge, gap) => {
|
|
748
866
|
const lo = box.x + 1;
|
|
749
867
|
const hi = box.x + box.w - 2;
|
|
750
868
|
const ideal = box.x + Math.min(box.w - 2, Math.max(1, Math.round((k + 1) * (box.w - 1) / (n + 1))));
|
|
751
869
|
for (let d = 0; d <= hi - lo; d++) {
|
|
752
870
|
for (const x of d === 0 ? [ideal] : [ideal - d, ideal + d]) {
|
|
753
|
-
if (x >= lo && x <= hi && isFree(box, x)
|
|
871
|
+
if (x >= lo && x <= hi && isFree(box, x) && verticalFree(gap, x, edge)) {
|
|
872
|
+
takeVertical(gap, x, edge);
|
|
873
|
+
return claim(box, x);
|
|
874
|
+
}
|
|
754
875
|
}
|
|
755
876
|
}
|
|
756
877
|
return ideal;
|
|
757
878
|
};
|
|
758
|
-
const attach = /* @__PURE__ */ new Map();
|
|
759
879
|
for (const r of bent) {
|
|
760
|
-
const outs = outgoing.get(r.
|
|
761
|
-
const ins = incoming.get(r.
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
ex: freeSlot(r.toBox, ins.indexOf(r), ins.length)
|
|
765
|
-
});
|
|
880
|
+
const outs = outgoing.get(r.from);
|
|
881
|
+
const ins = incoming.get(r.to);
|
|
882
|
+
r.exitX = freeSlot(r.from, outs.indexOf(r), outs.length, r, r.fromBand);
|
|
883
|
+
r.entryX = freeSlot(r.to, ins.indexOf(r), ins.length, r, r.toBand - 1);
|
|
766
884
|
}
|
|
767
|
-
const skipRoutes = bent.filter((r) => r.toBand - r.fromBand > 1);
|
|
768
885
|
const usedDescent = /* @__PURE__ */ new Set();
|
|
769
|
-
const descentX = /* @__PURE__ */ new Map();
|
|
770
886
|
let fallbackCount = 0;
|
|
771
887
|
const blockedByBox = (band, x) => bandBoxes[band].some((b) => x >= b.x && x <= b.x + b.w - 1);
|
|
772
|
-
|
|
773
|
-
|
|
888
|
+
const descentGapsFree = (r, c) => {
|
|
889
|
+
for (let g = r.fromBand; g <= r.toBand - 1; g++) {
|
|
890
|
+
if (!verticalFree(g, c, r)) return false;
|
|
891
|
+
}
|
|
892
|
+
return true;
|
|
893
|
+
};
|
|
894
|
+
for (const r of bent.filter((e) => e.toBand - e.fromBand > 1)) {
|
|
895
|
+
const ex = r.entryX;
|
|
774
896
|
let chosen;
|
|
775
897
|
for (let d = 0; d <= contentWidth && chosen === void 0; d++) {
|
|
776
898
|
for (const c of d === 0 ? [ex] : [ex - d, ex + d]) {
|
|
777
899
|
if (c < LEFT_MARGIN || c > contentWidth + 1 || usedDescent.has(c)) continue;
|
|
900
|
+
if (!descentGapsFree(r, c)) continue;
|
|
778
901
|
let blocked = false;
|
|
779
902
|
for (let b = r.fromBand + 1; b < r.toBand && !blocked; b++) blocked = blockedByBox(b, c);
|
|
780
903
|
if (!blocked) {
|
|
@@ -785,167 +908,185 @@ function buildCanvasWith(map, opts, geo) {
|
|
|
785
908
|
}
|
|
786
909
|
if (chosen === void 0) chosen = contentWidth + 2 + fallbackCount++ * 2;
|
|
787
910
|
usedDescent.add(chosen);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const gapCount = bands.length - 1;
|
|
792
|
-
const gapSegments = Array.from(
|
|
793
|
-
|
|
911
|
+
for (let g = r.fromBand; g <= r.toBand - 1; g++) takeVertical(g, chosen, r);
|
|
912
|
+
r.descentX = chosen;
|
|
913
|
+
}
|
|
914
|
+
const gapCount = Math.max(0, columns.bands.length - 1);
|
|
915
|
+
const gapSegments = Array.from(
|
|
916
|
+
{ length: gapCount },
|
|
917
|
+
() => []
|
|
918
|
+
);
|
|
794
919
|
for (const r of bent) {
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
gapSegments[r.toBand - 1].push(
|
|
799
|
-
|
|
920
|
+
const sx = r.exitX;
|
|
921
|
+
const ex = r.entryX;
|
|
922
|
+
if (r.descentX === void 0) {
|
|
923
|
+
gapSegments[r.toBand - 1].push({
|
|
924
|
+
edge: r,
|
|
925
|
+
kind: "landing",
|
|
926
|
+
segment: { lo: Math.min(sx, ex), hi: Math.max(sx, ex) }
|
|
927
|
+
});
|
|
800
928
|
} else {
|
|
801
|
-
const c = descentX
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
929
|
+
const c = r.descentX;
|
|
930
|
+
gapSegments[r.fromBand].push({ edge: r, kind: "exit", segment: { lo: Math.min(sx, c), hi: Math.max(sx, c) } });
|
|
931
|
+
gapSegments[r.toBand - 1].push({
|
|
932
|
+
edge: r,
|
|
933
|
+
kind: "landing",
|
|
934
|
+
segment: { lo: Math.min(c, ex), hi: Math.max(c, ex) }
|
|
935
|
+
});
|
|
807
936
|
}
|
|
808
937
|
}
|
|
809
|
-
const
|
|
810
|
-
const
|
|
938
|
+
const exitRow = /* @__PURE__ */ new Map();
|
|
939
|
+
const landingRow = /* @__PURE__ */ new Map();
|
|
940
|
+
const gapRowCount = gapSegments.map((entries) => {
|
|
811
941
|
const rowEnds = [];
|
|
812
|
-
for (const
|
|
813
|
-
let row = rowEnds.findIndex((end) =>
|
|
942
|
+
for (const e of [...entries].sort((a, b) => a.segment.lo - b.segment.lo)) {
|
|
943
|
+
let row = rowEnds.findIndex((end) => e.segment.lo > end + 1);
|
|
814
944
|
if (row === -1) {
|
|
815
|
-
rowEnds.push(
|
|
945
|
+
rowEnds.push(e.segment.hi);
|
|
816
946
|
row = rowEnds.length - 1;
|
|
817
947
|
} else {
|
|
818
|
-
rowEnds[row] = Math.max(rowEnds[row],
|
|
948
|
+
rowEnds[row] = Math.max(rowEnds[row], e.segment.hi);
|
|
819
949
|
}
|
|
820
|
-
|
|
950
|
+
(e.kind === "exit" ? exitRow : landingRow).set(e.edge, row);
|
|
821
951
|
}
|
|
822
952
|
return rowEnds.length;
|
|
823
953
|
});
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
y += 1 + geo.barGap;
|
|
830
|
-
}
|
|
831
|
-
const barY = [];
|
|
832
|
-
const gapTrackStartY = [];
|
|
833
|
-
for (let b = 0; b < bands.length; b++) {
|
|
834
|
-
barY.push(y);
|
|
835
|
-
y += 1 + geo.barGap;
|
|
836
|
-
const row = bandBoxes[b];
|
|
837
|
-
for (const box of row) box.y = y;
|
|
838
|
-
y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
|
|
839
|
-
if (b < gapCount) {
|
|
840
|
-
y += geo.breathe;
|
|
841
|
-
gapTrackStartY.push(y);
|
|
842
|
-
y += gapRowCount[b];
|
|
843
|
-
y += geo.breathe;
|
|
954
|
+
const edges = pending.map((r) => {
|
|
955
|
+
const common = { from: r.from, to: r.to, fromBand: r.fromBand, toBand: r.toBand };
|
|
956
|
+
if (r.straightX !== void 0) return { ...common, kind: "straight", x: r.straightX };
|
|
957
|
+
if (r.descentX === void 0) {
|
|
958
|
+
return { ...common, kind: "dogleg", exitX: r.exitX, entryX: r.entryX, landingRow: landingRow.get(r) };
|
|
844
959
|
}
|
|
960
|
+
return {
|
|
961
|
+
...common,
|
|
962
|
+
kind: "thread",
|
|
963
|
+
exitX: r.exitX,
|
|
964
|
+
entryX: r.entryX,
|
|
965
|
+
descentX: r.descentX,
|
|
966
|
+
exitRow: exitRow.get(r),
|
|
967
|
+
landingRow: landingRow.get(r)
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
return { edges, gapRowCount, fallbackCount };
|
|
971
|
+
}
|
|
972
|
+
function edgePolyline(edge, rows) {
|
|
973
|
+
const from = rows.boxOf.get(edge.from.node.id);
|
|
974
|
+
const to = rows.boxOf.get(edge.to.node.id);
|
|
975
|
+
const sy = from.y + from.h - 1;
|
|
976
|
+
const ey = to.y;
|
|
977
|
+
if (edge.kind === "straight") {
|
|
978
|
+
return [
|
|
979
|
+
[edge.x, sy],
|
|
980
|
+
[edge.x, ey]
|
|
981
|
+
];
|
|
845
982
|
}
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
983
|
+
const landingY = rows.gapTrackStartY[edge.toBand - 1] + edge.landingRow;
|
|
984
|
+
if (edge.kind === "dogleg") {
|
|
985
|
+
return [
|
|
986
|
+
[edge.exitX, sy],
|
|
987
|
+
[edge.exitX, landingY],
|
|
988
|
+
[edge.entryX, landingY],
|
|
989
|
+
[edge.entryX, ey]
|
|
990
|
+
];
|
|
855
991
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
}
|
|
881
|
-
const { sx, ex } = attach.get(r);
|
|
882
|
-
const segments = segmentOf.get(r);
|
|
883
|
-
const landingY = rowYOf(r.toBand - 1, segments.landing);
|
|
884
|
-
if (r.toBand - r.fromBand === 1) {
|
|
885
|
-
drawPath(
|
|
886
|
-
canvas,
|
|
887
|
-
[
|
|
888
|
-
[sx, sy],
|
|
889
|
-
[sx, landingY],
|
|
890
|
-
[ex, landingY],
|
|
891
|
-
[ex, ey]
|
|
892
|
-
],
|
|
893
|
-
bright
|
|
894
|
-
);
|
|
895
|
-
} else {
|
|
896
|
-
const c = descentX.get(r);
|
|
897
|
-
const exitY = rowYOf(r.fromBand, segments.exit);
|
|
898
|
-
drawPath(
|
|
899
|
-
canvas,
|
|
900
|
-
[
|
|
901
|
-
[sx, sy],
|
|
902
|
-
[sx, exitY],
|
|
903
|
-
[c, exitY],
|
|
904
|
-
[c, landingY],
|
|
905
|
-
[ex, landingY],
|
|
906
|
-
[ex, ey]
|
|
907
|
-
],
|
|
908
|
-
bright
|
|
909
|
-
);
|
|
910
|
-
}
|
|
992
|
+
const exitY = rows.gapTrackStartY[edge.fromBand] + edge.exitRow;
|
|
993
|
+
return [
|
|
994
|
+
[edge.exitX, sy],
|
|
995
|
+
[edge.exitX, exitY],
|
|
996
|
+
[edge.descentX, exitY],
|
|
997
|
+
[edge.descentX, landingY],
|
|
998
|
+
[edge.entryX, landingY],
|
|
999
|
+
[edge.entryX, ey]
|
|
1000
|
+
];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/render/skins.ts
|
|
1004
|
+
function styleFor(face) {
|
|
1005
|
+
switch (face) {
|
|
1006
|
+
case "planned":
|
|
1007
|
+
return "dim";
|
|
1008
|
+
case "in-progress":
|
|
1009
|
+
return "amber";
|
|
1010
|
+
case "done":
|
|
1011
|
+
return "green";
|
|
1012
|
+
case "done-unverified":
|
|
1013
|
+
return "greenDim";
|
|
1014
|
+
case "regressed":
|
|
1015
|
+
return "red";
|
|
911
1016
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1017
|
+
}
|
|
1018
|
+
function statusSgr(status) {
|
|
1019
|
+
return SGR[styleFor(status)];
|
|
1020
|
+
}
|
|
1021
|
+
function skinFor(face, unicode) {
|
|
1022
|
+
const style = styleFor(face);
|
|
1023
|
+
if (!unicode) {
|
|
1024
|
+
return face === "planned" ? { h: ".", v: ":", corners: ["+", "+", "+", "+"], style } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style };
|
|
1025
|
+
}
|
|
1026
|
+
switch (face) {
|
|
1027
|
+
case "planned":
|
|
1028
|
+
return { h: "\u254C", v: "\u254E", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
|
|
1029
|
+
case "in-progress":
|
|
1030
|
+
return { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
|
|
1031
|
+
// an unverified done keeps the heavy border of done — it is the same
|
|
1032
|
+
// claim, told with a hollow glyph and a dimmer green
|
|
1033
|
+
case "done":
|
|
1034
|
+
case "done-unverified":
|
|
1035
|
+
case "regressed":
|
|
1036
|
+
return { h: "\u2501", v: "\u2503", corners: ["\u250F", "\u2513", "\u2517", "\u251B"], style };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function glyphFor(face, opts) {
|
|
1040
|
+
if (face === "in-progress") return spinnerGlyph(opts.spinnerFrame, opts.unicode);
|
|
1041
|
+
return face === "done-unverified" ? unverifiedDoneGlyph(opts.unicode) : statusGlyph(face, opts.unicode);
|
|
1042
|
+
}
|
|
1043
|
+
function neutralSkin(unicode) {
|
|
1044
|
+
return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
|
|
1045
|
+
}
|
|
1046
|
+
function neutralGlyph(node, unicode) {
|
|
1047
|
+
return (node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0) ?? (unicode ? "\xB7" : ".");
|
|
1048
|
+
}
|
|
1049
|
+
function unverifiedDoneIds(declared, drawn) {
|
|
1050
|
+
const out = /* @__PURE__ */ new Set();
|
|
1051
|
+
const declaredById = new Map(declared.nodes.map((n) => [n.id, n]));
|
|
1052
|
+
for (const node of drawn.nodes) {
|
|
1053
|
+
if (node.status !== "done") continue;
|
|
1054
|
+
const own = declaredById.get(node.id);
|
|
1055
|
+
if (own !== void 0) {
|
|
1056
|
+
if (own.evidence === void 0) out.add(node.id);
|
|
1057
|
+
} else if (declared.nodes.some(
|
|
1058
|
+
(m) => m.group === node.id && m.status === "done" && m.evidence === void 0
|
|
1059
|
+
)) {
|
|
1060
|
+
out.add(node.id);
|
|
934
1061
|
}
|
|
935
1062
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1063
|
+
return out;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/render/draw.ts
|
|
1067
|
+
function drawTitle(canvas, title) {
|
|
1068
|
+
canvas.text(LEFT_MARGIN, 0, title, "none", true);
|
|
1069
|
+
}
|
|
1070
|
+
function drawLaneHeaders(canvas, map, columns, rows) {
|
|
1071
|
+
if (rows.laneHeaderY === void 0) return;
|
|
1072
|
+
for (let i = 0; i < map.lanes.length; i++) {
|
|
1073
|
+
const region = columns.lanes[i];
|
|
1074
|
+
const label = fitWidth(map.lanes[i].label, region.w);
|
|
1075
|
+
const cx = region.x + Math.max(0, Math.floor((region.w - displayWidth(label)) / 2));
|
|
1076
|
+
canvas.text(cx, rows.laneHeaderY, label, "faint", true);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function drawBands(canvas, columns, rows, wiredWidth, totalWidth) {
|
|
1080
|
+
for (let b = 0; b < columns.bands.length; b++) {
|
|
1081
|
+
const label = columns.bandLabel[b];
|
|
1082
|
+
for (let x = 0; x < wiredWidth; x++) canvas.line(x, rows.barY[b], LEFT | RIGHT, true);
|
|
1083
|
+
canvas.text(totalWidth - displayWidth(label), rows.barY[b], label, "none", true);
|
|
1084
|
+
}
|
|
944
1085
|
}
|
|
945
|
-
function drawBox(canvas, box, opts, neutral, focused = false) {
|
|
1086
|
+
function drawBox(canvas, box, opts, neutral, face, focused = false) {
|
|
946
1087
|
const { node, x, y, w } = box;
|
|
947
|
-
const skin = neutral ? neutralSkin(opts.unicode) : skinFor(
|
|
948
|
-
const slotGlyph = neutral ? (node
|
|
1088
|
+
const skin = neutral ? neutralSkin(opts.unicode) : skinFor(face, opts.unicode);
|
|
1089
|
+
const slotGlyph = neutral ? neutralGlyph(node, opts.unicode) : glyphFor(face, opts);
|
|
949
1090
|
if (box.borderless) {
|
|
950
1091
|
canvas.text(x + 1, y, slotGlyph, skin.style, true);
|
|
951
1092
|
return;
|
|
@@ -965,65 +1106,239 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
|
|
|
965
1106
|
}
|
|
966
1107
|
canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], skin.style, focused);
|
|
967
1108
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
|
|
974
|
-
var PAGES_DIR_NAME = "mellos-mapping.pages";
|
|
975
|
-
function makePageId(raw) {
|
|
976
|
-
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
977
|
-
}
|
|
978
|
-
function pageFilePath(defaultFile, page) {
|
|
979
|
-
return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
980
|
-
}
|
|
981
|
-
function pageIdOfFile(defaultFile, path) {
|
|
982
|
-
if (path === defaultFile) return void 0;
|
|
983
|
-
const name = basename(path);
|
|
984
|
-
return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
|
|
1109
|
+
function drawEdges(canvas, edges, rows, opts) {
|
|
1110
|
+
for (const edge of edges) {
|
|
1111
|
+
const bright = opts.focus !== void 0 && (edge.from.node.id === opts.focus || edge.to.node.id === opts.focus);
|
|
1112
|
+
drawPath(canvas, edgePolyline(edge, rows), bright);
|
|
1113
|
+
}
|
|
985
1114
|
}
|
|
986
|
-
function
|
|
987
|
-
|
|
988
|
-
if (
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1115
|
+
function drawLegend(canvas, map, opts, legendY, neutral, anyUnverified) {
|
|
1116
|
+
let lx = LEFT_MARGIN;
|
|
1117
|
+
if (neutral) {
|
|
1118
|
+
lx = canvas.text(lx, legendY, map.kind, "faint");
|
|
1119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1120
|
+
for (const n of map.nodes) {
|
|
1121
|
+
const k = n.kind;
|
|
1122
|
+
if (k === void 0 || seen.has(k) || kindGlyph(k, opts.unicode) === void 0) continue;
|
|
1123
|
+
seen.add(k);
|
|
1124
|
+
lx = canvas.text(lx, legendY, " ", "none");
|
|
1125
|
+
lx = canvas.text(lx, legendY, `${kindGlyph(k, opts.unicode)} ${k}`, "none");
|
|
1126
|
+
}
|
|
1127
|
+
return;
|
|
993
1128
|
}
|
|
994
|
-
|
|
995
|
-
|
|
1129
|
+
const legendOpts = { ...opts, spinnerFrame: 0 };
|
|
1130
|
+
const faces = ["planned", "in-progress", "done", "regressed"];
|
|
1131
|
+
if (anyUnverified) faces.push("done-unverified");
|
|
1132
|
+
for (const face of faces) {
|
|
1133
|
+
if (lx > LEFT_MARGIN) lx = canvas.text(lx, legendY, " ", "none");
|
|
1134
|
+
const word = face === "done-unverified" ? "done, no evidence" : face;
|
|
1135
|
+
lx = canvas.text(lx, legendY, `${glyphFor(face, legendOpts)} ${word}`, styleFor(face));
|
|
996
1136
|
}
|
|
997
|
-
return out;
|
|
998
1137
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1138
|
+
|
|
1139
|
+
// src/render/layout.ts
|
|
1140
|
+
var LABEL_BUDGET_MIN = 4;
|
|
1141
|
+
function boxSpec(node, geo, unicode, neutral) {
|
|
1142
|
+
const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
|
|
1143
|
+
const badge = node.submap !== void 0 ? unicode ? " \u229E" : " +" : "";
|
|
1144
|
+
const badgeW = displayWidth(badge);
|
|
1145
|
+
const text = !neutral && glyph !== void 0 ? `${glyph} ${node.label}` : node.label;
|
|
1146
|
+
if (geo.mode === "constellation") {
|
|
1147
|
+
return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
|
|
1148
|
+
}
|
|
1149
|
+
if (geo.mode === "detail" && geo.detail !== void 0) {
|
|
1150
|
+
const budget2 = geo.detail;
|
|
1151
|
+
const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, budget2.innerMin), budget2.innerMax);
|
|
1152
|
+
const extra = [];
|
|
1153
|
+
if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
|
|
1154
|
+
if (node.detail !== void 0) {
|
|
1155
|
+
const wrapped = wrapWidth(node.detail, innerW - 2);
|
|
1156
|
+
for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
|
|
1157
|
+
const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
|
|
1158
|
+
extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
w: innerW + 2,
|
|
1163
|
+
h: BOX_H + extra.length,
|
|
1164
|
+
label: fitWidth(text, innerW - 4 - badgeW) + badge,
|
|
1165
|
+
pad: 1,
|
|
1166
|
+
borderless: false,
|
|
1167
|
+
extra
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
const budget = Math.max(LABEL_BUDGET_MIN, Math.ceil(displayWidth(text) * geo.scale));
|
|
1171
|
+
const label = fitWidth(text, budget) + badge;
|
|
1172
|
+
return {
|
|
1173
|
+
w: displayWidth(label) + 4 + 2 * geo.pad,
|
|
1174
|
+
h: BOX_H,
|
|
1175
|
+
label,
|
|
1176
|
+
pad: geo.pad,
|
|
1177
|
+
borderless: false,
|
|
1178
|
+
extra: []
|
|
1179
|
+
};
|
|
1002
1180
|
}
|
|
1003
|
-
function
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1181
|
+
function layoutColumns(map, geo, unicode, neutral) {
|
|
1182
|
+
const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
|
|
1183
|
+
const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
|
|
1184
|
+
const sized = /* @__PURE__ */ new Map();
|
|
1185
|
+
const bandSized = bands.map(() => []);
|
|
1186
|
+
for (const node of map.nodes) {
|
|
1187
|
+
const spec = { node, ...boxSpec(node, geo, unicode, neutral) };
|
|
1188
|
+
bandSized[bandIndexOf.get(node.layer)].push(spec);
|
|
1189
|
+
sized.set(node.id, spec);
|
|
1190
|
+
}
|
|
1191
|
+
const columnOf = /* @__PURE__ */ new Map();
|
|
1192
|
+
const lanes = [];
|
|
1193
|
+
if (map.lanes.length === 0) {
|
|
1194
|
+
for (const row of bandSized) {
|
|
1195
|
+
let x = LEFT_MARGIN;
|
|
1196
|
+
for (const spec of row) {
|
|
1197
|
+
columnOf.set(spec, x);
|
|
1198
|
+
x += spec.w + geo.boxGap;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
} else {
|
|
1202
|
+
const laneCount = map.lanes.length;
|
|
1203
|
+
const laneGap = geo.boxGap + 2;
|
|
1204
|
+
const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
|
|
1205
|
+
const regions = laneCount + 1;
|
|
1206
|
+
const grouped = bandSized.map((row) => {
|
|
1207
|
+
const cells = Array.from({ length: regions }, () => []);
|
|
1208
|
+
for (const spec of row) {
|
|
1209
|
+
const lane = spec.node.lane;
|
|
1210
|
+
cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(spec);
|
|
1211
|
+
}
|
|
1212
|
+
return cells;
|
|
1213
|
+
});
|
|
1214
|
+
const regionW = Array.from({ length: regions }, () => 0);
|
|
1215
|
+
for (const cells of grouped) {
|
|
1216
|
+
for (let i = 0; i < regions; i++) {
|
|
1217
|
+
const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
|
|
1218
|
+
regionW[i] = Math.max(regionW[i], rowW);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
|
|
1222
|
+
let x0 = LEFT_MARGIN;
|
|
1223
|
+
for (let i = 0; i < regions; i++) {
|
|
1224
|
+
lanes.push({ x: x0, w: regionW[i] });
|
|
1225
|
+
x0 += regionW[i] + laneGap;
|
|
1226
|
+
}
|
|
1227
|
+
for (const cells of grouped) {
|
|
1228
|
+
for (let i = 0; i < regions; i++) {
|
|
1229
|
+
let x = lanes[i].x;
|
|
1230
|
+
for (const spec of cells[i]) {
|
|
1231
|
+
columnOf.set(spec, x);
|
|
1232
|
+
x += spec.w + geo.boxGap;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1010
1236
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1237
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1238
|
+
for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
|
|
1239
|
+
const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
|
|
1240
|
+
const boxOf = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
|
|
1242
|
+
const bandLabel = bands.map((l, i) => {
|
|
1243
|
+
const row = bandBoxes[i];
|
|
1244
|
+
const done = row.filter((b) => b.node.status === "done").length;
|
|
1245
|
+
return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
|
|
1246
|
+
});
|
|
1247
|
+
let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
|
|
1248
|
+
for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
|
|
1249
|
+
for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
|
|
1250
|
+
return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
|
|
1251
|
+
}
|
|
1252
|
+
function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
|
|
1253
|
+
let y = 0;
|
|
1254
|
+
if (hasTitle) y += 1 + geo.titleGap;
|
|
1255
|
+
let laneHeaderY;
|
|
1256
|
+
if (hasLanes) {
|
|
1257
|
+
laneHeaderY = y;
|
|
1258
|
+
y += 1 + geo.barGap;
|
|
1014
1259
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1260
|
+
const barY = [];
|
|
1261
|
+
const gapTrackStartY = [];
|
|
1262
|
+
const bandBoxes = [];
|
|
1263
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1264
|
+
const gapCount = columns.bands.length - 1;
|
|
1265
|
+
for (let b = 0; b < columns.bands.length; b++) {
|
|
1266
|
+
barY.push(y);
|
|
1267
|
+
y += 1 + geo.barGap;
|
|
1268
|
+
const row = columns.bandBoxes[b];
|
|
1269
|
+
for (const box of row) placed.set(box, { ...box, y });
|
|
1270
|
+
bandBoxes.push(row.map((box) => placed.get(box)));
|
|
1271
|
+
y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
|
|
1272
|
+
if (b < gapCount) {
|
|
1273
|
+
y += geo.breathe;
|
|
1274
|
+
gapTrackStartY.push(y);
|
|
1275
|
+
y += gapRowCount[b];
|
|
1276
|
+
y += geo.breathe;
|
|
1277
|
+
}
|
|
1020
1278
|
}
|
|
1021
|
-
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1279
|
+
const boxOf = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
|
|
1281
|
+
return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/render/render.ts
|
|
1285
|
+
function renderMapWindow(map, opts, viewport) {
|
|
1286
|
+
const built = buildCanvas(map, opts);
|
|
1287
|
+
return {
|
|
1288
|
+
lines: built.canvas.emit(opts, viewport),
|
|
1289
|
+
contentWidth: built.canvas.width,
|
|
1290
|
+
contentHeight: built.canvas.height,
|
|
1291
|
+
hits: built.hits
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function buildCanvas(map, opts) {
|
|
1295
|
+
const oriented = flipForSequence(map);
|
|
1296
|
+
const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
|
|
1297
|
+
const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
|
|
1298
|
+
const drawn = aggregated ?? oriented;
|
|
1299
|
+
return paint(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo, unverifiedDoneIds(oriented, drawn));
|
|
1300
|
+
}
|
|
1301
|
+
function paint(map, opts, geo, unverified) {
|
|
1302
|
+
const canvas = new Canvas();
|
|
1303
|
+
if (map.layers.length === 0) {
|
|
1304
|
+
canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
|
|
1305
|
+
canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
|
|
1306
|
+
return { canvas, hits: [] };
|
|
1307
|
+
}
|
|
1308
|
+
const neutral = isNeutralKind(map);
|
|
1309
|
+
const columns = layoutColumns(map, geo, opts.unicode, neutral);
|
|
1310
|
+
const routing = routeEdges(map, columns);
|
|
1311
|
+
const rows = layoutRows(columns, geo, routing.gapRowCount, map.title !== void 0, map.lanes.length > 0);
|
|
1312
|
+
const wiredWidth = routing.fallbackCount > 0 ? columns.contentWidth + 2 + routing.fallbackCount * 2 : columns.contentWidth;
|
|
1313
|
+
const totalWidth = wiredWidth + Math.max(...columns.bandLabel.map(displayWidth));
|
|
1314
|
+
if (map.title !== void 0) drawTitle(canvas, map.title);
|
|
1315
|
+
drawLaneHeaders(canvas, map, columns, rows);
|
|
1316
|
+
drawBands(canvas, columns, rows, wiredWidth, totalWidth);
|
|
1317
|
+
const faceOf = (id, status) => unverified.has(id) ? "done-unverified" : status;
|
|
1318
|
+
for (const box of rows.boxOf.values()) {
|
|
1319
|
+
const id = box.node.id;
|
|
1320
|
+
drawBox(canvas, box, opts, neutral, faceOf(id, box.node.status), opts.focus !== void 0 && id === opts.focus);
|
|
1321
|
+
}
|
|
1322
|
+
drawEdges(canvas, routing.edges, rows, opts);
|
|
1323
|
+
drawLegend(canvas, map, opts, rows.legendY, neutral, unverified.size > 0);
|
|
1324
|
+
const hits = [...rows.boxOf.values()].map((b) => ({
|
|
1325
|
+
id: b.node.id,
|
|
1326
|
+
x: b.x,
|
|
1327
|
+
y: b.y,
|
|
1328
|
+
w: b.w,
|
|
1329
|
+
h: b.h
|
|
1330
|
+
}));
|
|
1331
|
+
return { canvas, hits };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/store/store.ts
|
|
1335
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1336
|
+
import { basename, dirname, join } from "node:path";
|
|
1337
|
+
|
|
1338
|
+
// src/store/format.ts
|
|
1339
|
+
var STATE_FILE_VERSION = 1;
|
|
1340
|
+
function makePageId(raw) {
|
|
1341
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
1027
1342
|
}
|
|
1028
1343
|
function describeStoreError(e) {
|
|
1029
1344
|
switch (e.kind) {
|
|
@@ -1035,111 +1350,169 @@ function describeStoreError(e) {
|
|
|
1035
1350
|
return `map file ${e.path} has an unexpected shape: ${e.detail}`;
|
|
1036
1351
|
case "invariant-violation":
|
|
1037
1352
|
return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
|
|
1353
|
+
case "save-failed":
|
|
1354
|
+
return `could not write ${e.path}: ${e.detail}`;
|
|
1355
|
+
case "delete-failed":
|
|
1356
|
+
return `could not delete ${e.path}: ${e.detail}`;
|
|
1038
1357
|
}
|
|
1039
1358
|
}
|
|
1040
1359
|
function isRecord(v) {
|
|
1041
1360
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1042
1361
|
}
|
|
1043
|
-
function
|
|
1044
|
-
|
|
1362
|
+
function describeValue(v) {
|
|
1363
|
+
if (v === void 0) return "missing";
|
|
1364
|
+
if (v === null) return "null";
|
|
1365
|
+
if (Array.isArray(v)) return "an array";
|
|
1366
|
+
return `a ${typeof v}`;
|
|
1045
1367
|
}
|
|
1046
|
-
function
|
|
1047
|
-
return
|
|
1368
|
+
function badShape(path, where, expected, got) {
|
|
1369
|
+
return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
|
|
1370
|
+
}
|
|
1371
|
+
function arrayField(raw, key, path, presence) {
|
|
1372
|
+
const v = raw[key];
|
|
1373
|
+
if (Array.isArray(v)) return ok(v);
|
|
1374
|
+
if (v === void 0 && presence === "optional") return ok([]);
|
|
1375
|
+
return badShape(path, `"${key}"`, "an array", v);
|
|
1376
|
+
}
|
|
1377
|
+
function requiredString(rec, key, where, path) {
|
|
1378
|
+
const v = rec[key];
|
|
1379
|
+
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
1380
|
+
}
|
|
1381
|
+
function optionalString(rec, key, where, path) {
|
|
1382
|
+
const v = rec[key];
|
|
1383
|
+
if (v === void 0) return ok(void 0);
|
|
1384
|
+
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
1048
1385
|
}
|
|
1049
1386
|
function parseMap(raw, path) {
|
|
1050
1387
|
if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
|
|
1051
1388
|
if (raw["version"] !== STATE_FILE_VERSION) {
|
|
1052
1389
|
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
|
|
1053
1390
|
}
|
|
1391
|
+
const layers = arrayField(raw, "layers", path, "required");
|
|
1392
|
+
if (!layers.ok) return layers;
|
|
1393
|
+
const nodes = arrayField(raw, "nodes", path, "required");
|
|
1394
|
+
if (!nodes.ok) return nodes;
|
|
1395
|
+
const edges = arrayField(raw, "edges", path, "required");
|
|
1396
|
+
if (!edges.ok) return edges;
|
|
1397
|
+
const lanes = arrayField(raw, "lanes", path, "optional");
|
|
1398
|
+
if (!lanes.ok) return lanes;
|
|
1399
|
+
const groups = arrayField(raw, "groups", path, "optional");
|
|
1400
|
+
if (!groups.ok) return groups;
|
|
1054
1401
|
let map = EMPTY_MAP;
|
|
1055
|
-
const title = optionalString(raw
|
|
1056
|
-
if (title
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1402
|
+
const title = optionalString(raw, "title", "map", path);
|
|
1403
|
+
if (!title.ok) return title;
|
|
1404
|
+
if (title.value !== void 0) map = setTitle(map, title.value);
|
|
1405
|
+
const rawKind = optionalString(raw, "kind", "map", path);
|
|
1406
|
+
if (!rawKind.ok) return rawKind;
|
|
1407
|
+
if (rawKind.value !== void 0) {
|
|
1408
|
+
const kind = makeMapKind(rawKind.value);
|
|
1060
1409
|
if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
|
|
1061
1410
|
map = setKind(map, kind.value);
|
|
1062
1411
|
}
|
|
1063
|
-
for (const [i, rawLayer] of
|
|
1064
|
-
|
|
1065
|
-
|
|
1412
|
+
for (const [i, rawLayer] of layers.value.entries()) {
|
|
1413
|
+
const where = `layers[${i}]`;
|
|
1414
|
+
if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
|
|
1415
|
+
const rawId = requiredString(rawLayer, "id", where, path);
|
|
1416
|
+
if (!rawId.ok) return rawId;
|
|
1417
|
+
const id = makeLayerId(rawId.value);
|
|
1066
1418
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1067
|
-
const name =
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1419
|
+
const name = requiredString(rawLayer, "name", where, path);
|
|
1420
|
+
if (!name.ok) return name;
|
|
1421
|
+
const rawRank = rawLayer["rank"];
|
|
1422
|
+
if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
|
|
1423
|
+
const rank = makeRank(rawRank);
|
|
1424
|
+
if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
|
|
1425
|
+
const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
|
|
1073
1426
|
if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
|
|
1074
1427
|
map = next.value;
|
|
1075
1428
|
}
|
|
1076
|
-
for (const [i, rawLane] of
|
|
1077
|
-
|
|
1078
|
-
|
|
1429
|
+
for (const [i, rawLane] of lanes.value.entries()) {
|
|
1430
|
+
const where = `lanes[${i}]`;
|
|
1431
|
+
if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
|
|
1432
|
+
const rawId = requiredString(rawLane, "id", where, path);
|
|
1433
|
+
if (!rawId.ok) return rawId;
|
|
1434
|
+
const id = makeLaneId(rawId.value);
|
|
1079
1435
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1080
|
-
const label =
|
|
1081
|
-
if (label
|
|
1082
|
-
const declared = declareLane(map, { id: id.value, label });
|
|
1436
|
+
const label = requiredString(rawLane, "label", where, path);
|
|
1437
|
+
if (!label.ok) return label;
|
|
1438
|
+
const declared = declareLane(map, { id: id.value, label: label.value });
|
|
1083
1439
|
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1084
1440
|
map = declared.value;
|
|
1085
1441
|
}
|
|
1086
|
-
for (const [i, rawGroup] of
|
|
1087
|
-
|
|
1088
|
-
|
|
1442
|
+
for (const [i, rawGroup] of groups.value.entries()) {
|
|
1443
|
+
const where = `groups[${i}]`;
|
|
1444
|
+
if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
|
|
1445
|
+
const rawId = requiredString(rawGroup, "id", where, path);
|
|
1446
|
+
if (!rawId.ok) return rawId;
|
|
1447
|
+
const id = makeGroupId(rawId.value);
|
|
1089
1448
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1090
|
-
const
|
|
1449
|
+
const rawLayer = requiredString(rawGroup, "layer", where, path);
|
|
1450
|
+
if (!rawLayer.ok) return rawLayer;
|
|
1451
|
+
const layer = makeLayerId(rawLayer.value);
|
|
1091
1452
|
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
1092
|
-
const label =
|
|
1093
|
-
if (label
|
|
1094
|
-
const declared = declareGroup(map, { id: id.value, label, layer: layer.value });
|
|
1453
|
+
const label = requiredString(rawGroup, "label", where, path);
|
|
1454
|
+
if (!label.ok) return label;
|
|
1455
|
+
const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
|
|
1095
1456
|
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1096
1457
|
map = declared.value;
|
|
1097
1458
|
}
|
|
1098
|
-
for (const [i, rawNode] of
|
|
1099
|
-
|
|
1100
|
-
|
|
1459
|
+
for (const [i, rawNode] of nodes.value.entries()) {
|
|
1460
|
+
const where = `nodes[${i}]`;
|
|
1461
|
+
if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
|
|
1462
|
+
const rawId = requiredString(rawNode, "id", where, path);
|
|
1463
|
+
if (!rawId.ok) return rawId;
|
|
1464
|
+
const id = makeNodeId(rawId.value);
|
|
1101
1465
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1102
|
-
const
|
|
1466
|
+
const rawLayer = requiredString(rawNode, "layer", where, path);
|
|
1467
|
+
if (!rawLayer.ok) return rawLayer;
|
|
1468
|
+
const layer = makeLayerId(rawLayer.value);
|
|
1103
1469
|
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
1104
|
-
const
|
|
1470
|
+
const rawStatus = requiredString(rawNode, "status", where, path);
|
|
1471
|
+
if (!rawStatus.ok) return rawStatus;
|
|
1472
|
+
const status = makeNodeStatus(rawStatus.value);
|
|
1105
1473
|
if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
|
|
1106
|
-
const label =
|
|
1107
|
-
if (label
|
|
1108
|
-
const detail = optionalString(rawNode
|
|
1109
|
-
|
|
1474
|
+
const label = requiredString(rawNode, "label", where, path);
|
|
1475
|
+
if (!label.ok) return label;
|
|
1476
|
+
const detail = optionalString(rawNode, "detail", where, path);
|
|
1477
|
+
if (!detail.ok) return detail;
|
|
1478
|
+
const rawGroup = optionalString(rawNode, "group", where, path);
|
|
1479
|
+
if (!rawGroup.ok) return rawGroup;
|
|
1110
1480
|
let group;
|
|
1111
|
-
if (rawGroup !== void 0) {
|
|
1112
|
-
const made = makeGroupId(rawGroup);
|
|
1481
|
+
if (rawGroup.value !== void 0) {
|
|
1482
|
+
const made = makeGroupId(rawGroup.value);
|
|
1113
1483
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1114
1484
|
group = made.value;
|
|
1115
1485
|
}
|
|
1116
|
-
const rawNodeKind = optionalString(rawNode
|
|
1486
|
+
const rawNodeKind = optionalString(rawNode, "kind", where, path);
|
|
1487
|
+
if (!rawNodeKind.ok) return rawNodeKind;
|
|
1117
1488
|
let nodeKind;
|
|
1118
|
-
if (rawNodeKind !== void 0) {
|
|
1119
|
-
const made = makeNodeKind(rawNodeKind);
|
|
1489
|
+
if (rawNodeKind.value !== void 0) {
|
|
1490
|
+
const made = makeNodeKind(rawNodeKind.value);
|
|
1120
1491
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1121
1492
|
nodeKind = made.value;
|
|
1122
1493
|
}
|
|
1123
|
-
const rawLane = optionalString(rawNode
|
|
1494
|
+
const rawLane = optionalString(rawNode, "lane", where, path);
|
|
1495
|
+
if (!rawLane.ok) return rawLane;
|
|
1124
1496
|
let lane;
|
|
1125
|
-
if (rawLane !== void 0) {
|
|
1126
|
-
const made = makeLaneId(rawLane);
|
|
1497
|
+
if (rawLane.value !== void 0) {
|
|
1498
|
+
const made = makeLaneId(rawLane.value);
|
|
1127
1499
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1128
1500
|
lane = made.value;
|
|
1129
1501
|
}
|
|
1130
|
-
const rawSubmap = optionalString(rawNode
|
|
1502
|
+
const rawSubmap = optionalString(rawNode, "submap", where, path);
|
|
1503
|
+
if (!rawSubmap.ok) return rawSubmap;
|
|
1131
1504
|
let submap;
|
|
1132
|
-
if (rawSubmap !== void 0) {
|
|
1133
|
-
const made = makeSubmapRef(rawSubmap);
|
|
1505
|
+
if (rawSubmap.value !== void 0) {
|
|
1506
|
+
const made = makeSubmapRef(rawSubmap.value);
|
|
1134
1507
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1135
1508
|
submap = made.value;
|
|
1136
1509
|
}
|
|
1137
1510
|
const declared = declareNode(map, {
|
|
1138
1511
|
id: id.value,
|
|
1139
|
-
label,
|
|
1512
|
+
label: label.value,
|
|
1140
1513
|
layer: layer.value,
|
|
1141
1514
|
status: status.value,
|
|
1142
|
-
...detail !== void 0 ? { detail } : {},
|
|
1515
|
+
...detail.value !== void 0 ? { detail: detail.value } : {},
|
|
1143
1516
|
...group !== void 0 ? { group } : {},
|
|
1144
1517
|
...nodeKind !== void 0 ? { kind: nodeKind } : {},
|
|
1145
1518
|
...lane !== void 0 ? { lane } : {},
|
|
@@ -1147,25 +1520,210 @@ function parseMap(raw, path) {
|
|
|
1147
1520
|
});
|
|
1148
1521
|
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1149
1522
|
map = declared.value;
|
|
1150
|
-
const evidence = optionalString(rawNode
|
|
1151
|
-
if (evidence
|
|
1152
|
-
|
|
1523
|
+
const evidence = optionalString(rawNode, "evidence", where, path);
|
|
1524
|
+
if (!evidence.ok) return evidence;
|
|
1525
|
+
if (evidence.value !== void 0) {
|
|
1526
|
+
const updated = updateNode(map, { id: id.value, evidence: evidence.value });
|
|
1153
1527
|
if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
|
|
1154
1528
|
map = updated.value;
|
|
1155
1529
|
}
|
|
1156
1530
|
}
|
|
1157
|
-
for (const [i, rawEdge] of
|
|
1158
|
-
|
|
1159
|
-
|
|
1531
|
+
for (const [i, rawEdge] of edges.value.entries()) {
|
|
1532
|
+
const where = `edges[${i}]`;
|
|
1533
|
+
if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
|
|
1534
|
+
const rawFrom = requiredString(rawEdge, "from", where, path);
|
|
1535
|
+
if (!rawFrom.ok) return rawFrom;
|
|
1536
|
+
const from = makeNodeId(rawFrom.value);
|
|
1160
1537
|
if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
|
|
1161
|
-
const
|
|
1538
|
+
const rawTo = requiredString(rawEdge, "to", where, path);
|
|
1539
|
+
if (!rawTo.ok) return rawTo;
|
|
1540
|
+
const to = makeNodeId(rawTo.value);
|
|
1162
1541
|
if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
|
|
1163
|
-
const
|
|
1542
|
+
const label = optionalString(rawEdge, "label", where, path);
|
|
1543
|
+
if (!label.ok) return label;
|
|
1544
|
+
const linked = linkNodes(map, from.value, to.value, label.value);
|
|
1164
1545
|
if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
|
|
1165
1546
|
map = linked.value;
|
|
1166
1547
|
}
|
|
1167
1548
|
return ok(map);
|
|
1168
1549
|
}
|
|
1550
|
+
|
|
1551
|
+
// src/store/store.ts
|
|
1552
|
+
var RENAME_MAX_ATTEMPTS = 10;
|
|
1553
|
+
var RENAME_BACKOFF_STEP_MS = 10;
|
|
1554
|
+
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
|
|
1555
|
+
function sleepSync(ms) {
|
|
1556
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1557
|
+
}
|
|
1558
|
+
function discardTemp(tmp) {
|
|
1559
|
+
try {
|
|
1560
|
+
rmSync(tmp, { force: true });
|
|
1561
|
+
} catch {
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function errnoOf(e) {
|
|
1565
|
+
return e.code ?? e.message;
|
|
1566
|
+
}
|
|
1567
|
+
function writeFileAtomic(path, contents) {
|
|
1568
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
1569
|
+
try {
|
|
1570
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1571
|
+
writeFileSync(tmp, contents, "utf8");
|
|
1572
|
+
} catch (e) {
|
|
1573
|
+
discardTemp(tmp);
|
|
1574
|
+
return err({ kind: "save-failed", path, detail: `writing the temp file failed: ${errnoOf(e)}` });
|
|
1575
|
+
}
|
|
1576
|
+
let attempt = 1;
|
|
1577
|
+
for (; ; ) {
|
|
1578
|
+
try {
|
|
1579
|
+
renameSync(tmp, path);
|
|
1580
|
+
return ok(void 0);
|
|
1581
|
+
} catch (e) {
|
|
1582
|
+
const code = errnoOf(e);
|
|
1583
|
+
if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= RENAME_MAX_ATTEMPTS) {
|
|
1584
|
+
discardTemp(tmp);
|
|
1585
|
+
return err({ kind: "save-failed", path, detail: `${code} after ${attempt} attempt(s)` });
|
|
1586
|
+
}
|
|
1587
|
+
sleepSync(attempt * RENAME_BACKOFF_STEP_MS);
|
|
1588
|
+
attempt += 1;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function isRecord2(v) {
|
|
1593
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1594
|
+
}
|
|
1595
|
+
function stripBom(text) {
|
|
1596
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1597
|
+
}
|
|
1598
|
+
var STORE_DIR_NAME = ".mellos";
|
|
1599
|
+
var STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, "map.json");
|
|
1600
|
+
var PAGES_DIR_NAME = "pages";
|
|
1601
|
+
function pageFilePath(defaultFile, page) {
|
|
1602
|
+
return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
1603
|
+
}
|
|
1604
|
+
function pageIdOfFile(defaultFile, path) {
|
|
1605
|
+
if (path === defaultFile) return void 0;
|
|
1606
|
+
const name = basename(path);
|
|
1607
|
+
return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
|
|
1608
|
+
}
|
|
1609
|
+
function listPageFiles(defaultFile) {
|
|
1610
|
+
const out = [];
|
|
1611
|
+
if (existsSync(defaultFile)) out.push(defaultFile);
|
|
1612
|
+
let entries = [];
|
|
1613
|
+
try {
|
|
1614
|
+
entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
for (const e of entries.sort()) {
|
|
1618
|
+
if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
|
|
1619
|
+
}
|
|
1620
|
+
return out;
|
|
1621
|
+
}
|
|
1622
|
+
function deletePageFile(path) {
|
|
1623
|
+
try {
|
|
1624
|
+
rmSync(path, { force: true });
|
|
1625
|
+
return ok(void 0);
|
|
1626
|
+
} catch (e) {
|
|
1627
|
+
return err({ kind: "delete-failed", path, detail: errnoOf(e) });
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
var FOCUS_FILE_NAME = "focus";
|
|
1631
|
+
function focusFilePath(defaultFile) {
|
|
1632
|
+
return join(dirname(defaultFile), FOCUS_FILE_NAME);
|
|
1633
|
+
}
|
|
1634
|
+
function takeFocusRequest(defaultFile) {
|
|
1635
|
+
const path = focusFilePath(defaultFile);
|
|
1636
|
+
let raw;
|
|
1637
|
+
try {
|
|
1638
|
+
raw = readFileSync(path, "utf8");
|
|
1639
|
+
} catch {
|
|
1640
|
+
return void 0;
|
|
1641
|
+
}
|
|
1642
|
+
try {
|
|
1643
|
+
rmSync(path, { force: true });
|
|
1644
|
+
} catch {
|
|
1645
|
+
}
|
|
1646
|
+
let parsed;
|
|
1647
|
+
try {
|
|
1648
|
+
parsed = JSON.parse(raw);
|
|
1649
|
+
} catch {
|
|
1650
|
+
return void 0;
|
|
1651
|
+
}
|
|
1652
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1653
|
+
const page = parsed.page;
|
|
1654
|
+
if (page === void 0 || page === null) return { page: void 0 };
|
|
1655
|
+
if (typeof page !== "string") return void 0;
|
|
1656
|
+
const id = makePageId(page);
|
|
1657
|
+
return id.ok ? { page: id.value } : void 0;
|
|
1658
|
+
}
|
|
1659
|
+
var QUIT_FILE_NAME = "quit";
|
|
1660
|
+
function quitFilePath(defaultFile) {
|
|
1661
|
+
return join(dirname(defaultFile), QUIT_FILE_NAME);
|
|
1662
|
+
}
|
|
1663
|
+
function takeQuitRequest(defaultFile) {
|
|
1664
|
+
const path = quitFilePath(defaultFile);
|
|
1665
|
+
let raw;
|
|
1666
|
+
try {
|
|
1667
|
+
raw = readFileSync(path, "utf8");
|
|
1668
|
+
} catch {
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
sweepQuitRequest(defaultFile);
|
|
1672
|
+
let parsed;
|
|
1673
|
+
try {
|
|
1674
|
+
parsed = JSON.parse(stripBom(raw));
|
|
1675
|
+
} catch {
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
return isRecord2(parsed);
|
|
1679
|
+
}
|
|
1680
|
+
function sweepQuitRequest(defaultFile) {
|
|
1681
|
+
try {
|
|
1682
|
+
rmSync(quitFilePath(defaultFile), { force: true });
|
|
1683
|
+
} catch {
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
var VIEWERS_DIR_NAME = "viewers";
|
|
1687
|
+
var VIEWER_FILE_VERSION = 1;
|
|
1688
|
+
var VIEWER_HEARTBEAT_MS = 1e3;
|
|
1689
|
+
function viewersDirPath(defaultFile) {
|
|
1690
|
+
return join(dirname(defaultFile), VIEWERS_DIR_NAME);
|
|
1691
|
+
}
|
|
1692
|
+
function viewerFilePath(defaultFile, pid) {
|
|
1693
|
+
return join(viewersDirPath(defaultFile), `${pid}.json`);
|
|
1694
|
+
}
|
|
1695
|
+
function publishViewer(defaultFile, pid, report) {
|
|
1696
|
+
const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow };
|
|
1697
|
+
return writeFileAtomic(viewerFilePath(defaultFile, pid), `${JSON.stringify(body, null, 2)}
|
|
1698
|
+
`);
|
|
1699
|
+
}
|
|
1700
|
+
function retireViewer(defaultFile, pid) {
|
|
1701
|
+
try {
|
|
1702
|
+
rmSync(viewerFilePath(defaultFile, pid), { force: true });
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
1707
|
+
function configFilePath(defaultFile) {
|
|
1708
|
+
return join(dirname(defaultFile), CONFIG_FILE_NAME);
|
|
1709
|
+
}
|
|
1710
|
+
var LEGACY_STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
|
|
1711
|
+
var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
|
|
1712
|
+
var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
|
|
1713
|
+
function migrateLegacyStore(defaultFile) {
|
|
1714
|
+
const projectRoot = dirname(dirname(defaultFile));
|
|
1715
|
+
const legacyDefault = join(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
|
|
1716
|
+
const legacyPages = join(dirname(legacyDefault), LEGACY_PAGES_DIR_NAME);
|
|
1717
|
+
const legacyConfig = join(dirname(legacyDefault), LEGACY_CONFIG_FILE_NAME);
|
|
1718
|
+
const hasLegacy = existsSync(legacyDefault) || existsSync(legacyPages) || existsSync(legacyConfig);
|
|
1719
|
+
const hasCurrent = existsSync(defaultFile) || existsSync(join(dirname(defaultFile), PAGES_DIR_NAME)) || existsSync(configFilePath(defaultFile));
|
|
1720
|
+
if (!hasLegacy || hasCurrent) return false;
|
|
1721
|
+
mkdirSync(dirname(defaultFile), { recursive: true });
|
|
1722
|
+
if (existsSync(legacyDefault)) renameSync(legacyDefault, defaultFile);
|
|
1723
|
+
if (existsSync(legacyPages)) renameSync(legacyPages, join(dirname(defaultFile), PAGES_DIR_NAME));
|
|
1724
|
+
if (existsSync(legacyConfig)) renameSync(legacyConfig, configFilePath(defaultFile));
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1169
1727
|
function loadMapFile(path) {
|
|
1170
1728
|
let text;
|
|
1171
1729
|
try {
|
|
@@ -1177,7 +1735,7 @@ function loadMapFile(path) {
|
|
|
1177
1735
|
}
|
|
1178
1736
|
let raw;
|
|
1179
1737
|
try {
|
|
1180
|
-
raw = JSON.parse(text);
|
|
1738
|
+
raw = JSON.parse(stripBom(text));
|
|
1181
1739
|
} catch (e) {
|
|
1182
1740
|
return err({ kind: "malformed-json", path, detail: e.message });
|
|
1183
1741
|
}
|
|
@@ -1188,14 +1746,18 @@ function loadMapFile(path) {
|
|
|
1188
1746
|
var KEY_H_STEP = 4;
|
|
1189
1747
|
var KEY_V_STEP = 2;
|
|
1190
1748
|
var WHEEL_V_STEP = 3;
|
|
1749
|
+
var WHEEL_H_STEP = 4;
|
|
1191
1750
|
var MOTION = 32;
|
|
1192
1751
|
var WHEEL = 64;
|
|
1193
1752
|
var SHIFT = 4;
|
|
1194
1753
|
var BUTTON_BITS = 3;
|
|
1754
|
+
var WHEEL_BITS = 3;
|
|
1195
1755
|
var SGR_MOUSE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
1196
1756
|
var ARROW = /^\x1b\[([ABCD])/;
|
|
1197
1757
|
var SHIFT_TAB = /^\x1b\[Z/;
|
|
1198
|
-
var
|
|
1758
|
+
var CSI_SEQUENCE = /^\x1b\[[0-9;:<=>?]*[ -/]*[@-~]/;
|
|
1759
|
+
var SS3_SEQUENCE = /^\x1bO[@-~]/;
|
|
1760
|
+
var PARTIAL_ESCAPE = /^(?:\x1b\[[0-9;:<=>?]*[ -/]*|\x1bO)$/;
|
|
1199
1761
|
var ARROW_PAN = {
|
|
1200
1762
|
A: { dx: 0, dy: -KEY_V_STEP },
|
|
1201
1763
|
B: { dx: 0, dy: KEY_V_STEP },
|
|
@@ -1210,8 +1772,17 @@ var KEY_PAN = {
|
|
|
1210
1772
|
};
|
|
1211
1773
|
function mouseEvent(code, x, y, final) {
|
|
1212
1774
|
if (code & WHEEL) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1775
|
+
switch (code & WHEEL_BITS) {
|
|
1776
|
+
case 0:
|
|
1777
|
+
case 1: {
|
|
1778
|
+
const down = (code & 1) !== 0;
|
|
1779
|
+
return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1, at: { x, y } };
|
|
1780
|
+
}
|
|
1781
|
+
default: {
|
|
1782
|
+
const right = (code & 1) !== 0;
|
|
1783
|
+
return { kind: "pan", dx: (right ? 1 : -1) * WHEEL_H_STEP, dy: 0 };
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1215
1786
|
}
|
|
1216
1787
|
const buttons = code & BUTTON_BITS;
|
|
1217
1788
|
if (final === "m") return buttons === 0 ? { kind: "mouse-up", x, y } : void 0;
|
|
@@ -1223,7 +1794,6 @@ function mouseEvent(code, x, y, final) {
|
|
|
1223
1794
|
return buttons === 0 ? { kind: "mouse-down", x, y } : void 0;
|
|
1224
1795
|
}
|
|
1225
1796
|
function parseInput(chunk) {
|
|
1226
|
-
if (chunk === "\x1B") return { events: [{ kind: "clear" }], rest: "" };
|
|
1227
1797
|
const events = [];
|
|
1228
1798
|
let i = 0;
|
|
1229
1799
|
while (i < chunk.length) {
|
|
@@ -1248,18 +1818,24 @@ function parseInput(chunk) {
|
|
|
1248
1818
|
i += shiftTab[0].length;
|
|
1249
1819
|
continue;
|
|
1250
1820
|
}
|
|
1251
|
-
const
|
|
1252
|
-
if (
|
|
1821
|
+
const sequence = CSI_SEQUENCE.exec(slice) ?? SS3_SEQUENCE.exec(slice);
|
|
1822
|
+
if (sequence) {
|
|
1823
|
+
i += sequence[0].length;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (PARTIAL_ESCAPE.test(slice)) {
|
|
1253
1827
|
return { events, rest: slice };
|
|
1254
1828
|
}
|
|
1255
1829
|
const ch = chunk[i];
|
|
1256
|
-
if (ch === "
|
|
1830
|
+
if (ch === "\x1B") events.push({ kind: "clear" });
|
|
1831
|
+
else if (ch === "q" || ch === "Q" || ch === "" || ch === "") events.push({ kind: "quit" });
|
|
1257
1832
|
else if (ch === "0") events.push({ kind: "reset" });
|
|
1258
1833
|
else if (ch === "+" || ch === "=") events.push({ kind: "zoom", delta: 1 });
|
|
1259
1834
|
else if (ch === "-") events.push({ kind: "zoom", delta: -1 });
|
|
1260
1835
|
else if (ch === " ") events.push({ kind: "next-page" });
|
|
1261
1836
|
else if (ch === "\x7F" || ch === "\b") events.push({ kind: "back" });
|
|
1262
1837
|
else if (ch === "f" || ch === "F") events.push({ kind: "follow-toggle" });
|
|
1838
|
+
else if (ch === "x" || ch === "X") events.push({ kind: "delete-page" });
|
|
1263
1839
|
else if (ch >= "1" && ch <= "9") events.push({ kind: "page", index: ch.charCodeAt(0) - "1".charCodeAt(0) });
|
|
1264
1840
|
else if (KEY_PAN[ch]) events.push({ kind: "pan", ...KEY_PAN[ch] });
|
|
1265
1841
|
i += 1;
|
|
@@ -1267,28 +1843,209 @@ function parseInput(chunk) {
|
|
|
1267
1843
|
return { events, rest: "" };
|
|
1268
1844
|
}
|
|
1269
1845
|
|
|
1846
|
+
// src/watch/pane-state.ts
|
|
1847
|
+
function describePageFault(fault) {
|
|
1848
|
+
return fault.kind === "unreadable" ? `cannot read ${fault.path}: ${fault.detail}` : describeStoreError(fault);
|
|
1849
|
+
}
|
|
1850
|
+
function isTransient(fault) {
|
|
1851
|
+
return fault.kind === "malformed-json";
|
|
1852
|
+
}
|
|
1853
|
+
function mapOf(entry) {
|
|
1854
|
+
if (entry === void 0 || entry.state.kind === "absent") return void 0;
|
|
1855
|
+
return entry.state.kind === "loaded" ? entry.state.map : entry.state.lastGood;
|
|
1856
|
+
}
|
|
1857
|
+
function mtimeOf(entry) {
|
|
1858
|
+
return entry === void 0 || entry.state.kind === "absent" ? void 0 : entry.state.mtimeMs;
|
|
1859
|
+
}
|
|
1860
|
+
function initialPaneState(follow, requestedFile) {
|
|
1861
|
+
return {
|
|
1862
|
+
pages: [],
|
|
1863
|
+
activeFile: void 0,
|
|
1864
|
+
pendingFocusFile: requestedFile,
|
|
1865
|
+
follow,
|
|
1866
|
+
diveStack: [],
|
|
1867
|
+
scanned: false,
|
|
1868
|
+
pendingDelete: void 0
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function entryOf(state, file) {
|
|
1872
|
+
return file === void 0 ? void 0 : state.pages.find((p) => p.file === file);
|
|
1873
|
+
}
|
|
1874
|
+
function mapsOf(state) {
|
|
1875
|
+
return new Map(state.pages.map((p) => [p.file, mapOf(p)]));
|
|
1876
|
+
}
|
|
1877
|
+
function filesOf(state) {
|
|
1878
|
+
return state.pages.map((p) => p.file);
|
|
1879
|
+
}
|
|
1880
|
+
function markViewed(state, file) {
|
|
1881
|
+
if (file === void 0 || !state.pages.some((p) => p.file === file && p.fresh)) return state;
|
|
1882
|
+
return { ...state, pages: state.pages.map((p) => p.file === file ? { ...p, fresh: false } : p) };
|
|
1883
|
+
}
|
|
1884
|
+
function userSwitch(state, file) {
|
|
1885
|
+
const followTurnedOff = state.follow;
|
|
1886
|
+
return {
|
|
1887
|
+
state: markViewed(
|
|
1888
|
+
// Looking elsewhere withdraws an armed deletion: it was aimed at the
|
|
1889
|
+
// page that was on screen, and the confirming press must never land on
|
|
1890
|
+
// whichever page took its place.
|
|
1891
|
+
{ ...state, activeFile: file, pendingFocusFile: void 0, follow: false, pendingDelete: void 0 },
|
|
1892
|
+
file
|
|
1893
|
+
),
|
|
1894
|
+
followTurnedOff
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function disarmDelete(state) {
|
|
1898
|
+
return state.pendingDelete === void 0 ? state : { ...state, pendingDelete: void 0 };
|
|
1899
|
+
}
|
|
1900
|
+
function requestDelete(state, now, windowMs) {
|
|
1901
|
+
const file = state.activeFile;
|
|
1902
|
+
if (file === void 0) return { state: disarmDelete(state), request: { kind: "none" } };
|
|
1903
|
+
if (entryOf(state, file)?.state.kind === "absent") {
|
|
1904
|
+
return { state: disarmDelete(state), request: { kind: "absent", file } };
|
|
1905
|
+
}
|
|
1906
|
+
const armed = state.pendingDelete;
|
|
1907
|
+
if (armed !== void 0 && armed.file === file && now <= armed.until) {
|
|
1908
|
+
return { state: { ...state, pendingDelete: void 0 }, request: { kind: "confirmed", file } };
|
|
1909
|
+
}
|
|
1910
|
+
const until = now + windowMs;
|
|
1911
|
+
return { state: { ...state, pendingDelete: { file, until } }, request: { kind: "armed", file, until } };
|
|
1912
|
+
}
|
|
1913
|
+
function toggleFollow(state) {
|
|
1914
|
+
return { ...state, follow: !state.follow };
|
|
1915
|
+
}
|
|
1916
|
+
function pushDive(state, file) {
|
|
1917
|
+
return { ...state, diveStack: [...state.diveStack, file] };
|
|
1918
|
+
}
|
|
1919
|
+
function popDive(state) {
|
|
1920
|
+
const files = new Set(filesOf(state));
|
|
1921
|
+
const stack = [...state.diveStack];
|
|
1922
|
+
while (stack.length > 0) {
|
|
1923
|
+
const parent = stack.pop();
|
|
1924
|
+
if (files.has(parent)) return { state: { ...state, diveStack: stack }, parent };
|
|
1925
|
+
}
|
|
1926
|
+
return { state: { ...state, diveStack: [] }, parent: void 0 };
|
|
1927
|
+
}
|
|
1928
|
+
function scan(state, input) {
|
|
1929
|
+
const first = !state.scanned;
|
|
1930
|
+
const previousActive = state.activeFile;
|
|
1931
|
+
const pages = [];
|
|
1932
|
+
const freshened = [];
|
|
1933
|
+
const changed = [];
|
|
1934
|
+
for (const file of input.files) {
|
|
1935
|
+
const held = entryOf(state, file);
|
|
1936
|
+
const mtimeMs = input.mtimeAt(file);
|
|
1937
|
+
if (mtimeMs === void 0) {
|
|
1938
|
+
pages.push(held ?? { file, state: { kind: "absent" }, fresh: false });
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
const settled = held !== void 0 && mtimeOf(held) === mtimeMs && !(held.state.kind === "faulted" && held.state.transient);
|
|
1942
|
+
if (settled) {
|
|
1943
|
+
pages.push(held);
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
const loaded = input.load(file);
|
|
1947
|
+
if (loaded.ok) {
|
|
1948
|
+
if (!first) changed.push(file);
|
|
1949
|
+
const fresh = !first && file !== previousActive;
|
|
1950
|
+
if (fresh) freshened.push(file);
|
|
1951
|
+
pages.push({ file, state: { kind: "loaded", map: loaded.value, mtimeMs }, fresh });
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
pages.push({
|
|
1955
|
+
file,
|
|
1956
|
+
state: {
|
|
1957
|
+
kind: "faulted",
|
|
1958
|
+
fault: loaded.error,
|
|
1959
|
+
lastGood: mapOf(held),
|
|
1960
|
+
mtimeMs,
|
|
1961
|
+
transient: isTransient(loaded.error)
|
|
1962
|
+
},
|
|
1963
|
+
fresh: held?.fresh ?? false
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
let pendingFocusFile = state.pendingFocusFile;
|
|
1967
|
+
if (input.focusRequest !== void 0 && !(first && pendingFocusFile !== void 0)) {
|
|
1968
|
+
pendingFocusFile = input.focusRequest;
|
|
1969
|
+
}
|
|
1970
|
+
let activeFile = previousActive;
|
|
1971
|
+
let requestApplied = false;
|
|
1972
|
+
if (pendingFocusFile !== void 0 && input.files.includes(pendingFocusFile)) {
|
|
1973
|
+
activeFile = pendingFocusFile;
|
|
1974
|
+
pendingFocusFile = void 0;
|
|
1975
|
+
requestApplied = true;
|
|
1976
|
+
}
|
|
1977
|
+
const mtimeIn = (file) => mtimeOf(pages.find((p) => p.file === file));
|
|
1978
|
+
if (state.follow && !requestApplied && changed.length > 0 && !input.engaged) {
|
|
1979
|
+
activeFile = mostRecentKey(changed, mtimeIn) ?? activeFile;
|
|
1980
|
+
}
|
|
1981
|
+
if (activeFile === void 0 || !input.files.includes(activeFile)) {
|
|
1982
|
+
activeFile = mostRecentKey(input.files, mtimeIn);
|
|
1983
|
+
}
|
|
1984
|
+
const files = new Set(input.files);
|
|
1985
|
+
const next = {
|
|
1986
|
+
pages,
|
|
1987
|
+
activeFile,
|
|
1988
|
+
pendingFocusFile,
|
|
1989
|
+
follow: state.follow,
|
|
1990
|
+
diveStack: state.diveStack.filter((f) => files.has(f)),
|
|
1991
|
+
scanned: true,
|
|
1992
|
+
// An armed deletion belongs to the page it was armed on. If the scan
|
|
1993
|
+
// moved the view (follow, a request, a page that vanished), the request
|
|
1994
|
+
// is stale — the confirming press must never hit a page that merely
|
|
1995
|
+
// arrived under the cursor.
|
|
1996
|
+
pendingDelete: state.pendingDelete?.file === activeFile ? state.pendingDelete : void 0
|
|
1997
|
+
};
|
|
1998
|
+
return { state: markViewed(next, activeFile), freshened };
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1270
2001
|
// src/watch/watch.ts
|
|
2002
|
+
function describeArgsError(e) {
|
|
2003
|
+
switch (e.kind) {
|
|
2004
|
+
case "unknown-flag":
|
|
2005
|
+
return `unknown flag "${e.flag}"`;
|
|
2006
|
+
case "missing-value":
|
|
2007
|
+
return `${e.flag} needs a value`;
|
|
2008
|
+
case "invalid-value":
|
|
2009
|
+
return `${e.flag} got "${e.raw}" (expected: ${e.rule})`;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
var USAGE = "usage: mellos-mapping-watch [--file <map.json>] [--page <slug>] [--interval <ms>] [--ascii] [--no-color] [--no-mouse] [--no-follow]";
|
|
1271
2013
|
function parseArgs(argv, cwd) {
|
|
1272
2014
|
let file = join2(cwd, STATE_FILE_RELATIVE_PATH);
|
|
1273
|
-
let intervalMs =
|
|
2015
|
+
let intervalMs = POLL_INTERVAL_DEFAULT_MS;
|
|
1274
2016
|
let unicode = true;
|
|
1275
2017
|
let color = true;
|
|
1276
2018
|
let mouse = true;
|
|
1277
2019
|
let page;
|
|
1278
2020
|
let follow = true;
|
|
2021
|
+
const valueOf = (flag, raw) => raw === void 0 || raw.startsWith("--") ? err({ kind: "missing-value", flag }) : ok(raw);
|
|
1279
2022
|
for (let i = 0; i < argv.length; i++) {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
2023
|
+
const flag = argv[i];
|
|
2024
|
+
switch (flag) {
|
|
2025
|
+
case "--file": {
|
|
2026
|
+
const value = valueOf(flag, argv[++i]);
|
|
2027
|
+
if (!value.ok) return value;
|
|
2028
|
+
file = value.value;
|
|
1283
2029
|
break;
|
|
2030
|
+
}
|
|
1284
2031
|
case "--page": {
|
|
1285
|
-
const
|
|
1286
|
-
if (
|
|
2032
|
+
const value = valueOf(flag, argv[++i]);
|
|
2033
|
+
if (!value.ok) return value;
|
|
2034
|
+
const parsed = makePageId(value.value);
|
|
2035
|
+
if (!parsed.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed.error.rule });
|
|
2036
|
+
page = parsed.value;
|
|
1287
2037
|
break;
|
|
1288
2038
|
}
|
|
1289
|
-
case "--interval":
|
|
1290
|
-
|
|
2039
|
+
case "--interval": {
|
|
2040
|
+
const value = valueOf(flag, argv[++i]);
|
|
2041
|
+
if (!value.ok) return value;
|
|
2042
|
+
const ms = Number(value.value);
|
|
2043
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
2044
|
+
return err({ kind: "invalid-value", flag, raw: value.value, rule: "a positive number of milliseconds" });
|
|
2045
|
+
}
|
|
2046
|
+
intervalMs = Math.max(POLL_INTERVAL_MIN_MS, ms);
|
|
1291
2047
|
break;
|
|
2048
|
+
}
|
|
1292
2049
|
case "--ascii":
|
|
1293
2050
|
unicode = false;
|
|
1294
2051
|
break;
|
|
@@ -1302,10 +2059,24 @@ function parseArgs(argv, cwd) {
|
|
|
1302
2059
|
follow = false;
|
|
1303
2060
|
break;
|
|
1304
2061
|
default:
|
|
1305
|
-
|
|
2062
|
+
return err({ kind: "unknown-flag", flag });
|
|
1306
2063
|
}
|
|
1307
2064
|
}
|
|
1308
|
-
return { file, intervalMs, unicode, color, mouse, page, follow };
|
|
2065
|
+
return ok({ file, intervalMs, unicode, color, mouse, page, follow });
|
|
2066
|
+
}
|
|
2067
|
+
function readPage(file) {
|
|
2068
|
+
try {
|
|
2069
|
+
return loadMapFile(file);
|
|
2070
|
+
} catch (e) {
|
|
2071
|
+
return err({ kind: "unreadable", path: file, detail: e.code ?? e.message });
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
function renderWindow(map, opts, viewport) {
|
|
2075
|
+
try {
|
|
2076
|
+
return ok(renderMapWindow(map, opts, viewport));
|
|
2077
|
+
} catch (e) {
|
|
2078
|
+
return err(`this map could not be drawn: ${e.message}`);
|
|
2079
|
+
}
|
|
1309
2080
|
}
|
|
1310
2081
|
function dividerRow(width, unicode, follow) {
|
|
1311
2082
|
const grip = unicode ? " \u22EF " : " ~ ";
|
|
@@ -1319,18 +2090,6 @@ function dividerRow(width, unicode, follow) {
|
|
|
1319
2090
|
}
|
|
1320
2091
|
return bar;
|
|
1321
2092
|
}
|
|
1322
|
-
function mostRecentPageFile(files, mtimeOf) {
|
|
1323
|
-
let best;
|
|
1324
|
-
let bestMtime = -Infinity;
|
|
1325
|
-
for (const file of files) {
|
|
1326
|
-
const mtime = mtimeOf(file);
|
|
1327
|
-
if (mtime !== void 0 && mtime > bestMtime) {
|
|
1328
|
-
best = file;
|
|
1329
|
-
bestMtime = mtime;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
return best ?? files[0];
|
|
1333
|
-
}
|
|
1334
2093
|
var HIDE_CURSOR = "\x1B[?25l";
|
|
1335
2094
|
var SHOW_CURSOR = "\x1B[?25h";
|
|
1336
2095
|
var CLEAR_ALL = "\x1B[H\x1B[2J";
|
|
@@ -1339,9 +2098,23 @@ var ERASE_LINE_END = "\x1B[K";
|
|
|
1339
2098
|
var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
|
|
1340
2099
|
var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
|
|
1341
2100
|
var RESET = "\x1B[0m";
|
|
2101
|
+
function terminalRestoreSequence(mouseActive) {
|
|
2102
|
+
return (mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + RESET + "\n";
|
|
2103
|
+
}
|
|
1342
2104
|
var PANEL_CONTENT_ROWS = 6;
|
|
1343
2105
|
var PANEL_ROWS_MIN = 2;
|
|
1344
2106
|
var MAP_ROWS_MIN = 4;
|
|
2107
|
+
var POLL_INTERVAL_DEFAULT_MS = 250;
|
|
2108
|
+
var POLL_INTERVAL_MIN_MS = 50;
|
|
2109
|
+
var SPLASH_FRAME_MS = 80;
|
|
2110
|
+
var FLASH_ACK_MS = 2500;
|
|
2111
|
+
var FLASH_NOTICE_MS = 3e3;
|
|
2112
|
+
var FLASH_BACKGROUND_NEWS_MS = 4e3;
|
|
2113
|
+
var DOUBLE_CLICK_MS = 450;
|
|
2114
|
+
var CONFIRM_WINDOW_MS = 3e3;
|
|
2115
|
+
var FALLBACK_COLUMNS = 100;
|
|
2116
|
+
var FALLBACK_ROWS = 30;
|
|
2117
|
+
var DEFAULT_PAGE_TAB_LABEL = "main";
|
|
1345
2118
|
function usableColumns(cols) {
|
|
1346
2119
|
return Math.max(1, cols - 1);
|
|
1347
2120
|
}
|
|
@@ -1352,18 +2125,6 @@ function clampPanelRows(wanted, totalRows, tabRows) {
|
|
|
1352
2125
|
function panelRowsFromDividerY(termY, totalRows, tabRows) {
|
|
1353
2126
|
return clampPanelRows(totalRows - termY - 1, totalRows, tabRows);
|
|
1354
2127
|
}
|
|
1355
|
-
var STATUS_GLYPH = {
|
|
1356
|
-
planned: ["\xB7", "."],
|
|
1357
|
-
"in-progress": ["\u283F", "*"],
|
|
1358
|
-
done: ["\u25A0", "#"],
|
|
1359
|
-
regressed: ["\u2717", "X"]
|
|
1360
|
-
};
|
|
1361
|
-
var STATUS_SGR = {
|
|
1362
|
-
planned: "2",
|
|
1363
|
-
"in-progress": "33",
|
|
1364
|
-
done: "32",
|
|
1365
|
-
regressed: "31"
|
|
1366
|
-
};
|
|
1367
2128
|
function anchorOffsets(anchor, offset, before, after) {
|
|
1368
2129
|
if (anchor) {
|
|
1369
2130
|
return {
|
|
@@ -1377,14 +2138,19 @@ function anchorOffsets(anchor, offset, before, after) {
|
|
|
1377
2138
|
};
|
|
1378
2139
|
}
|
|
1379
2140
|
var TAB_INDICATOR_W = 3;
|
|
1380
|
-
|
|
2141
|
+
var CLOSE_TAB_TEXT = { unicode: "\xD7 ", ascii: "x " };
|
|
2142
|
+
var CLOSE_TAB_SGR = "90";
|
|
2143
|
+
function pageTabRow(tabs, width, unicode, scroll = 0, closable = false) {
|
|
1381
2144
|
const texts = tabs.map((tab) => {
|
|
1382
2145
|
const marker = tab.active ? unicode ? "\u25CF" : "*" : unicode ? "\u25CB" : "o";
|
|
1383
|
-
const glyph =
|
|
2146
|
+
const glyph = statusGlyph(tab.status, unicode);
|
|
1384
2147
|
return tab.neutral === true ? ` ${marker} ${tab.title} ` : ` ${marker} ${glyph} ${tab.title} `;
|
|
1385
2148
|
});
|
|
1386
|
-
const sgrOf = (tab) => tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${
|
|
1387
|
-
const
|
|
2149
|
+
const sgrOf = (tab) => tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${statusSgr(tab.status)};1` : tab.fresh ? statusSgr(tab.status) : "90";
|
|
2150
|
+
const closeText = CLOSE_TAB_TEXT[unicode ? "unicode" : "ascii"];
|
|
2151
|
+
const closeW = displayWidth(closeText);
|
|
2152
|
+
const closeOf = (i) => closable && tabs[i].active ? closeW : 0;
|
|
2153
|
+
const widths = texts.map((t, i) => displayWidth(t) + closeOf(i));
|
|
1388
2154
|
const count = tabs.length;
|
|
1389
2155
|
let lo = 0;
|
|
1390
2156
|
let hi = count - 1;
|
|
@@ -1404,37 +2170,37 @@ function pageTabRow(tabs, width, unicode, scroll = 0) {
|
|
|
1404
2170
|
if (lo > 0) push(unicode ? " \u2039 " : " < ", "90", { kind: "scroll", delta: -1 });
|
|
1405
2171
|
const tail = hi < count - 1 ? TAB_INDICATOR_W : 0;
|
|
1406
2172
|
for (let i = lo; i <= hi; i++) {
|
|
1407
|
-
|
|
2173
|
+
const close = closeOf(i);
|
|
2174
|
+
push(fitWidth(texts[i], Math.max(1, width - (col - 1) - tail - close)), sgrOf(tabs[i]), {
|
|
2175
|
+
kind: "switch",
|
|
2176
|
+
index: i
|
|
2177
|
+
});
|
|
2178
|
+
if (close > 0 && col - 1 + close + tail <= width) push(closeText, CLOSE_TAB_SGR, { kind: "delete" });
|
|
1408
2179
|
}
|
|
1409
2180
|
if (hi < count - 1) push(unicode ? " \u203A " : " > ", "90", { kind: "scroll", delta: 1 });
|
|
1410
2181
|
return segments;
|
|
1411
2182
|
}
|
|
1412
|
-
function tabScrollFor(tabs, width, unicode, scroll, index) {
|
|
2183
|
+
function tabScrollFor(tabs, width, unicode, scroll, index, closable = false) {
|
|
1413
2184
|
if (index <= scroll) return Math.max(0, index);
|
|
1414
|
-
const visibleAt = (s2) => pageTabRow(tabs, width, unicode, s2).some(
|
|
2185
|
+
const visibleAt = (s2) => pageTabRow(tabs, width, unicode, s2, closable).some(
|
|
2186
|
+
(seg) => seg.action.kind === "switch" && seg.action.index === index
|
|
2187
|
+
);
|
|
1415
2188
|
let s = Math.max(0, Math.min(scroll, tabs.length - 1));
|
|
1416
2189
|
while (s < index && !visibleAt(s)) s++;
|
|
1417
2190
|
return s;
|
|
1418
2191
|
}
|
|
1419
|
-
function topLevelFiles(defaultFile, files,
|
|
1420
|
-
const
|
|
1421
|
-
for (const m of mapOf.values()) {
|
|
1422
|
-
for (const n of m?.nodes ?? []) if (n.submap !== void 0) refs.add(n.submap);
|
|
1423
|
-
}
|
|
2192
|
+
function topLevelFiles(defaultFile, files, mapOf2) {
|
|
2193
|
+
const interior = interiorPages(files.map((f) => [pageIdOfFile(defaultFile, f), mapOf2.get(f)]));
|
|
1424
2194
|
return files.filter((f) => {
|
|
1425
2195
|
const id = pageIdOfFile(defaultFile, f);
|
|
1426
|
-
return id === void 0 || !
|
|
2196
|
+
return id === void 0 || !interior.has(id);
|
|
1427
2197
|
});
|
|
1428
2198
|
}
|
|
1429
|
-
function diveOrigin(defaultFile, file, files,
|
|
2199
|
+
function diveOrigin(defaultFile, file, files, mapOf2) {
|
|
1430
2200
|
const id = pageIdOfFile(defaultFile, file);
|
|
1431
2201
|
if (id === void 0) return void 0;
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
const node = mapOf.get(f)?.nodes.find((n) => n.submap === id);
|
|
1435
|
-
if (node !== void 0) return { parent: f, label: node.label };
|
|
1436
|
-
}
|
|
1437
|
-
return void 0;
|
|
2202
|
+
const entries = files.filter((f) => f !== file).map((f) => [f, mapOf2.get(f)]);
|
|
2203
|
+
return diveParent(entries, id);
|
|
1438
2204
|
}
|
|
1439
2205
|
function nearestHit(hits, cx, cy) {
|
|
1440
2206
|
let best;
|
|
@@ -1449,37 +2215,23 @@ function nearestHit(hits, cx, cy) {
|
|
|
1449
2215
|
return best;
|
|
1450
2216
|
}
|
|
1451
2217
|
function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_ROWS) {
|
|
1452
|
-
const g = (s) =>
|
|
2218
|
+
const g = (s) => statusGlyph(s, unicode);
|
|
1453
2219
|
const pinMark = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
|
|
1454
|
-
const
|
|
1455
|
-
if (
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
const status
|
|
1459
|
-
const layerName2 = map.layers.find((l) => l.id === group.layer)?.name ?? group.layer;
|
|
2220
|
+
const focus = focusInfo(map, focusId);
|
|
2221
|
+
if (focus === void 0) return void 0;
|
|
2222
|
+
const refText = (r) => `${g(r.status)} ${r.label}${r.edgeLabel !== void 0 ? ` (${r.edgeLabel})` : ""}`;
|
|
2223
|
+
if (focus.kind === "group") {
|
|
2224
|
+
const { group, status, layerName: layerName2, members } = focus;
|
|
1460
2225
|
const [right2, left2] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
const owner = n.group !== void 0 ? map.groups.find((gr) => gr.id === n.group) : void 0;
|
|
1464
|
-
return owner !== void 0 ? `${g(groupStatus(map, owner.id))} ${owner.label}` : `${g(n.status)} ${n.label}`;
|
|
1465
|
-
};
|
|
1466
|
-
const uses2 = [
|
|
1467
|
-
...new Set(
|
|
1468
|
-
map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => repLabel(e.to))
|
|
1469
|
-
)
|
|
1470
|
-
];
|
|
1471
|
-
const usedBy2 = [
|
|
1472
|
-
...new Set(
|
|
1473
|
-
map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => repLabel(e.from))
|
|
1474
|
-
)
|
|
1475
|
-
];
|
|
2226
|
+
const uses2 = focus.uses.map(refText);
|
|
2227
|
+
const usedBy2 = focus.usedBy.map(refText);
|
|
1476
2228
|
const lines2 = [
|
|
1477
2229
|
{
|
|
1478
2230
|
text: fitWidth(
|
|
1479
2231
|
`${g(status)} ${group.label} [${group.id}] \xB7 ${layerName2} \xB7 ${status} \xB7 ${members.length} member(s)${pinMark}`,
|
|
1480
2232
|
width
|
|
1481
2233
|
),
|
|
1482
|
-
sgr: `${
|
|
2234
|
+
sgr: `${statusSgr(status)};1`
|
|
1483
2235
|
},
|
|
1484
2236
|
{
|
|
1485
2237
|
text: fitWidth(`members: ${members.map((n) => `${g(n.status)} ${n.label}`).join(" ") || "\u2014"}`, width),
|
|
@@ -1491,21 +2243,13 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1491
2243
|
while (lines2.length < rows) lines2.push({ text: "", sgr: "" });
|
|
1492
2244
|
return lines2.slice(0, rows);
|
|
1493
2245
|
}
|
|
1494
|
-
const node
|
|
1495
|
-
if (!node) return void 0;
|
|
2246
|
+
const { node, layerName, laneLabel } = focus;
|
|
1496
2247
|
const neutral = isNeutralKind(map);
|
|
1497
|
-
const layerName = map.layers.find((l) => l.id === node.layer)?.name ?? node.layer;
|
|
1498
2248
|
const [right, left] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
};
|
|
1503
|
-
const withEdgeLabel = (base, label) => label !== void 0 ? `${base} (${label})` : base;
|
|
1504
|
-
const uses = map.edges.filter((e) => e.from === node.id).map((e) => withEdgeLabel(withGlyph(e.to), e.label));
|
|
1505
|
-
const usedBy = map.edges.filter((e) => e.to === node.id).map((e) => withEdgeLabel(withGlyph(e.from), e.label));
|
|
1506
|
-
const pin = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
|
|
2249
|
+
const uses = focus.uses.map(refText);
|
|
2250
|
+
const usedBy = focus.usedBy.map(refText);
|
|
2251
|
+
const pin = pinMark;
|
|
1507
2252
|
const headGlyph = neutral ? (node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0) ?? (unicode ? "\xB7" : ".") : g(node.status);
|
|
1508
|
-
const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
|
|
1509
2253
|
const headParts = [
|
|
1510
2254
|
`${headGlyph} ${node.label} [${node.id}]`,
|
|
1511
2255
|
layerName,
|
|
@@ -1518,7 +2262,7 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1518
2262
|
const lines = [
|
|
1519
2263
|
{
|
|
1520
2264
|
text: fitWidth(`${headParts.join(" \xB7 ")}${pin}`, width),
|
|
1521
|
-
sgr: neutral ? "1" : `${
|
|
2265
|
+
sgr: neutral ? "1" : `${statusSgr(node.status)};1`
|
|
1522
2266
|
},
|
|
1523
2267
|
{ text: fitWidth(`evidence: ${node.evidence ?? "\u2014"}`, width), sgr: "90" },
|
|
1524
2268
|
{ text: fitWidth(`${usesWord} ${right} ${uses.join(" ") || "\u2014"}`, width), sgr: "" },
|
|
@@ -1535,17 +2279,6 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1535
2279
|
}
|
|
1536
2280
|
return lines.slice(0, rows);
|
|
1537
2281
|
}
|
|
1538
|
-
var WATER_ROWS = 7;
|
|
1539
|
-
var WATER_COLS_MAX = 60;
|
|
1540
|
-
var SPLASH_SHADES = {
|
|
1541
|
-
unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
|
|
1542
|
-
ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
|
|
1543
|
-
};
|
|
1544
|
-
var WAVE_RAMP = [17, 18, 19, 61, 24, 25, 31, 37, 44, 45, 51, 87, 123, 159, 195];
|
|
1545
|
-
var SPINNER_FRAMES = {
|
|
1546
|
-
unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
1547
|
-
ascii: ["|", "/", "-", "\\"]
|
|
1548
|
-
};
|
|
1549
2282
|
function elapsedLabel(ms) {
|
|
1550
2283
|
const s = Math.max(0, Math.floor(ms / 1e3));
|
|
1551
2284
|
const m = Math.floor(s / 60);
|
|
@@ -1565,95 +2298,19 @@ function waitingInfo(s, width) {
|
|
|
1565
2298
|
lines.push("the map appears at the first mmap_declare");
|
|
1566
2299
|
return lines.map((l) => fitWidth(l, w));
|
|
1567
2300
|
}
|
|
1568
|
-
var WAVE_INTERVAL = 18;
|
|
1569
|
-
var WAVE_LIFETIME = 64;
|
|
1570
|
-
var WAVE_SPEED = 0.9;
|
|
1571
|
-
var WAVE_ENVELOPE = 10;
|
|
1572
|
-
var WAVE_NUMBER = 0.42;
|
|
1573
|
-
var WAVE_LEVELS = 7;
|
|
1574
|
-
var WAVE_GAIN = 4.5;
|
|
1575
|
-
function waveHash(n) {
|
|
1576
|
-
let h = Math.imul(n + 1, 2654435761) >>> 0;
|
|
1577
|
-
h ^= h >>> 15;
|
|
1578
|
-
h = Math.imul(h, 2246822519) >>> 0;
|
|
1579
|
-
h ^= h >>> 13;
|
|
1580
|
-
return h >>> 0;
|
|
1581
|
-
}
|
|
1582
|
-
var WAVE_CORNERS = [
|
|
1583
|
-
[0, 0],
|
|
1584
|
-
[1, 0],
|
|
1585
|
-
[0, 1],
|
|
1586
|
-
[1, 1]
|
|
1587
|
-
];
|
|
1588
|
-
function liveRipples(frame, width, height) {
|
|
1589
|
-
const out = [];
|
|
1590
|
-
const first = Math.floor((frame - WAVE_LIFETIME - WAVE_INTERVAL) / WAVE_INTERVAL);
|
|
1591
|
-
const last = Math.floor(frame / WAVE_INTERVAL);
|
|
1592
|
-
for (let n = Math.max(0, first); n <= last; n++) {
|
|
1593
|
-
const h = waveHash(n);
|
|
1594
|
-
const age = frame - (n * WAVE_INTERVAL + h % WAVE_INTERVAL);
|
|
1595
|
-
if (age < 0 || age > WAVE_LIFETIME) continue;
|
|
1596
|
-
const [fx, fy] = WAVE_CORNERS[h % WAVE_CORNERS.length];
|
|
1597
|
-
out.push({
|
|
1598
|
-
ox: fx * (width - 1),
|
|
1599
|
-
oy: fy * (height - 1),
|
|
1600
|
-
r: age * WAVE_SPEED,
|
|
1601
|
-
fade: 1 - age / WAVE_LIFETIME
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
return out;
|
|
1605
|
-
}
|
|
1606
|
-
function waveAt(ripples, x, y) {
|
|
1607
|
-
let value = 0;
|
|
1608
|
-
for (const w of ripples) {
|
|
1609
|
-
const front = Math.hypot(x - w.ox, (y - w.oy) * 2) - w.r;
|
|
1610
|
-
value += Math.cos(front * WAVE_NUMBER) * Math.exp(-(front * front) / (2 * WAVE_ENVELOPE ** 2)) * w.fade;
|
|
1611
|
-
}
|
|
1612
|
-
return value;
|
|
1613
|
-
}
|
|
1614
|
-
function waveLevel(value) {
|
|
1615
|
-
return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
|
|
1616
|
-
}
|
|
1617
2301
|
function splashFrame(notice, info, frame, width, height, unicode, color) {
|
|
1618
|
-
|
|
1619
|
-
if (fieldW < 24 || height < WATER_ROWS + info.length + 3) return void 0;
|
|
1620
|
-
const mode = unicode ? "unicode" : "ascii";
|
|
1621
|
-
const shades = SPLASH_SHADES[mode];
|
|
1622
|
-
const indent = " ".repeat(Math.max(0, Math.floor((width - fieldW) / 2)));
|
|
1623
|
-
const ripples = liveRipples(frame, fieldW, WATER_ROWS);
|
|
1624
|
-
const paintRow = (y) => {
|
|
1625
|
-
const levels = Array.from({ length: fieldW }, (_, x) => waveLevel(waveAt(ripples, x, y)));
|
|
1626
|
-
let out = "";
|
|
1627
|
-
for (let i = 0; i < fieldW; ) {
|
|
1628
|
-
const level = levels[i];
|
|
1629
|
-
let j = i;
|
|
1630
|
-
while (j < fieldW && levels[j] === level) j++;
|
|
1631
|
-
if (level === 0) out += " ".repeat(j - i);
|
|
1632
|
-
else {
|
|
1633
|
-
const ink = shades[Math.abs(level)].repeat(j - i);
|
|
1634
|
-
out += color ? `\x1B[38;5;${WAVE_RAMP[WAVE_LEVELS + level]}m${ink}${RESET}` : ink;
|
|
1635
|
-
}
|
|
1636
|
-
i = j;
|
|
1637
|
-
}
|
|
1638
|
-
return out;
|
|
1639
|
-
};
|
|
2302
|
+
if (width < 24 || height < info.length + 3) return void 0;
|
|
1640
2303
|
const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
|
|
1641
|
-
const spinner = SPINNER_FRAMES[
|
|
2304
|
+
const spinner = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
|
|
1642
2305
|
const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
|
|
1643
2306
|
const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
|
|
1644
2307
|
const infoWidth = Math.max(0, ...info.map((l) => displayWidth(l)));
|
|
1645
2308
|
const infoIndent = " ".repeat(Math.max(0, Math.floor((width - infoWidth) / 2)));
|
|
1646
|
-
const block = [
|
|
1647
|
-
...Array.from({ length: WATER_ROWS }, (_, y) => indent + paintRow(y)),
|
|
1648
|
-
"",
|
|
1649
|
-
statusIndent + dim(status),
|
|
1650
|
-
"",
|
|
1651
|
-
...info.map((l) => infoIndent + dim(l))
|
|
1652
|
-
];
|
|
2309
|
+
const block = [statusIndent + dim(status), "", ...info.map((l) => infoIndent + dim(l))];
|
|
1653
2310
|
return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
|
|
1654
2311
|
}
|
|
1655
2312
|
function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
1656
|
-
const g = (s) =>
|
|
2313
|
+
const g = (s) => statusGlyph(s, unicode);
|
|
1657
2314
|
const count = (s) => map.nodes.filter((n) => n.status === s).length;
|
|
1658
2315
|
const statuses = ["done", "in-progress", "planned", "regressed"];
|
|
1659
2316
|
const counts = statuses.filter((s) => count(s) > 0).map((s) => `${g(s)} ${count(s)} ${s}`).join(" ");
|
|
@@ -1670,8 +2327,20 @@ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
|
1670
2327
|
while (lines.length < rows) lines.push({ text: "", sgr: "" });
|
|
1671
2328
|
return lines.slice(0, rows);
|
|
1672
2329
|
}
|
|
2330
|
+
function viewerReportOf(pane, defaultFile) {
|
|
2331
|
+
const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
|
|
2332
|
+
return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
|
|
2333
|
+
}
|
|
1673
2334
|
function main() {
|
|
1674
|
-
const
|
|
2335
|
+
const parsed = parseArgs(process.argv.slice(2), process.cwd());
|
|
2336
|
+
if (!parsed.ok) {
|
|
2337
|
+
console.error(`mellos-mapping-watch: ${describeArgsError(parsed.error)}
|
|
2338
|
+
${USAGE}`);
|
|
2339
|
+
process.exit(1);
|
|
2340
|
+
}
|
|
2341
|
+
const cfg = parsed.value;
|
|
2342
|
+
if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
|
|
2343
|
+
sweepQuitRequest(cfg.file);
|
|
1675
2344
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1676
2345
|
const mouseActive = interactive && cfg.mouse;
|
|
1677
2346
|
let lastFrame = "";
|
|
@@ -1683,13 +2352,11 @@ function main() {
|
|
|
1683
2352
|
let notice = standbyNotice;
|
|
1684
2353
|
let lastCols = process.stdout.columns ?? 0;
|
|
1685
2354
|
let lastRows = process.stdout.rows ?? 0;
|
|
1686
|
-
let
|
|
1687
|
-
|
|
2355
|
+
let pane = initialPaneState(
|
|
2356
|
+
cfg.follow,
|
|
2357
|
+
cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
|
|
2358
|
+
);
|
|
1688
2359
|
const pageViews = /* @__PURE__ */ new Map();
|
|
1689
|
-
let activeFile;
|
|
1690
|
-
let firstScan = true;
|
|
1691
|
-
let pendingFocusFile = cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page);
|
|
1692
|
-
let follow = cfg.follow;
|
|
1693
2360
|
let lastTabSegments = [];
|
|
1694
2361
|
let tabScroll = 0;
|
|
1695
2362
|
let offsetX = 0;
|
|
@@ -1705,62 +2372,72 @@ function main() {
|
|
|
1705
2372
|
let panelContentRows = PANEL_CONTENT_ROWS;
|
|
1706
2373
|
let dividerDrag = false;
|
|
1707
2374
|
let lastClick;
|
|
1708
|
-
const diveStack = [];
|
|
1709
2375
|
let flash;
|
|
1710
2376
|
let lastTabFiles = [];
|
|
1711
|
-
const
|
|
1712
|
-
const
|
|
1713
|
-
const inSubmap = () => activeFile !== void 0 && !topFiles().includes(activeFile);
|
|
2377
|
+
const topFiles = () => topLevelFiles(cfg.file, filesOf(pane), mapsOf(pane));
|
|
2378
|
+
const inSubmap = () => pane.activeFile !== void 0 && !topFiles().includes(pane.activeFile);
|
|
1714
2379
|
const tabRows = () => topFiles().length > 1 || inSubmap() ? 1 : 0;
|
|
1715
2380
|
const climbBack = () => {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
if (parent !== void 0 && parent !== activeFile) {
|
|
2381
|
+
const climbed = popDive(pane);
|
|
2382
|
+
pane = climbed.state;
|
|
2383
|
+
const parent = climbed.parent ?? (pane.activeFile !== void 0 ? diveOrigin(cfg.file, pane.activeFile, filesOf(pane), mapsOf(pane))?.parent : void 0);
|
|
2384
|
+
if (parent !== void 0 && parent !== pane.activeFile) {
|
|
1722
2385
|
handSwitch(parent);
|
|
1723
2386
|
return true;
|
|
1724
2387
|
}
|
|
1725
2388
|
return false;
|
|
1726
2389
|
};
|
|
1727
|
-
const viewWidth = () => usableColumns(process.stdout.columns ??
|
|
1728
|
-
const viewHeight = () => Math.max(1, (process.stdout.rows ??
|
|
2390
|
+
const viewWidth = () => usableColumns(process.stdout.columns ?? FALLBACK_COLUMNS);
|
|
2391
|
+
const viewHeight = () => Math.max(1, (process.stdout.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
|
|
1729
2392
|
const dividerY = () => tabRows() + viewHeight() + 1;
|
|
1730
2393
|
const pageTabsOf = (files) => files.map((f) => {
|
|
1731
|
-
const
|
|
2394
|
+
const entry = entryOf(pane, f);
|
|
2395
|
+
const m = mapOf(entry);
|
|
1732
2396
|
return {
|
|
1733
|
-
title: m?.title ?? (pageIdOfFile(cfg.file, f) ??
|
|
2397
|
+
title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? DEFAULT_PAGE_TAB_LABEL),
|
|
1734
2398
|
status: m !== void 0 ? mapStatus(m) : "planned",
|
|
1735
|
-
active: f === activeFile,
|
|
1736
|
-
fresh:
|
|
2399
|
+
active: f === pane.activeFile,
|
|
2400
|
+
fresh: entry?.fresh ?? false,
|
|
1737
2401
|
neutral: m !== void 0 && isNeutralKind(m)
|
|
1738
2402
|
};
|
|
1739
2403
|
});
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
2404
|
+
const noticeFor = (file) => {
|
|
2405
|
+
const entry = entryOf(pane, file);
|
|
2406
|
+
if (entry === void 0 || entry.state.kind === "absent") {
|
|
2407
|
+
return file === void 0 || file === cfg.file ? standbyNotice : `waiting for ${file} ...`;
|
|
2408
|
+
}
|
|
2409
|
+
if (entry.state.kind === "loaded") return "";
|
|
2410
|
+
return entry.state.transient && entry.state.lastGood !== void 0 ? "" : describePageFault(entry.state.fault);
|
|
2411
|
+
};
|
|
2412
|
+
const adoptPage = () => {
|
|
2413
|
+
map = mapOf(entryOf(pane, pane.activeFile));
|
|
2414
|
+
notice = noticeFor(pane.activeFile);
|
|
2415
|
+
};
|
|
2416
|
+
const adoptView = (previous) => {
|
|
2417
|
+
const file = pane.activeFile;
|
|
2418
|
+
if (file === void 0 || file === previous) return;
|
|
2419
|
+
if (previous !== void 0) pageViews.set(previous, { offsetX, offsetY, zoom, selectedId });
|
|
1743
2420
|
const view = pageViews.get(file);
|
|
1744
2421
|
offsetX = view?.offsetX ?? 0;
|
|
1745
2422
|
offsetY = view?.offsetY ?? 0;
|
|
1746
2423
|
zoom = view?.zoom ?? ZOOM_DEFAULT;
|
|
1747
2424
|
selectedId = view?.selectedId;
|
|
1748
2425
|
hoverId = void 0;
|
|
1749
|
-
const entry = pageData.get(file);
|
|
1750
|
-
if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
|
|
1751
|
-
map = entry?.map;
|
|
1752
|
-
notice = map !== void 0 ? "" : entry?.error ?? (file === cfg.file ? standbyNotice : `waiting for ${file} ...`);
|
|
1753
2426
|
const top = topFiles();
|
|
1754
2427
|
const tabIndex = top.indexOf(file);
|
|
1755
|
-
if (tabIndex >= 0)
|
|
2428
|
+
if (tabIndex >= 0) {
|
|
2429
|
+
tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex, mouseActive);
|
|
2430
|
+
}
|
|
1756
2431
|
};
|
|
1757
2432
|
const handSwitch = (file) => {
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2433
|
+
const previous = pane.activeFile;
|
|
2434
|
+
const switched = userSwitch(pane, file);
|
|
2435
|
+
pane = switched.state;
|
|
2436
|
+
if (switched.followTurnedOff) {
|
|
2437
|
+
flash = { text: "auto-follow off \u2014 press f to re-enable", until: Date.now() + FLASH_NOTICE_MS };
|
|
1762
2438
|
}
|
|
1763
|
-
|
|
2439
|
+
adoptView(previous);
|
|
2440
|
+
adoptPage();
|
|
1764
2441
|
};
|
|
1765
2442
|
const hitTest = (termX, termY) => {
|
|
1766
2443
|
const sx = termX - 1;
|
|
@@ -1771,41 +2448,57 @@ function main() {
|
|
|
1771
2448
|
const cy = sy + offsetY;
|
|
1772
2449
|
return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
|
|
1773
2450
|
};
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
process.stdout.write((mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + "\n");
|
|
1777
|
-
process.exit(0);
|
|
2451
|
+
const publishPresence = () => {
|
|
2452
|
+
publishViewer(cfg.file, process.pid, viewerReportOf(pane, cfg.file));
|
|
1778
2453
|
};
|
|
1779
|
-
process.
|
|
1780
|
-
process.on("
|
|
1781
|
-
|
|
1782
|
-
|
|
2454
|
+
process.stdout.write(HIDE_CURSOR + CLEAR_ALL + (mouseActive ? MOUSE_ON : ""));
|
|
2455
|
+
process.on("exit", () => {
|
|
2456
|
+
process.stdout.write(terminalRestoreSequence(mouseActive));
|
|
2457
|
+
retireViewer(cfg.file, process.pid);
|
|
2458
|
+
});
|
|
2459
|
+
const quit = () => process.exit(0);
|
|
2460
|
+
process.on("SIGINT", quit);
|
|
2461
|
+
process.on("SIGTERM", quit);
|
|
2462
|
+
process.on("uncaughtException", (e) => {
|
|
2463
|
+
process.stderr.write(`
|
|
2464
|
+
the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
2465
|
+
`);
|
|
2466
|
+
process.exit(1);
|
|
2467
|
+
});
|
|
2468
|
+
const paint2 = () => {
|
|
2469
|
+
const cols = process.stdout.columns ?? FALLBACK_COLUMNS;
|
|
1783
2470
|
const viewW = viewWidth();
|
|
1784
|
-
panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ??
|
|
2471
|
+
panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
|
|
1785
2472
|
const viewH = viewHeight();
|
|
1786
2473
|
const focus = hoverId ?? selectedId;
|
|
1787
2474
|
let body;
|
|
1788
2475
|
let panned = "";
|
|
1789
2476
|
let pannable = false;
|
|
1790
2477
|
if (map !== void 0) {
|
|
1791
|
-
const
|
|
2478
|
+
const rendered = renderWindow(
|
|
1792
2479
|
map,
|
|
1793
2480
|
{ color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
|
|
1794
2481
|
{ x: offsetX, y: offsetY, width: viewW, height: viewH }
|
|
1795
2482
|
);
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2483
|
+
if (!rendered.ok) {
|
|
2484
|
+
body = ["", fitWidth(` ! ${rendered.error}`, viewW), ""];
|
|
2485
|
+
lastHits = [];
|
|
2486
|
+
} else {
|
|
2487
|
+
const windowed = rendered.value;
|
|
2488
|
+
const maxX = Math.max(0, windowed.contentWidth - viewW);
|
|
2489
|
+
const maxY = Math.max(0, windowed.contentHeight - viewH);
|
|
2490
|
+
if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
|
|
2491
|
+
offsetX = Math.min(Math.max(0, offsetX), maxX);
|
|
2492
|
+
offsetY = Math.min(Math.max(0, offsetY), maxY);
|
|
2493
|
+
paint2();
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
pannable = maxX > 0 || maxY > 0;
|
|
2497
|
+
body = windowed.lines;
|
|
2498
|
+
lastHits = windowed.hits;
|
|
2499
|
+
lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
|
|
2500
|
+
if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
|
|
1803
2501
|
}
|
|
1804
|
-
pannable = maxX > 0 || maxY > 0;
|
|
1805
|
-
body = windowed.lines;
|
|
1806
|
-
lastHits = windowed.hits;
|
|
1807
|
-
lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
|
|
1808
|
-
if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
|
|
1809
2502
|
} else {
|
|
1810
2503
|
const info = waitingInfo(
|
|
1811
2504
|
{
|
|
@@ -1813,7 +2506,9 @@ function main() {
|
|
|
1813
2506
|
pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
|
|
1814
2507
|
intervalMs: cfg.intervalMs,
|
|
1815
2508
|
elapsedMs: interactive ? Date.now() - startedAt : void 0,
|
|
1816
|
-
broken:
|
|
2509
|
+
broken: pane.pages.flatMap(
|
|
2510
|
+
(p) => p.state.kind === "faulted" && p.state.lastGood === void 0 ? [describePageFault(p.state.fault)] : []
|
|
2511
|
+
)
|
|
1817
2512
|
},
|
|
1818
2513
|
Math.max(1, viewW - 2)
|
|
1819
2514
|
);
|
|
@@ -1831,7 +2526,7 @@ function main() {
|
|
|
1831
2526
|
} else {
|
|
1832
2527
|
panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
|
|
1833
2528
|
}
|
|
1834
|
-
const separator = dividerRow(viewW, cfg.unicode, follow);
|
|
2529
|
+
const separator = dividerRow(viewW, cfg.unicode, pane.follow);
|
|
1835
2530
|
const panelRows = [
|
|
1836
2531
|
cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
|
|
1837
2532
|
...panel.map(
|
|
@@ -1840,12 +2535,12 @@ function main() {
|
|
|
1840
2535
|
];
|
|
1841
2536
|
let tabLine;
|
|
1842
2537
|
lastTabFiles = topFiles();
|
|
1843
|
-
if (inSubmap() && activeFile !== void 0) {
|
|
1844
|
-
const stackParent = [
|
|
1845
|
-
const
|
|
1846
|
-
const parentFile = stackParent ??
|
|
1847
|
-
const parentTitle = parentFile !== void 0 ?
|
|
1848
|
-
const nodeLabel =
|
|
2538
|
+
if (inSubmap() && pane.activeFile !== void 0) {
|
|
2539
|
+
const stackParent = pane.diveStack[pane.diveStack.length - 1];
|
|
2540
|
+
const origin = diveOrigin(cfg.file, pane.activeFile, filesOf(pane), mapsOf(pane));
|
|
2541
|
+
const parentFile = stackParent ?? origin?.parent;
|
|
2542
|
+
const parentTitle = parentFile !== void 0 ? mapOf(entryOf(pane, parentFile))?.title ?? (pageIdOfFile(cfg.file, parentFile) ?? DEFAULT_PAGE_TAB_LABEL) : DEFAULT_PAGE_TAB_LABEL;
|
|
2543
|
+
const nodeLabel = origin?.label ?? map?.title ?? "";
|
|
1849
2544
|
const crumbHead = ` ${cfg.unicode ? "\u232B" : "<"} ${parentTitle} ${cfg.unicode ? "\u25B8" : ">"} `;
|
|
1850
2545
|
const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead), action: { kind: "back" } };
|
|
1851
2546
|
const tailText = fitWidth(`${nodeLabel} `, Math.max(1, viewW - displayWidth(crumbHead)));
|
|
@@ -1859,14 +2554,14 @@ function main() {
|
|
|
1859
2554
|
lastTabSegments = [head, tail];
|
|
1860
2555
|
tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
1861
2556
|
} else if (tabRows() > 0) {
|
|
1862
|
-
const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll);
|
|
2557
|
+
const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll, mouseActive);
|
|
1863
2558
|
lastTabSegments = segments;
|
|
1864
2559
|
tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
1865
2560
|
} else {
|
|
1866
2561
|
lastTabSegments = [];
|
|
1867
2562
|
}
|
|
1868
2563
|
const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
|
|
1869
|
-
const hint = !interactive ? cfg.file : (flash !== void 0 ? `${flash.text} \xB7 ` : "") + `${zoomTag} \xB7 wheel zoom \xB7 ` + (pannable ? "drag pan \xB7 " : "") + "hover/click \xB7 0 reset \xB7 q quit";
|
|
2564
|
+
const hint = !interactive ? cfg.file : (flash !== void 0 ? `${flash.text} \xB7 ` : "") + `${zoomTag} \xB7 wheel zoom \xB7 ` + (pannable ? "drag pan \xB7 " : "") + "hover/click \xB7 0 reset \xB7 x delete page \xB7 q quit";
|
|
1870
2565
|
const footerText = fitWidth(` ${hint}${panned}`, viewW);
|
|
1871
2566
|
const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
|
|
1872
2567
|
let frame = HOME;
|
|
@@ -1884,89 +2579,87 @@ function main() {
|
|
|
1884
2579
|
lastRows = process.stdout.rows ?? lastRows;
|
|
1885
2580
|
lastFrame = "";
|
|
1886
2581
|
process.stdout.write(CLEAR_ALL);
|
|
1887
|
-
|
|
2582
|
+
paint2();
|
|
1888
2583
|
};
|
|
1889
2584
|
const tick = () => {
|
|
2585
|
+
if (takeQuitRequest(cfg.file)) quit();
|
|
1890
2586
|
if ((process.stdout.columns ?? lastCols) !== lastCols || (process.stdout.rows ?? lastRows) !== lastRows) {
|
|
1891
2587
|
handleResize();
|
|
1892
2588
|
}
|
|
1893
2589
|
const discovered = listPageFiles(cfg.file);
|
|
1894
|
-
|
|
1895
|
-
for (const known of [...pageData.keys()]) {
|
|
1896
|
-
if (!pageFiles.includes(known)) {
|
|
1897
|
-
pageData.delete(known);
|
|
1898
|
-
pageViews.delete(known);
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
const changedFiles = [];
|
|
1902
|
-
for (const file of pageFiles) {
|
|
1903
|
-
let mtimeMs;
|
|
1904
|
-
try {
|
|
1905
|
-
mtimeMs = statSync(file).mtimeMs;
|
|
1906
|
-
} catch {
|
|
1907
|
-
continue;
|
|
1908
|
-
}
|
|
1909
|
-
const entry = pageData.get(file);
|
|
1910
|
-
if (mtimeMs === entry?.mtimeMs) continue;
|
|
1911
|
-
const loaded = loadMapFile(file);
|
|
1912
|
-
if (loaded.ok) {
|
|
1913
|
-
if (!firstScan) changedFiles.push(file);
|
|
1914
|
-
const becameFresh = !firstScan && file !== activeFile;
|
|
1915
|
-
pageData.set(file, { map: loaded.value, mtimeMs, fresh: becameFresh });
|
|
1916
|
-
if (file === activeFile) {
|
|
1917
|
-
map = loaded.value;
|
|
1918
|
-
notice = "";
|
|
1919
|
-
} else if (becameFresh && !topFiles().includes(file)) {
|
|
1920
|
-
const title = loaded.value.title ?? (pageIdOfFile(cfg.file, file) ?? "?");
|
|
1921
|
-
flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + 4e3 };
|
|
1922
|
-
}
|
|
1923
|
-
} else if (loaded.error.kind === "malformed-json") {
|
|
1924
|
-
pageData.set(file, {
|
|
1925
|
-
map: entry?.map,
|
|
1926
|
-
mtimeMs: entry?.mtimeMs ?? -1,
|
|
1927
|
-
fresh: entry?.fresh ?? false,
|
|
1928
|
-
error: describeStoreError(loaded.error)
|
|
1929
|
-
});
|
|
1930
|
-
if (file === activeFile && entry?.map === void 0) notice = describeStoreError(loaded.error);
|
|
1931
|
-
} else {
|
|
1932
|
-
pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false, error: describeStoreError(loaded.error) });
|
|
1933
|
-
if (file === activeFile) notice = describeStoreError(loaded.error);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
2590
|
+
const files = discovered.length > 0 ? discovered : [cfg.file];
|
|
1936
2591
|
const request = takeFocusRequest(cfg.file);
|
|
1937
|
-
|
|
1938
|
-
|
|
2592
|
+
const previous = pane.activeFile;
|
|
2593
|
+
const scanned = scan(pane, {
|
|
2594
|
+
files,
|
|
2595
|
+
mtimeAt: (file) => {
|
|
2596
|
+
try {
|
|
2597
|
+
return statSync2(file).mtimeMs;
|
|
2598
|
+
} catch {
|
|
2599
|
+
return void 0;
|
|
2600
|
+
}
|
|
2601
|
+
},
|
|
2602
|
+
load: readPage,
|
|
2603
|
+
focusRequest: request === void 0 ? void 0 : pageFilePath(cfg.file, request.page),
|
|
2604
|
+
// a drag in progress holds auto-follow off: the user is engaged with
|
|
2605
|
+
// THIS page, and a missed switch is re-triggered by the next save
|
|
2606
|
+
engaged: dragAnchor !== void 0
|
|
2607
|
+
});
|
|
2608
|
+
pane = scanned.state;
|
|
2609
|
+
for (const known of [...pageViews.keys()]) {
|
|
2610
|
+
if (!files.includes(known)) pageViews.delete(known);
|
|
1939
2611
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2612
|
+
adoptView(previous);
|
|
2613
|
+
adoptPage();
|
|
2614
|
+
if (flash?.confirm === true && pane.pendingDelete === void 0) flash = void 0;
|
|
2615
|
+
const top = topFiles();
|
|
2616
|
+
for (const file of scanned.freshened) {
|
|
2617
|
+
if (top.includes(file)) continue;
|
|
2618
|
+
const title = mapOf(entryOf(pane, file))?.title ?? (pageIdOfFile(cfg.file, file) ?? "?");
|
|
2619
|
+
flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + FLASH_BACKGROUND_NEWS_MS };
|
|
1945
2620
|
}
|
|
1946
|
-
if (
|
|
1947
|
-
|
|
1948
|
-
|
|
2621
|
+
if (pane.pages.some((p) => mapOf(p)?.nodes.some((n) => n.status === "in-progress"))) spinnerFrame++;
|
|
2622
|
+
if (flash !== void 0 && Date.now() > flash.until) flash = void 0;
|
|
2623
|
+
paint2();
|
|
2624
|
+
};
|
|
2625
|
+
const pageName = (file) => pageIdOfFile(cfg.file, file) ?? DEFAULT_PAGE_TAB_LABEL;
|
|
2626
|
+
const askDelete = () => {
|
|
2627
|
+
const now = Date.now();
|
|
2628
|
+
const asked = requestDelete(pane, now, CONFIRM_WINDOW_MS);
|
|
2629
|
+
pane = asked.state;
|
|
2630
|
+
if (asked.request.kind === "none") return;
|
|
2631
|
+
if (asked.request.kind === "absent") {
|
|
2632
|
+
flash = {
|
|
2633
|
+
text: `nothing to delete \u2014 ${pageName(asked.request.file)} has no file`,
|
|
2634
|
+
until: now + FLASH_NOTICE_MS
|
|
2635
|
+
};
|
|
2636
|
+
return;
|
|
1949
2637
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2638
|
+
if (asked.request.kind === "armed") {
|
|
2639
|
+
flash = {
|
|
2640
|
+
text: `press x again to delete ${pageName(asked.request.file)} \u2014 its file is removed`,
|
|
2641
|
+
until: asked.request.until,
|
|
2642
|
+
confirm: true
|
|
2643
|
+
};
|
|
2644
|
+
return;
|
|
1953
2645
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2646
|
+
const file = asked.request.file;
|
|
2647
|
+
const removed = deletePageFile(file);
|
|
2648
|
+
flash = removed.ok ? { text: `deleted ${pageName(file)}`, until: now + FLASH_ACK_MS } : { text: describeStoreError(removed.error), until: now + FLASH_NOTICE_MS };
|
|
2649
|
+
tick();
|
|
1957
2650
|
};
|
|
1958
2651
|
if (interactive) {
|
|
1959
2652
|
process.stdin.setRawMode(true);
|
|
1960
2653
|
process.stdin.resume();
|
|
1961
2654
|
process.stdin.setEncoding("utf8");
|
|
1962
2655
|
process.stdin.on("data", (chunk) => {
|
|
1963
|
-
const
|
|
1964
|
-
pendingInput =
|
|
2656
|
+
const parsed2 = parseInput(pendingInput + chunk);
|
|
2657
|
+
pendingInput = parsed2.rest;
|
|
1965
2658
|
let dirty = false;
|
|
1966
|
-
for (const event of
|
|
2659
|
+
for (const event of parsed2.events) {
|
|
1967
2660
|
switch (event.kind) {
|
|
1968
2661
|
case "quit":
|
|
1969
|
-
|
|
2662
|
+
quit();
|
|
1970
2663
|
return;
|
|
1971
2664
|
case "reset":
|
|
1972
2665
|
offsetX = 0;
|
|
@@ -1975,7 +2668,10 @@ function main() {
|
|
|
1975
2668
|
dirty = true;
|
|
1976
2669
|
break;
|
|
1977
2670
|
case "clear":
|
|
1978
|
-
if (
|
|
2671
|
+
if (pane.pendingDelete !== void 0) {
|
|
2672
|
+
pane = disarmDelete(pane);
|
|
2673
|
+
flash = void 0;
|
|
2674
|
+
} else if (selectedId !== void 0) selectedId = void 0;
|
|
1979
2675
|
else climbBack();
|
|
1980
2676
|
dirty = true;
|
|
1981
2677
|
break;
|
|
@@ -1995,11 +2691,16 @@ function main() {
|
|
|
1995
2691
|
const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
|
|
1996
2692
|
const before = lastHits.find((h) => h.id === anchorId);
|
|
1997
2693
|
zoom = next;
|
|
1998
|
-
const
|
|
2694
|
+
const measured = renderWindow(
|
|
1999
2695
|
map,
|
|
2000
2696
|
{ color: false, unicode: cfg.unicode, spinnerFrame: 0, zoom },
|
|
2001
2697
|
{ x: 0, y: 0, width: 0, height: 0 }
|
|
2002
2698
|
);
|
|
2699
|
+
if (!measured.ok) {
|
|
2700
|
+
dirty = true;
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2703
|
+
const sized = measured.value;
|
|
2003
2704
|
const after = before === void 0 ? void 0 : sized.hits.find((h) => h.id === before.id);
|
|
2004
2705
|
const moved = anchorOffsets(
|
|
2005
2706
|
before !== void 0 && after !== void 0 ? { before, after } : void 0,
|
|
@@ -2030,7 +2731,7 @@ function main() {
|
|
|
2030
2731
|
break;
|
|
2031
2732
|
case "mouse-drag":
|
|
2032
2733
|
if (dividerDrag) {
|
|
2033
|
-
const next = panelRowsFromDividerY(event.y, process.stdout.rows ??
|
|
2734
|
+
const next = panelRowsFromDividerY(event.y, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
|
|
2034
2735
|
if (next !== panelContentRows) {
|
|
2035
2736
|
panelContentRows = next;
|
|
2036
2737
|
dirty = true;
|
|
@@ -2060,22 +2761,25 @@ function main() {
|
|
|
2060
2761
|
climbBack();
|
|
2061
2762
|
} else if (tabHit.action.kind === "scroll") {
|
|
2062
2763
|
tabScroll = Math.max(0, Math.min(tabScroll + tabHit.action.delta, lastTabFiles.length - 1));
|
|
2764
|
+
} else if (tabHit.action.kind === "delete") {
|
|
2765
|
+
askDelete();
|
|
2063
2766
|
} else {
|
|
2064
2767
|
const target = lastTabFiles[tabHit.action.index];
|
|
2065
|
-
if (target !== void 0 && target !== activeFile) handSwitch(target);
|
|
2768
|
+
if (target !== void 0 && target !== pane.activeFile) handSwitch(target);
|
|
2066
2769
|
}
|
|
2067
2770
|
} else {
|
|
2068
2771
|
const id = hitTest(event.x, event.y);
|
|
2069
2772
|
const now = Date.now();
|
|
2070
|
-
if (id !== void 0 && lastClick?.id === id && now - lastClick.at <=
|
|
2773
|
+
if (id !== void 0 && lastClick?.id === id && now - lastClick.at <= DOUBLE_CLICK_MS) {
|
|
2071
2774
|
const submap = map?.nodes.find((n) => n.id === id)?.submap;
|
|
2072
|
-
if (submap !== void 0 && activeFile !== void 0) {
|
|
2775
|
+
if (submap !== void 0 && pane.activeFile !== void 0) {
|
|
2073
2776
|
const target = pageFilePath(cfg.file, submap);
|
|
2074
|
-
|
|
2075
|
-
|
|
2777
|
+
const files = filesOf(pane);
|
|
2778
|
+
if (files.includes(target) && target !== pane.activeFile) {
|
|
2779
|
+
pane = pushDive(pane, pane.activeFile);
|
|
2076
2780
|
handSwitch(target);
|
|
2077
|
-
} else if (!
|
|
2078
|
-
flash = { text: `submap "${submap}" has no page yet`, until: now +
|
|
2781
|
+
} else if (!files.includes(target)) {
|
|
2782
|
+
flash = { text: `submap "${submap}" has no page yet`, until: now + FLASH_ACK_MS };
|
|
2079
2783
|
}
|
|
2080
2784
|
}
|
|
2081
2785
|
lastClick = void 0;
|
|
@@ -2092,11 +2796,11 @@ function main() {
|
|
|
2092
2796
|
case "next-page":
|
|
2093
2797
|
case "prev-page": {
|
|
2094
2798
|
const top = topFiles();
|
|
2095
|
-
if (top.length > 0 && activeFile !== void 0) {
|
|
2096
|
-
const current = top.indexOf(activeFile);
|
|
2799
|
+
if (top.length > 0 && pane.activeFile !== void 0) {
|
|
2800
|
+
const current = top.indexOf(pane.activeFile);
|
|
2097
2801
|
const step = event.kind === "next-page" ? 1 : -1;
|
|
2098
2802
|
const target = top[(current + step + top.length) % top.length];
|
|
2099
|
-
if (target !== activeFile) {
|
|
2803
|
+
if (target !== pane.activeFile) {
|
|
2100
2804
|
handSwitch(target);
|
|
2101
2805
|
dirty = true;
|
|
2102
2806
|
}
|
|
@@ -2105,7 +2809,7 @@ function main() {
|
|
|
2105
2809
|
}
|
|
2106
2810
|
case "page": {
|
|
2107
2811
|
const target = topFiles()[event.index];
|
|
2108
|
-
if (target !== void 0 && target !== activeFile) {
|
|
2812
|
+
if (target !== void 0 && target !== pane.activeFile) {
|
|
2109
2813
|
handSwitch(target);
|
|
2110
2814
|
dirty = true;
|
|
2111
2815
|
}
|
|
@@ -2115,24 +2819,30 @@ function main() {
|
|
|
2115
2819
|
if (climbBack()) dirty = true;
|
|
2116
2820
|
break;
|
|
2117
2821
|
case "follow-toggle":
|
|
2118
|
-
|
|
2119
|
-
flash = { text: follow ? "auto-follow on" : "auto-follow off", until: Date.now() +
|
|
2822
|
+
pane = toggleFollow(pane);
|
|
2823
|
+
flash = { text: pane.follow ? "auto-follow on" : "auto-follow off", until: Date.now() + FLASH_ACK_MS };
|
|
2824
|
+
dirty = true;
|
|
2825
|
+
break;
|
|
2826
|
+
case "delete-page":
|
|
2827
|
+
askDelete();
|
|
2120
2828
|
dirty = true;
|
|
2121
2829
|
break;
|
|
2122
2830
|
}
|
|
2123
2831
|
}
|
|
2124
|
-
if (dirty)
|
|
2832
|
+
if (dirty) paint2();
|
|
2125
2833
|
});
|
|
2126
2834
|
process.stdout.on("resize", handleResize);
|
|
2127
2835
|
}
|
|
2128
2836
|
tick();
|
|
2129
2837
|
setInterval(tick, cfg.intervalMs);
|
|
2838
|
+
publishPresence();
|
|
2839
|
+
setInterval(publishPresence, VIEWER_HEARTBEAT_MS);
|
|
2130
2840
|
if (interactive) {
|
|
2131
2841
|
setInterval(() => {
|
|
2132
2842
|
if (map !== void 0) return;
|
|
2133
2843
|
splashTick++;
|
|
2134
|
-
|
|
2135
|
-
},
|
|
2844
|
+
paint2();
|
|
2845
|
+
}, SPLASH_FRAME_MS);
|
|
2136
2846
|
}
|
|
2137
2847
|
}
|
|
2138
2848
|
function launchedAsEntry(argv1, moduleUrl) {
|
|
@@ -2148,30 +2858,30 @@ if (launchedAsEntry(process.argv[1], import.meta.url)) {
|
|
|
2148
2858
|
}
|
|
2149
2859
|
export {
|
|
2150
2860
|
PANEL_ROWS_MIN,
|
|
2151
|
-
|
|
2152
|
-
WAVE_NUMBER,
|
|
2861
|
+
USAGE,
|
|
2153
2862
|
anchorOffsets,
|
|
2154
2863
|
clampPanelRows,
|
|
2864
|
+
describeArgsError,
|
|
2865
|
+
describePageFault,
|
|
2155
2866
|
diveOrigin,
|
|
2156
2867
|
dividerRow,
|
|
2157
2868
|
elapsedLabel,
|
|
2158
2869
|
fitWidth,
|
|
2159
2870
|
launchedAsEntry,
|
|
2160
|
-
liveRipples,
|
|
2161
2871
|
mapPanel,
|
|
2162
|
-
mostRecentPageFile,
|
|
2163
2872
|
nearestHit,
|
|
2164
2873
|
nodePanel,
|
|
2165
2874
|
pageTabRow,
|
|
2166
2875
|
panelRowsFromDividerY,
|
|
2167
2876
|
parseArgs,
|
|
2877
|
+
readPage,
|
|
2878
|
+
renderWindow,
|
|
2168
2879
|
splashFrame,
|
|
2169
2880
|
tabScrollFor,
|
|
2881
|
+
terminalRestoreSequence,
|
|
2170
2882
|
topLevelFiles,
|
|
2171
2883
|
usableColumns,
|
|
2884
|
+
viewerReportOf,
|
|
2172
2885
|
waitingInfo,
|
|
2173
|
-
waveAt,
|
|
2174
|
-
waveHash,
|
|
2175
|
-
waveLevel,
|
|
2176
2886
|
wrapWidth
|
|
2177
2887
|
};
|