mellos-mapping 0.20.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 +360 -63
- package/README.zh-CN.md +314 -54
- package/dist/hook-session-start.mjs +239 -0
- package/dist/mmap.mjs +338 -0
- package/dist/server.mjs +1614 -809
- package/dist/store-paths.mjs +107 -0
- package/dist/watch.mjs +1391 -760
- package/lib/domain/ops.d.ts +71 -12
- package/lib/domain/ops.js +145 -14
- package/lib/domain/types.d.ts +47 -6
- package/lib/domain/types.js +34 -3
- 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 +32 -46
- package/lib/render/render.js +58 -789
- 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 +53 -4
- package/lib/semantics/semantics.js +130 -6
- package/lib/semantics/vocabulary.d.ts +79 -0
- package/lib/semantics/vocabulary.js +112 -0
- package/lib/store/format.d.ts +17 -0
- package/lib/store/format.js +185 -66
- package/lib/store/store.d.ts +220 -20
- package/lib/store/store.js +491 -38
- package/package.json +12 -4
- 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,25 +206,79 @@ 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
|
|
|
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
|
+
|
|
202
282
|
// src/semantics/semantics.ts
|
|
203
283
|
var ZOOM_MIN = -4;
|
|
204
284
|
var ZOOM_MAX = 2;
|
|
@@ -323,12 +403,44 @@ function focusInfo(map, focusId) {
|
|
|
323
403
|
usedBy: map.edges.filter((e) => e.to === node.id).map((e) => ref(e.from, e.label))
|
|
324
404
|
};
|
|
325
405
|
}
|
|
326
|
-
function
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
}
|
|
330
442
|
}
|
|
331
|
-
return
|
|
443
|
+
return interior;
|
|
332
444
|
}
|
|
333
445
|
function diveParent(entries, pageId) {
|
|
334
446
|
for (const [key, m] of entries) {
|
|
@@ -337,11 +449,11 @@ function diveParent(entries, pageId) {
|
|
|
337
449
|
}
|
|
338
450
|
return void 0;
|
|
339
451
|
}
|
|
340
|
-
function mostRecentKey(keys,
|
|
452
|
+
function mostRecentKey(keys, mtimeOf2) {
|
|
341
453
|
let best;
|
|
342
454
|
let bestMtime = -Infinity;
|
|
343
455
|
for (const key of keys) {
|
|
344
|
-
const mtime =
|
|
456
|
+
const mtime = mtimeOf2(key);
|
|
345
457
|
if (mtime !== void 0 && mtime > bestMtime) {
|
|
346
458
|
best = key;
|
|
347
459
|
bestMtime = mtime;
|
|
@@ -353,15 +465,60 @@ function flipForSequence(map) {
|
|
|
353
465
|
if (map.kind !== "sequence") return map;
|
|
354
466
|
return {
|
|
355
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).
|
|
356
477
|
layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
|
|
357
478
|
edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
|
|
358
479
|
};
|
|
359
480
|
}
|
|
360
481
|
|
|
361
|
-
// src/render/
|
|
482
|
+
// src/render/width.ts
|
|
362
483
|
var WIDE_RANGES = [
|
|
363
484
|
[4352, 4447],
|
|
364
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],
|
|
365
522
|
[11904, 42191],
|
|
366
523
|
// CJK radicals .. Yi (covers CJK Unified Ideographs)
|
|
367
524
|
[43360, 43391],
|
|
@@ -374,14 +531,41 @@ var WIDE_RANGES = [
|
|
|
374
531
|
[65280, 65376],
|
|
375
532
|
// fullwidth forms
|
|
376
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
|
|
377
541
|
[131072, 262141]
|
|
378
542
|
// CJK extension planes
|
|
379
543
|
];
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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;
|
|
383
563
|
}
|
|
384
|
-
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;
|
|
385
569
|
}
|
|
386
570
|
function displayWidth(text) {
|
|
387
571
|
let w = 0;
|
|
@@ -423,6 +607,19 @@ function wrapWidth(s, width) {
|
|
|
423
607
|
if (line !== "") lines.push(line);
|
|
424
608
|
return lines;
|
|
425
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";
|
|
426
623
|
var UP = 1;
|
|
427
624
|
var DOWN = 2;
|
|
428
625
|
var LEFT = 4;
|
|
@@ -457,15 +654,6 @@ function maskChar(mask, heavyHorizontal, unicode) {
|
|
|
457
654
|
}
|
|
458
655
|
return LIGHT_BY_MASK[mask] ?? "\u253C";
|
|
459
656
|
}
|
|
460
|
-
var SGR = {
|
|
461
|
-
none: "",
|
|
462
|
-
dim: "2",
|
|
463
|
-
amber: "33",
|
|
464
|
-
green: "32",
|
|
465
|
-
red: "31",
|
|
466
|
-
faint: "90"
|
|
467
|
-
};
|
|
468
|
-
var ANSI_RESET = "\x1B[0m";
|
|
469
657
|
var BORDER_JUNCTION = {
|
|
470
658
|
"\u2500": { down: "\u252C", up: "\u2534" },
|
|
471
659
|
"\u254C": { down: "\u252C", up: "\u2534" },
|
|
@@ -491,11 +679,17 @@ var Canvas = class {
|
|
|
491
679
|
text(x, y, s, style, bold = false) {
|
|
492
680
|
let cx = x;
|
|
493
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
|
+
}
|
|
494
689
|
const c = this.cell(cx, y);
|
|
495
690
|
c.literal = ch;
|
|
496
691
|
c.style = style;
|
|
497
692
|
c.bold = bold;
|
|
498
|
-
const w = charWidth(ch.codePointAt(0));
|
|
499
693
|
if (w === 2) {
|
|
500
694
|
const phantom = this.cell(cx + 1, y);
|
|
501
695
|
phantom.literal = "";
|
|
@@ -574,61 +768,12 @@ function drawPath(canvas, points, bright = false) {
|
|
|
574
768
|
}
|
|
575
769
|
}
|
|
576
770
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
function skinFor(status, unicode) {
|
|
580
|
-
const style = status === "planned" ? "dim" : status === "in-progress" ? "amber" : status === "done" ? "green" : "red";
|
|
581
|
-
if (!unicode) {
|
|
582
|
-
return status === "planned" ? { h: ".", v: ":", corners: ["+", "+", "+", "+"], style } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style };
|
|
583
|
-
}
|
|
584
|
-
switch (status) {
|
|
585
|
-
case "planned":
|
|
586
|
-
return { h: "\u254C", v: "\u254E", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
|
|
587
|
-
case "in-progress":
|
|
588
|
-
return { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
|
|
589
|
-
case "done":
|
|
590
|
-
case "regressed":
|
|
591
|
-
return { h: "\u2501", v: "\u2503", corners: ["\u250F", "\u2513", "\u2517", "\u251B"], style };
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
function glyphFor(status, opts) {
|
|
595
|
-
const spinner = opts.unicode ? SPINNER_UNICODE : SPINNER_ASCII;
|
|
596
|
-
switch (status) {
|
|
597
|
-
case "planned":
|
|
598
|
-
return opts.unicode ? "\xB7" : ".";
|
|
599
|
-
case "in-progress":
|
|
600
|
-
return spinner[opts.spinnerFrame % spinner.length];
|
|
601
|
-
case "done":
|
|
602
|
-
return opts.unicode ? "\u25A0" : "#";
|
|
603
|
-
case "regressed":
|
|
604
|
-
return opts.unicode ? "\u2717" : "X";
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
var NODE_KIND_GLYPHS = {
|
|
608
|
-
selector: ["?", "?"],
|
|
609
|
-
sequence: ["\xBB", ">"],
|
|
610
|
-
parallel: ["\u2016", "="],
|
|
611
|
-
decorator: ["\u25CC", "o"],
|
|
612
|
-
condition: ["\u25C7", "c"],
|
|
613
|
-
action: ["\xB7", "."],
|
|
614
|
-
source: ["\u25CB", "o"],
|
|
615
|
-
transform: ["\u25D0", "%"],
|
|
616
|
-
sink: ["\u25CF", "*"],
|
|
617
|
-
service: ["\u25C6", "S"],
|
|
618
|
-
db: ["\u25A4", "D"],
|
|
619
|
-
queue: ["\u2263", "Q"],
|
|
620
|
-
ui: ["\u25A3", "U"]
|
|
621
|
-
};
|
|
622
|
-
function kindGlyph(kind, unicode) {
|
|
623
|
-
const pair = NODE_KIND_GLYPHS[kind];
|
|
624
|
-
return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
|
|
625
|
-
}
|
|
626
|
-
function neutralSkin(unicode) {
|
|
627
|
-
return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
|
|
628
|
-
}
|
|
771
|
+
|
|
772
|
+
// src/render/zoom-geometry.ts
|
|
629
773
|
var BOX_H = 3;
|
|
630
774
|
var BOX_GAP = 2;
|
|
631
775
|
var LEFT_MARGIN = 2;
|
|
776
|
+
var BAR_MIN_RUN = 7;
|
|
632
777
|
var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
|
|
633
778
|
var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
|
|
634
779
|
function zoomGeometry(zoom) {
|
|
@@ -651,56 +796,6 @@ function zoomGeometry(zoom) {
|
|
|
651
796
|
return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
652
797
|
}
|
|
653
798
|
}
|
|
654
|
-
var LABEL_BUDGET_MIN = 4;
|
|
655
|
-
function boxSpec(node, geo, unicode, neutral) {
|
|
656
|
-
const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
|
|
657
|
-
const badge = node.submap !== void 0 ? unicode ? " \u229E" : " +" : "";
|
|
658
|
-
const badgeW = displayWidth(badge);
|
|
659
|
-
const text = !neutral && glyph !== void 0 ? `${glyph} ${node.label}` : node.label;
|
|
660
|
-
if (geo.mode === "constellation") {
|
|
661
|
-
return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
|
|
662
|
-
}
|
|
663
|
-
if (geo.mode === "detail" && geo.detail !== void 0) {
|
|
664
|
-
const budget2 = geo.detail;
|
|
665
|
-
const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, budget2.innerMin), budget2.innerMax);
|
|
666
|
-
const extra = [];
|
|
667
|
-
if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
|
|
668
|
-
if (node.detail !== void 0) {
|
|
669
|
-
const wrapped = wrapWidth(node.detail, innerW - 2);
|
|
670
|
-
for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
|
|
671
|
-
const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
|
|
672
|
-
extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return {
|
|
676
|
-
w: innerW + 2,
|
|
677
|
-
h: BOX_H + extra.length,
|
|
678
|
-
label: fitWidth(text, innerW - 4 - badgeW) + badge,
|
|
679
|
-
pad: 1,
|
|
680
|
-
borderless: false,
|
|
681
|
-
extra
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
const budget = Math.max(LABEL_BUDGET_MIN, Math.ceil(displayWidth(text) * geo.scale));
|
|
685
|
-
const label = fitWidth(text, budget) + badge;
|
|
686
|
-
return {
|
|
687
|
-
w: displayWidth(label) + 4 + 2 * geo.pad,
|
|
688
|
-
h: BOX_H,
|
|
689
|
-
label,
|
|
690
|
-
pad: geo.pad,
|
|
691
|
-
borderless: false,
|
|
692
|
-
extra: []
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
function renderMapWindow(map, opts, viewport) {
|
|
696
|
-
const built = buildCanvas(map, opts);
|
|
697
|
-
return {
|
|
698
|
-
lines: built.canvas.emit(opts, viewport),
|
|
699
|
-
contentWidth: built.canvas.width,
|
|
700
|
-
contentHeight: built.canvas.height,
|
|
701
|
-
hits: built.hits
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
799
|
var AGGREGATE_GEO = {
|
|
705
800
|
mode: "boxes",
|
|
706
801
|
scale: 1,
|
|
@@ -711,99 +806,31 @@ var AGGREGATE_GEO = {
|
|
|
711
806
|
barGap: 1,
|
|
712
807
|
bandCounts: false
|
|
713
808
|
};
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const canvas = new Canvas();
|
|
722
|
-
const neutral = isNeutralKind(map);
|
|
723
|
-
const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
|
|
724
|
-
if (bands.length === 0) {
|
|
725
|
-
canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
|
|
726
|
-
canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
|
|
727
|
-
return { canvas, hits: [] };
|
|
728
|
-
}
|
|
729
|
-
const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
|
|
730
|
-
const boxes = /* @__PURE__ */ new Map();
|
|
731
|
-
const bandBoxes = bands.map(() => []);
|
|
732
|
-
for (const node of map.nodes) {
|
|
733
|
-
const band = bandIndexOf.get(node.layer);
|
|
734
|
-
const box = { node, ...boxSpec(node, geo, opts.unicode, neutral), x: LEFT_MARGIN, y: 0 };
|
|
735
|
-
bandBoxes[band].push(box);
|
|
736
|
-
boxes.set(node.id, box);
|
|
737
|
-
}
|
|
738
|
-
const laneCount = map.lanes.length;
|
|
739
|
-
const laneX = [];
|
|
740
|
-
const laneW = [];
|
|
741
|
-
if (laneCount === 0) {
|
|
742
|
-
for (const row of bandBoxes) {
|
|
743
|
-
let x = LEFT_MARGIN;
|
|
744
|
-
for (const box of row) {
|
|
745
|
-
box.x = x;
|
|
746
|
-
x += box.w + geo.boxGap;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
} else {
|
|
750
|
-
const laneGap = geo.boxGap + 2;
|
|
751
|
-
const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
|
|
752
|
-
const regions = laneCount + 1;
|
|
753
|
-
const grouped = bandBoxes.map((row) => {
|
|
754
|
-
const cells = Array.from({ length: regions }, () => []);
|
|
755
|
-
for (const box of row) {
|
|
756
|
-
const lane = box.node.lane;
|
|
757
|
-
cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(box);
|
|
758
|
-
}
|
|
759
|
-
return cells;
|
|
760
|
-
});
|
|
761
|
-
const regionW = Array.from({ length: regions }, () => 0);
|
|
762
|
-
for (const cells of grouped) {
|
|
763
|
-
for (let i = 0; i < regions; i++) {
|
|
764
|
-
const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
|
|
765
|
-
regionW[i] = Math.max(regionW[i], rowW);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
|
|
769
|
-
let x0 = LEFT_MARGIN;
|
|
770
|
-
for (let i = 0; i < regions; i++) {
|
|
771
|
-
laneX.push(x0);
|
|
772
|
-
laneW.push(regionW[i]);
|
|
773
|
-
x0 += regionW[i] + laneGap;
|
|
774
|
-
}
|
|
775
|
-
for (const cells of grouped) {
|
|
776
|
-
for (let i = 0; i < regions; i++) {
|
|
777
|
-
let x = laneX[i];
|
|
778
|
-
for (const box of cells[i]) {
|
|
779
|
-
box.x = x;
|
|
780
|
-
x += box.w + geo.boxGap;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
const bandLabel = bands.map((l, i) => {
|
|
786
|
-
const row = bandBoxes[i];
|
|
787
|
-
const done = row.filter((b) => b.node.status === "done").length;
|
|
788
|
-
return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
|
|
789
|
-
});
|
|
790
|
-
let contentWidth = LEFT_MARGIN;
|
|
791
|
-
for (const row of bandBoxes) {
|
|
792
|
-
const last = row[row.length - 1];
|
|
793
|
-
if (last) contentWidth = Math.max(contentWidth, last.x + last.w);
|
|
794
|
-
}
|
|
795
|
-
for (let i = 0; i < laneCount; i++) contentWidth = Math.max(contentWidth, laneX[i] + laneW[i]);
|
|
796
|
-
for (const label of bandLabel) contentWidth = Math.max(contentWidth, LEFT_MARGIN + displayWidth(label) + 7);
|
|
797
|
-
const routes = map.edges.map((e) => {
|
|
798
|
-
const fromBox = boxes.get(e.from);
|
|
799
|
-
const toBox = boxes.get(e.to);
|
|
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);
|
|
800
816
|
return {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
fromBand: bandIndexOf.get(
|
|
804
|
-
toBand: bandIndexOf.get(
|
|
817
|
+
from,
|
|
818
|
+
to,
|
|
819
|
+
fromBand: bandIndexOf.get(from.node.layer),
|
|
820
|
+
toBand: bandIndexOf.get(to.node.layer)
|
|
805
821
|
};
|
|
806
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;
|
|
830
|
+
};
|
|
831
|
+
const takeVertical = (gap, x, edge) => {
|
|
832
|
+
gapVerticals[gap]?.set(x, edge);
|
|
833
|
+
};
|
|
807
834
|
const claimedColumns = /* @__PURE__ */ new Map();
|
|
808
835
|
const isFree = (box, x) => !(claimedColumns.get(box)?.has(x) ?? false);
|
|
809
836
|
const claim = (box, x) => {
|
|
@@ -812,60 +839,65 @@ function buildCanvasWith(map, opts, geo) {
|
|
|
812
839
|
set.add(x);
|
|
813
840
|
return x;
|
|
814
841
|
};
|
|
815
|
-
const
|
|
816
|
-
for (const r of routes) {
|
|
842
|
+
for (const r of pending) {
|
|
817
843
|
if (r.toBand - r.fromBand !== 1) continue;
|
|
818
|
-
const lo = Math.max(r.
|
|
819
|
-
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);
|
|
820
846
|
if (lo > hi) continue;
|
|
821
847
|
const mid = Math.floor((lo + hi) / 2);
|
|
822
|
-
for (let d = 0; d <= hi - lo &&
|
|
848
|
+
for (let d = 0; d <= hi - lo && r.straightX === void 0; d++) {
|
|
823
849
|
for (const x of d === 0 ? [mid] : [mid - d, mid + d]) {
|
|
824
|
-
if (x >= lo && x <= hi && isFree(r.
|
|
825
|
-
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);
|
|
826
853
|
break;
|
|
827
854
|
}
|
|
828
855
|
}
|
|
829
856
|
}
|
|
830
857
|
}
|
|
831
|
-
const bent =
|
|
858
|
+
const bent = pending.filter((r) => r.straightX === void 0);
|
|
832
859
|
const outgoing = /* @__PURE__ */ new Map();
|
|
833
860
|
const incoming = /* @__PURE__ */ new Map();
|
|
834
861
|
for (const r of bent) {
|
|
835
|
-
outgoing.set(r.
|
|
836
|
-
incoming.set(r.
|
|
862
|
+
outgoing.set(r.from, [...outgoing.get(r.from) ?? [], r]);
|
|
863
|
+
incoming.set(r.to, [...incoming.get(r.to) ?? [], r]);
|
|
837
864
|
}
|
|
838
|
-
const freeSlot = (box, k, n) => {
|
|
865
|
+
const freeSlot = (box, k, n, edge, gap) => {
|
|
839
866
|
const lo = box.x + 1;
|
|
840
867
|
const hi = box.x + box.w - 2;
|
|
841
868
|
const ideal = box.x + Math.min(box.w - 2, Math.max(1, Math.round((k + 1) * (box.w - 1) / (n + 1))));
|
|
842
869
|
for (let d = 0; d <= hi - lo; d++) {
|
|
843
870
|
for (const x of d === 0 ? [ideal] : [ideal - d, ideal + d]) {
|
|
844
|
-
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
|
+
}
|
|
845
875
|
}
|
|
846
876
|
}
|
|
847
877
|
return ideal;
|
|
848
878
|
};
|
|
849
|
-
const attach = /* @__PURE__ */ new Map();
|
|
850
879
|
for (const r of bent) {
|
|
851
|
-
const outs = outgoing.get(r.
|
|
852
|
-
const ins = incoming.get(r.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
ex: freeSlot(r.toBox, ins.indexOf(r), ins.length)
|
|
856
|
-
});
|
|
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);
|
|
857
884
|
}
|
|
858
|
-
const skipRoutes = bent.filter((r) => r.toBand - r.fromBand > 1);
|
|
859
885
|
const usedDescent = /* @__PURE__ */ new Set();
|
|
860
|
-
const descentX = /* @__PURE__ */ new Map();
|
|
861
886
|
let fallbackCount = 0;
|
|
862
887
|
const blockedByBox = (band, x) => bandBoxes[band].some((b) => x >= b.x && x <= b.x + b.w - 1);
|
|
863
|
-
|
|
864
|
-
|
|
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;
|
|
865
896
|
let chosen;
|
|
866
897
|
for (let d = 0; d <= contentWidth && chosen === void 0; d++) {
|
|
867
898
|
for (const c of d === 0 ? [ex] : [ex - d, ex + d]) {
|
|
868
899
|
if (c < LEFT_MARGIN || c > contentWidth + 1 || usedDescent.has(c)) continue;
|
|
900
|
+
if (!descentGapsFree(r, c)) continue;
|
|
869
901
|
let blocked = false;
|
|
870
902
|
for (let b = r.fromBand + 1; b < r.toBand && !blocked; b++) blocked = blockedByBox(b, c);
|
|
871
903
|
if (!blocked) {
|
|
@@ -874,58 +906,368 @@ function buildCanvasWith(map, opts, geo) {
|
|
|
874
906
|
}
|
|
875
907
|
}
|
|
876
908
|
}
|
|
877
|
-
if (chosen === void 0) chosen = contentWidth + 2 + fallbackCount++ * 2;
|
|
878
|
-
usedDescent.add(chosen);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const gapCount = bands.length - 1;
|
|
883
|
-
const gapSegments = Array.from(
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
909
|
+
if (chosen === void 0) chosen = contentWidth + 2 + fallbackCount++ * 2;
|
|
910
|
+
usedDescent.add(chosen);
|
|
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
|
+
);
|
|
919
|
+
for (const r of bent) {
|
|
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
|
+
});
|
|
928
|
+
} else {
|
|
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
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const exitRow = /* @__PURE__ */ new Map();
|
|
939
|
+
const landingRow = /* @__PURE__ */ new Map();
|
|
940
|
+
const gapRowCount = gapSegments.map((entries) => {
|
|
941
|
+
const rowEnds = [];
|
|
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);
|
|
944
|
+
if (row === -1) {
|
|
945
|
+
rowEnds.push(e.segment.hi);
|
|
946
|
+
row = rowEnds.length - 1;
|
|
947
|
+
} else {
|
|
948
|
+
rowEnds[row] = Math.max(rowEnds[row], e.segment.hi);
|
|
949
|
+
}
|
|
950
|
+
(e.kind === "exit" ? exitRow : landingRow).set(e.edge, row);
|
|
951
|
+
}
|
|
952
|
+
return rowEnds.length;
|
|
953
|
+
});
|
|
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) };
|
|
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
|
+
];
|
|
982
|
+
}
|
|
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
|
+
];
|
|
991
|
+
}
|
|
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";
|
|
1016
|
+
}
|
|
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);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
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
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function drawBox(canvas, box, opts, neutral, face, focused = false) {
|
|
1087
|
+
const { node, x, y, w } = box;
|
|
1088
|
+
const skin = neutral ? neutralSkin(opts.unicode) : skinFor(face, opts.unicode);
|
|
1089
|
+
const slotGlyph = neutral ? neutralGlyph(node, opts.unicode) : glyphFor(face, opts);
|
|
1090
|
+
if (box.borderless) {
|
|
1091
|
+
canvas.text(x + 1, y, slotGlyph, skin.style, true);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const inner = w - 2;
|
|
1095
|
+
const pad = box.pad === 1 ? " " : "";
|
|
1096
|
+
canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], skin.style, focused);
|
|
1097
|
+
canvas.text(x, y + 1, skin.v, skin.style, focused);
|
|
1098
|
+
canvas.text(x + 1, y + 1, `${pad}${slotGlyph} ${box.label}${pad}`, skin.style, true);
|
|
1099
|
+
canvas.text(x + w - 1, y + 1, skin.v, skin.style, focused);
|
|
1100
|
+
for (let i = 0; i < box.extra.length; i++) {
|
|
1101
|
+
const row = box.extra[i];
|
|
1102
|
+
const yy = y + 2 + i;
|
|
1103
|
+
canvas.text(x, yy, skin.v, skin.style, focused);
|
|
1104
|
+
canvas.text(x + 1, yy, row.text, row.style);
|
|
1105
|
+
canvas.text(x + w - 1, yy, skin.v, skin.style, focused);
|
|
1106
|
+
}
|
|
1107
|
+
canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], skin.style, focused);
|
|
1108
|
+
}
|
|
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
|
+
}
|
|
1114
|
+
}
|
|
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;
|
|
1128
|
+
}
|
|
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));
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
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
|
+
};
|
|
1180
|
+
}
|
|
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;
|
|
898
1226
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
rowEnds.push(s.hi);
|
|
907
|
-
row = rowEnds.length - 1;
|
|
908
|
-
} else {
|
|
909
|
-
rowEnds[row] = Math.max(rowEnds[row], s.hi);
|
|
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
|
+
}
|
|
910
1234
|
}
|
|
911
|
-
segmentRow.set(s, row);
|
|
912
1235
|
}
|
|
913
|
-
|
|
1236
|
+
}
|
|
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}`;
|
|
914
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) {
|
|
915
1253
|
let y = 0;
|
|
916
|
-
if (
|
|
1254
|
+
if (hasTitle) y += 1 + geo.titleGap;
|
|
917
1255
|
let laneHeaderY;
|
|
918
|
-
if (
|
|
1256
|
+
if (hasLanes) {
|
|
919
1257
|
laneHeaderY = y;
|
|
920
1258
|
y += 1 + geo.barGap;
|
|
921
1259
|
}
|
|
922
1260
|
const barY = [];
|
|
923
1261
|
const gapTrackStartY = [];
|
|
924
|
-
|
|
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++) {
|
|
925
1266
|
barY.push(y);
|
|
926
1267
|
y += 1 + geo.barGap;
|
|
927
|
-
const row = bandBoxes[b];
|
|
928
|
-
for (const box of row) box
|
|
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)));
|
|
929
1271
|
y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
|
|
930
1272
|
if (b < gapCount) {
|
|
931
1273
|
y += geo.breathe;
|
|
@@ -934,97 +1276,52 @@ function buildCanvasWith(map, opts, geo) {
|
|
|
934
1276
|
y += geo.breathe;
|
|
935
1277
|
}
|
|
936
1278
|
}
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
[direct, sy],
|
|
966
|
-
[direct, ey]
|
|
967
|
-
],
|
|
968
|
-
bright
|
|
969
|
-
);
|
|
970
|
-
continue;
|
|
971
|
-
}
|
|
972
|
-
const { sx, ex } = attach.get(r);
|
|
973
|
-
const segments = segmentOf.get(r);
|
|
974
|
-
const landingY = rowYOf(r.toBand - 1, segments.landing);
|
|
975
|
-
if (r.toBand - r.fromBand === 1) {
|
|
976
|
-
drawPath(
|
|
977
|
-
canvas,
|
|
978
|
-
[
|
|
979
|
-
[sx, sy],
|
|
980
|
-
[sx, landingY],
|
|
981
|
-
[ex, landingY],
|
|
982
|
-
[ex, ey]
|
|
983
|
-
],
|
|
984
|
-
bright
|
|
985
|
-
);
|
|
986
|
-
} else {
|
|
987
|
-
const c = descentX.get(r);
|
|
988
|
-
const exitY = rowYOf(r.fromBand, segments.exit);
|
|
989
|
-
drawPath(
|
|
990
|
-
canvas,
|
|
991
|
-
[
|
|
992
|
-
[sx, sy],
|
|
993
|
-
[sx, exitY],
|
|
994
|
-
[c, exitY],
|
|
995
|
-
[c, landingY],
|
|
996
|
-
[ex, landingY],
|
|
997
|
-
[ex, ey]
|
|
998
|
-
],
|
|
999
|
-
bright
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
let lx = LEFT_MARGIN;
|
|
1004
|
-
if (neutral) {
|
|
1005
|
-
lx = canvas.text(lx, legendY, map.kind, "faint");
|
|
1006
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1007
|
-
for (const n of map.nodes) {
|
|
1008
|
-
const k = n.kind;
|
|
1009
|
-
if (k === void 0 || seen.has(k) || kindGlyph(k, opts.unicode) === void 0) continue;
|
|
1010
|
-
seen.add(k);
|
|
1011
|
-
lx = canvas.text(lx, legendY, " ", "none");
|
|
1012
|
-
lx = canvas.text(lx, legendY, `${kindGlyph(k, opts.unicode)} ${k}`, "none");
|
|
1013
|
-
}
|
|
1014
|
-
} else {
|
|
1015
|
-
const legendOpts = { ...opts, spinnerFrame: 0 };
|
|
1016
|
-
const legendEntries = [
|
|
1017
|
-
["planned", "dim"],
|
|
1018
|
-
["in-progress", "amber"],
|
|
1019
|
-
["done", "green"],
|
|
1020
|
-
["regressed", "red"]
|
|
1021
|
-
];
|
|
1022
|
-
for (const [status, style] of legendEntries) {
|
|
1023
|
-
if (lx > LEFT_MARGIN) lx = canvas.text(lx, legendY, " ", "none");
|
|
1024
|
-
lx = canvas.text(lx, legendY, `${glyphFor(status, legendOpts)} ${status}`, style);
|
|
1025
|
-
}
|
|
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: [] };
|
|
1026
1307
|
}
|
|
1027
|
-
const
|
|
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) => ({
|
|
1028
1325
|
id: b.node.id,
|
|
1029
1326
|
x: b.x,
|
|
1030
1327
|
y: b.y,
|
|
@@ -1033,32 +1330,9 @@ function buildCanvasWith(map, opts, geo) {
|
|
|
1033
1330
|
}));
|
|
1034
1331
|
return { canvas, hits };
|
|
1035
1332
|
}
|
|
1036
|
-
function drawBox(canvas, box, opts, neutral, focused = false) {
|
|
1037
|
-
const { node, x, y, w } = box;
|
|
1038
|
-
const skin = neutral ? neutralSkin(opts.unicode) : skinFor(node.status, opts.unicode);
|
|
1039
|
-
const slotGlyph = neutral ? (node.kind !== void 0 ? kindGlyph(node.kind, opts.unicode) : void 0) ?? (opts.unicode ? "\xB7" : ".") : glyphFor(node.status, opts);
|
|
1040
|
-
if (box.borderless) {
|
|
1041
|
-
canvas.text(x + 1, y, slotGlyph, skin.style, true);
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
const inner = w - 2;
|
|
1045
|
-
const pad = box.pad === 1 ? " " : "";
|
|
1046
|
-
canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], skin.style, focused);
|
|
1047
|
-
canvas.text(x, y + 1, skin.v, skin.style, focused);
|
|
1048
|
-
canvas.text(x + 1, y + 1, `${pad}${slotGlyph} ${box.label}${pad}`, skin.style, true);
|
|
1049
|
-
canvas.text(x + w - 1, y + 1, skin.v, skin.style, focused);
|
|
1050
|
-
for (let i = 0; i < box.extra.length; i++) {
|
|
1051
|
-
const row = box.extra[i];
|
|
1052
|
-
const yy = y + 2 + i;
|
|
1053
|
-
canvas.text(x, yy, skin.v, skin.style, focused);
|
|
1054
|
-
canvas.text(x + 1, yy, row.text, row.style);
|
|
1055
|
-
canvas.text(x + w - 1, yy, skin.v, skin.style, focused);
|
|
1056
|
-
}
|
|
1057
|
-
canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], skin.style, focused);
|
|
1058
|
-
}
|
|
1059
1333
|
|
|
1060
1334
|
// src/store/store.ts
|
|
1061
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1335
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1062
1336
|
import { basename, dirname, join } from "node:path";
|
|
1063
1337
|
|
|
1064
1338
|
// src/store/format.ts
|
|
@@ -1076,111 +1350,169 @@ function describeStoreError(e) {
|
|
|
1076
1350
|
return `map file ${e.path} has an unexpected shape: ${e.detail}`;
|
|
1077
1351
|
case "invariant-violation":
|
|
1078
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}`;
|
|
1079
1357
|
}
|
|
1080
1358
|
}
|
|
1081
1359
|
function isRecord(v) {
|
|
1082
1360
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1083
1361
|
}
|
|
1084
|
-
function
|
|
1085
|
-
|
|
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}`;
|
|
1367
|
+
}
|
|
1368
|
+
function badShape(path, where, expected, got) {
|
|
1369
|
+
return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
|
|
1086
1370
|
}
|
|
1087
|
-
function
|
|
1088
|
-
|
|
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);
|
|
1089
1385
|
}
|
|
1090
1386
|
function parseMap(raw, path) {
|
|
1091
1387
|
if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
|
|
1092
1388
|
if (raw["version"] !== STATE_FILE_VERSION) {
|
|
1093
1389
|
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
|
|
1094
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;
|
|
1095
1401
|
let map = EMPTY_MAP;
|
|
1096
|
-
const title = optionalString(raw
|
|
1097
|
-
if (title
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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);
|
|
1101
1409
|
if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
|
|
1102
1410
|
map = setKind(map, kind.value);
|
|
1103
1411
|
}
|
|
1104
|
-
for (const [i, rawLayer] of
|
|
1105
|
-
|
|
1106
|
-
|
|
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);
|
|
1107
1418
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1108
|
-
const name =
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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 });
|
|
1114
1426
|
if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
|
|
1115
1427
|
map = next.value;
|
|
1116
1428
|
}
|
|
1117
|
-
for (const [i, rawLane] of
|
|
1118
|
-
|
|
1119
|
-
|
|
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);
|
|
1120
1435
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1121
|
-
const label =
|
|
1122
|
-
if (label
|
|
1123
|
-
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 });
|
|
1124
1439
|
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1125
1440
|
map = declared.value;
|
|
1126
1441
|
}
|
|
1127
|
-
for (const [i, rawGroup] of
|
|
1128
|
-
|
|
1129
|
-
|
|
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);
|
|
1130
1448
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1131
|
-
const
|
|
1449
|
+
const rawLayer = requiredString(rawGroup, "layer", where, path);
|
|
1450
|
+
if (!rawLayer.ok) return rawLayer;
|
|
1451
|
+
const layer = makeLayerId(rawLayer.value);
|
|
1132
1452
|
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
1133
|
-
const label =
|
|
1134
|
-
if (label
|
|
1135
|
-
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 });
|
|
1136
1456
|
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1137
1457
|
map = declared.value;
|
|
1138
1458
|
}
|
|
1139
|
-
for (const [i, rawNode] of
|
|
1140
|
-
|
|
1141
|
-
|
|
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);
|
|
1142
1465
|
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1143
|
-
const
|
|
1466
|
+
const rawLayer = requiredString(rawNode, "layer", where, path);
|
|
1467
|
+
if (!rawLayer.ok) return rawLayer;
|
|
1468
|
+
const layer = makeLayerId(rawLayer.value);
|
|
1144
1469
|
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
1145
|
-
const
|
|
1470
|
+
const rawStatus = requiredString(rawNode, "status", where, path);
|
|
1471
|
+
if (!rawStatus.ok) return rawStatus;
|
|
1472
|
+
const status = makeNodeStatus(rawStatus.value);
|
|
1146
1473
|
if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
|
|
1147
|
-
const label =
|
|
1148
|
-
if (label
|
|
1149
|
-
const detail = optionalString(rawNode
|
|
1150
|
-
|
|
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;
|
|
1151
1480
|
let group;
|
|
1152
|
-
if (rawGroup !== void 0) {
|
|
1153
|
-
const made = makeGroupId(rawGroup);
|
|
1481
|
+
if (rawGroup.value !== void 0) {
|
|
1482
|
+
const made = makeGroupId(rawGroup.value);
|
|
1154
1483
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1155
1484
|
group = made.value;
|
|
1156
1485
|
}
|
|
1157
|
-
const rawNodeKind = optionalString(rawNode
|
|
1486
|
+
const rawNodeKind = optionalString(rawNode, "kind", where, path);
|
|
1487
|
+
if (!rawNodeKind.ok) return rawNodeKind;
|
|
1158
1488
|
let nodeKind;
|
|
1159
|
-
if (rawNodeKind !== void 0) {
|
|
1160
|
-
const made = makeNodeKind(rawNodeKind);
|
|
1489
|
+
if (rawNodeKind.value !== void 0) {
|
|
1490
|
+
const made = makeNodeKind(rawNodeKind.value);
|
|
1161
1491
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1162
1492
|
nodeKind = made.value;
|
|
1163
1493
|
}
|
|
1164
|
-
const rawLane = optionalString(rawNode
|
|
1494
|
+
const rawLane = optionalString(rawNode, "lane", where, path);
|
|
1495
|
+
if (!rawLane.ok) return rawLane;
|
|
1165
1496
|
let lane;
|
|
1166
|
-
if (rawLane !== void 0) {
|
|
1167
|
-
const made = makeLaneId(rawLane);
|
|
1497
|
+
if (rawLane.value !== void 0) {
|
|
1498
|
+
const made = makeLaneId(rawLane.value);
|
|
1168
1499
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1169
1500
|
lane = made.value;
|
|
1170
1501
|
}
|
|
1171
|
-
const rawSubmap = optionalString(rawNode
|
|
1502
|
+
const rawSubmap = optionalString(rawNode, "submap", where, path);
|
|
1503
|
+
if (!rawSubmap.ok) return rawSubmap;
|
|
1172
1504
|
let submap;
|
|
1173
|
-
if (rawSubmap !== void 0) {
|
|
1174
|
-
const made = makeSubmapRef(rawSubmap);
|
|
1505
|
+
if (rawSubmap.value !== void 0) {
|
|
1506
|
+
const made = makeSubmapRef(rawSubmap.value);
|
|
1175
1507
|
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1176
1508
|
submap = made.value;
|
|
1177
1509
|
}
|
|
1178
1510
|
const declared = declareNode(map, {
|
|
1179
1511
|
id: id.value,
|
|
1180
|
-
label,
|
|
1512
|
+
label: label.value,
|
|
1181
1513
|
layer: layer.value,
|
|
1182
1514
|
status: status.value,
|
|
1183
|
-
...detail !== void 0 ? { detail } : {},
|
|
1515
|
+
...detail.value !== void 0 ? { detail: detail.value } : {},
|
|
1184
1516
|
...group !== void 0 ? { group } : {},
|
|
1185
1517
|
...nodeKind !== void 0 ? { kind: nodeKind } : {},
|
|
1186
1518
|
...lane !== void 0 ? { lane } : {},
|
|
@@ -1188,20 +1520,28 @@ function parseMap(raw, path) {
|
|
|
1188
1520
|
});
|
|
1189
1521
|
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1190
1522
|
map = declared.value;
|
|
1191
|
-
const evidence = optionalString(rawNode
|
|
1192
|
-
if (evidence
|
|
1193
|
-
|
|
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 });
|
|
1194
1527
|
if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
|
|
1195
1528
|
map = updated.value;
|
|
1196
1529
|
}
|
|
1197
1530
|
}
|
|
1198
|
-
for (const [i, rawEdge] of
|
|
1199
|
-
|
|
1200
|
-
|
|
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);
|
|
1201
1537
|
if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
|
|
1202
|
-
const
|
|
1538
|
+
const rawTo = requiredString(rawEdge, "to", where, path);
|
|
1539
|
+
if (!rawTo.ok) return rawTo;
|
|
1540
|
+
const to = makeNodeId(rawTo.value);
|
|
1203
1541
|
if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
|
|
1204
|
-
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);
|
|
1205
1545
|
if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
|
|
1206
1546
|
map = linked.value;
|
|
1207
1547
|
}
|
|
@@ -1209,7 +1549,54 @@ function parseMap(raw, path) {
|
|
|
1209
1549
|
}
|
|
1210
1550
|
|
|
1211
1551
|
// src/store/store.ts
|
|
1212
|
-
var
|
|
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");
|
|
1213
1600
|
var PAGES_DIR_NAME = "pages";
|
|
1214
1601
|
function pageFilePath(defaultFile, page) {
|
|
1215
1602
|
return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
@@ -1232,6 +1619,14 @@ function listPageFiles(defaultFile) {
|
|
|
1232
1619
|
}
|
|
1233
1620
|
return out;
|
|
1234
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
|
+
}
|
|
1235
1630
|
var FOCUS_FILE_NAME = "focus";
|
|
1236
1631
|
function focusFilePath(defaultFile) {
|
|
1237
1632
|
return join(dirname(defaultFile), FOCUS_FILE_NAME);
|
|
@@ -1261,6 +1656,53 @@ function takeFocusRequest(defaultFile) {
|
|
|
1261
1656
|
const id = makePageId(page);
|
|
1262
1657
|
return id.ok ? { page: id.value } : void 0;
|
|
1263
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
|
+
}
|
|
1264
1706
|
var CONFIG_FILE_NAME = "config.json";
|
|
1265
1707
|
function configFilePath(defaultFile) {
|
|
1266
1708
|
return join(dirname(defaultFile), CONFIG_FILE_NAME);
|
|
@@ -1293,7 +1735,7 @@ function loadMapFile(path) {
|
|
|
1293
1735
|
}
|
|
1294
1736
|
let raw;
|
|
1295
1737
|
try {
|
|
1296
|
-
raw = JSON.parse(text);
|
|
1738
|
+
raw = JSON.parse(stripBom(text));
|
|
1297
1739
|
} catch (e) {
|
|
1298
1740
|
return err({ kind: "malformed-json", path, detail: e.message });
|
|
1299
1741
|
}
|
|
@@ -1304,14 +1746,18 @@ function loadMapFile(path) {
|
|
|
1304
1746
|
var KEY_H_STEP = 4;
|
|
1305
1747
|
var KEY_V_STEP = 2;
|
|
1306
1748
|
var WHEEL_V_STEP = 3;
|
|
1749
|
+
var WHEEL_H_STEP = 4;
|
|
1307
1750
|
var MOTION = 32;
|
|
1308
1751
|
var WHEEL = 64;
|
|
1309
1752
|
var SHIFT = 4;
|
|
1310
1753
|
var BUTTON_BITS = 3;
|
|
1754
|
+
var WHEEL_BITS = 3;
|
|
1311
1755
|
var SGR_MOUSE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
1312
1756
|
var ARROW = /^\x1b\[([ABCD])/;
|
|
1313
1757
|
var SHIFT_TAB = /^\x1b\[Z/;
|
|
1314
|
-
var
|
|
1758
|
+
var CSI_SEQUENCE = /^\x1b\[[0-9;:<=>?]*[ -/]*[@-~]/;
|
|
1759
|
+
var SS3_SEQUENCE = /^\x1bO[@-~]/;
|
|
1760
|
+
var PARTIAL_ESCAPE = /^(?:\x1b\[[0-9;:<=>?]*[ -/]*|\x1bO)$/;
|
|
1315
1761
|
var ARROW_PAN = {
|
|
1316
1762
|
A: { dx: 0, dy: -KEY_V_STEP },
|
|
1317
1763
|
B: { dx: 0, dy: KEY_V_STEP },
|
|
@@ -1326,8 +1772,17 @@ var KEY_PAN = {
|
|
|
1326
1772
|
};
|
|
1327
1773
|
function mouseEvent(code, x, y, final) {
|
|
1328
1774
|
if (code & WHEEL) {
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
+
}
|
|
1331
1786
|
}
|
|
1332
1787
|
const buttons = code & BUTTON_BITS;
|
|
1333
1788
|
if (final === "m") return buttons === 0 ? { kind: "mouse-up", x, y } : void 0;
|
|
@@ -1339,7 +1794,6 @@ function mouseEvent(code, x, y, final) {
|
|
|
1339
1794
|
return buttons === 0 ? { kind: "mouse-down", x, y } : void 0;
|
|
1340
1795
|
}
|
|
1341
1796
|
function parseInput(chunk) {
|
|
1342
|
-
if (chunk === "\x1B") return { events: [{ kind: "clear" }], rest: "" };
|
|
1343
1797
|
const events = [];
|
|
1344
1798
|
let i = 0;
|
|
1345
1799
|
while (i < chunk.length) {
|
|
@@ -1364,18 +1818,24 @@ function parseInput(chunk) {
|
|
|
1364
1818
|
i += shiftTab[0].length;
|
|
1365
1819
|
continue;
|
|
1366
1820
|
}
|
|
1367
|
-
const
|
|
1368
|
-
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)) {
|
|
1369
1827
|
return { events, rest: slice };
|
|
1370
1828
|
}
|
|
1371
1829
|
const ch = chunk[i];
|
|
1372
|
-
if (ch === "
|
|
1830
|
+
if (ch === "\x1B") events.push({ kind: "clear" });
|
|
1831
|
+
else if (ch === "q" || ch === "Q" || ch === "" || ch === "") events.push({ kind: "quit" });
|
|
1373
1832
|
else if (ch === "0") events.push({ kind: "reset" });
|
|
1374
1833
|
else if (ch === "+" || ch === "=") events.push({ kind: "zoom", delta: 1 });
|
|
1375
1834
|
else if (ch === "-") events.push({ kind: "zoom", delta: -1 });
|
|
1376
1835
|
else if (ch === " ") events.push({ kind: "next-page" });
|
|
1377
1836
|
else if (ch === "\x7F" || ch === "\b") events.push({ kind: "back" });
|
|
1378
1837
|
else if (ch === "f" || ch === "F") events.push({ kind: "follow-toggle" });
|
|
1838
|
+
else if (ch === "x" || ch === "X") events.push({ kind: "delete-page" });
|
|
1379
1839
|
else if (ch >= "1" && ch <= "9") events.push({ kind: "page", index: ch.charCodeAt(0) - "1".charCodeAt(0) });
|
|
1380
1840
|
else if (KEY_PAN[ch]) events.push({ kind: "pan", ...KEY_PAN[ch] });
|
|
1381
1841
|
i += 1;
|
|
@@ -1383,28 +1843,209 @@ function parseInput(chunk) {
|
|
|
1383
1843
|
return { events, rest: "" };
|
|
1384
1844
|
}
|
|
1385
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
|
+
|
|
1386
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]";
|
|
1387
2013
|
function parseArgs(argv, cwd) {
|
|
1388
2014
|
let file = join2(cwd, STATE_FILE_RELATIVE_PATH);
|
|
1389
|
-
let intervalMs =
|
|
2015
|
+
let intervalMs = POLL_INTERVAL_DEFAULT_MS;
|
|
1390
2016
|
let unicode = true;
|
|
1391
2017
|
let color = true;
|
|
1392
2018
|
let mouse = true;
|
|
1393
2019
|
let page;
|
|
1394
2020
|
let follow = true;
|
|
2021
|
+
const valueOf = (flag, raw) => raw === void 0 || raw.startsWith("--") ? err({ kind: "missing-value", flag }) : ok(raw);
|
|
1395
2022
|
for (let i = 0; i < argv.length; i++) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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;
|
|
1399
2029
|
break;
|
|
2030
|
+
}
|
|
1400
2031
|
case "--page": {
|
|
1401
|
-
const
|
|
1402
|
-
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;
|
|
1403
2037
|
break;
|
|
1404
2038
|
}
|
|
1405
|
-
case "--interval":
|
|
1406
|
-
|
|
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);
|
|
1407
2047
|
break;
|
|
2048
|
+
}
|
|
1408
2049
|
case "--ascii":
|
|
1409
2050
|
unicode = false;
|
|
1410
2051
|
break;
|
|
@@ -1418,10 +2059,24 @@ function parseArgs(argv, cwd) {
|
|
|
1418
2059
|
follow = false;
|
|
1419
2060
|
break;
|
|
1420
2061
|
default:
|
|
1421
|
-
|
|
2062
|
+
return err({ kind: "unknown-flag", flag });
|
|
1422
2063
|
}
|
|
1423
2064
|
}
|
|
1424
|
-
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
|
+
}
|
|
1425
2080
|
}
|
|
1426
2081
|
function dividerRow(width, unicode, follow) {
|
|
1427
2082
|
const grip = unicode ? " \u22EF " : " ~ ";
|
|
@@ -1435,9 +2090,6 @@ function dividerRow(width, unicode, follow) {
|
|
|
1435
2090
|
}
|
|
1436
2091
|
return bar;
|
|
1437
2092
|
}
|
|
1438
|
-
function mostRecentPageFile(files, mtimeOf) {
|
|
1439
|
-
return mostRecentKey(files, mtimeOf);
|
|
1440
|
-
}
|
|
1441
2093
|
var HIDE_CURSOR = "\x1B[?25l";
|
|
1442
2094
|
var SHOW_CURSOR = "\x1B[?25h";
|
|
1443
2095
|
var CLEAR_ALL = "\x1B[H\x1B[2J";
|
|
@@ -1446,9 +2098,23 @@ var ERASE_LINE_END = "\x1B[K";
|
|
|
1446
2098
|
var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
|
|
1447
2099
|
var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
|
|
1448
2100
|
var RESET = "\x1B[0m";
|
|
2101
|
+
function terminalRestoreSequence(mouseActive) {
|
|
2102
|
+
return (mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + RESET + "\n";
|
|
2103
|
+
}
|
|
1449
2104
|
var PANEL_CONTENT_ROWS = 6;
|
|
1450
2105
|
var PANEL_ROWS_MIN = 2;
|
|
1451
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";
|
|
1452
2118
|
function usableColumns(cols) {
|
|
1453
2119
|
return Math.max(1, cols - 1);
|
|
1454
2120
|
}
|
|
@@ -1459,18 +2125,6 @@ function clampPanelRows(wanted, totalRows, tabRows) {
|
|
|
1459
2125
|
function panelRowsFromDividerY(termY, totalRows, tabRows) {
|
|
1460
2126
|
return clampPanelRows(totalRows - termY - 1, totalRows, tabRows);
|
|
1461
2127
|
}
|
|
1462
|
-
var STATUS_GLYPH = {
|
|
1463
|
-
planned: ["\xB7", "."],
|
|
1464
|
-
"in-progress": ["\u283F", "*"],
|
|
1465
|
-
done: ["\u25A0", "#"],
|
|
1466
|
-
regressed: ["\u2717", "X"]
|
|
1467
|
-
};
|
|
1468
|
-
var STATUS_SGR = {
|
|
1469
|
-
planned: "2",
|
|
1470
|
-
"in-progress": "33",
|
|
1471
|
-
done: "32",
|
|
1472
|
-
regressed: "31"
|
|
1473
|
-
};
|
|
1474
2128
|
function anchorOffsets(anchor, offset, before, after) {
|
|
1475
2129
|
if (anchor) {
|
|
1476
2130
|
return {
|
|
@@ -1484,14 +2138,19 @@ function anchorOffsets(anchor, offset, before, after) {
|
|
|
1484
2138
|
};
|
|
1485
2139
|
}
|
|
1486
2140
|
var TAB_INDICATOR_W = 3;
|
|
1487
|
-
|
|
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) {
|
|
1488
2144
|
const texts = tabs.map((tab) => {
|
|
1489
2145
|
const marker = tab.active ? unicode ? "\u25CF" : "*" : unicode ? "\u25CB" : "o";
|
|
1490
|
-
const glyph =
|
|
2146
|
+
const glyph = statusGlyph(tab.status, unicode);
|
|
1491
2147
|
return tab.neutral === true ? ` ${marker} ${tab.title} ` : ` ${marker} ${glyph} ${tab.title} `;
|
|
1492
2148
|
});
|
|
1493
|
-
const sgrOf = (tab) => tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${
|
|
1494
|
-
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));
|
|
1495
2154
|
const count = tabs.length;
|
|
1496
2155
|
let lo = 0;
|
|
1497
2156
|
let hi = count - 1;
|
|
@@ -1511,29 +2170,36 @@ function pageTabRow(tabs, width, unicode, scroll = 0) {
|
|
|
1511
2170
|
if (lo > 0) push(unicode ? " \u2039 " : " < ", "90", { kind: "scroll", delta: -1 });
|
|
1512
2171
|
const tail = hi < count - 1 ? TAB_INDICATOR_W : 0;
|
|
1513
2172
|
for (let i = lo; i <= hi; i++) {
|
|
1514
|
-
|
|
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" });
|
|
1515
2179
|
}
|
|
1516
2180
|
if (hi < count - 1) push(unicode ? " \u203A " : " > ", "90", { kind: "scroll", delta: 1 });
|
|
1517
2181
|
return segments;
|
|
1518
2182
|
}
|
|
1519
|
-
function tabScrollFor(tabs, width, unicode, scroll, index) {
|
|
2183
|
+
function tabScrollFor(tabs, width, unicode, scroll, index, closable = false) {
|
|
1520
2184
|
if (index <= scroll) return Math.max(0, index);
|
|
1521
|
-
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
|
+
);
|
|
1522
2188
|
let s = Math.max(0, Math.min(scroll, tabs.length - 1));
|
|
1523
2189
|
while (s < index && !visibleAt(s)) s++;
|
|
1524
2190
|
return s;
|
|
1525
2191
|
}
|
|
1526
|
-
function topLevelFiles(defaultFile, files,
|
|
1527
|
-
const
|
|
2192
|
+
function topLevelFiles(defaultFile, files, mapOf2) {
|
|
2193
|
+
const interior = interiorPages(files.map((f) => [pageIdOfFile(defaultFile, f), mapOf2.get(f)]));
|
|
1528
2194
|
return files.filter((f) => {
|
|
1529
2195
|
const id = pageIdOfFile(defaultFile, f);
|
|
1530
|
-
return id === void 0 || !
|
|
2196
|
+
return id === void 0 || !interior.has(id);
|
|
1531
2197
|
});
|
|
1532
2198
|
}
|
|
1533
|
-
function diveOrigin(defaultFile, file, files,
|
|
2199
|
+
function diveOrigin(defaultFile, file, files, mapOf2) {
|
|
1534
2200
|
const id = pageIdOfFile(defaultFile, file);
|
|
1535
2201
|
if (id === void 0) return void 0;
|
|
1536
|
-
const entries = files.filter((f) => f !== file).map((f) => [f,
|
|
2202
|
+
const entries = files.filter((f) => f !== file).map((f) => [f, mapOf2.get(f)]);
|
|
1537
2203
|
return diveParent(entries, id);
|
|
1538
2204
|
}
|
|
1539
2205
|
function nearestHit(hits, cx, cy) {
|
|
@@ -1549,7 +2215,7 @@ function nearestHit(hits, cx, cy) {
|
|
|
1549
2215
|
return best;
|
|
1550
2216
|
}
|
|
1551
2217
|
function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_ROWS) {
|
|
1552
|
-
const g = (s) =>
|
|
2218
|
+
const g = (s) => statusGlyph(s, unicode);
|
|
1553
2219
|
const pinMark = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
|
|
1554
2220
|
const focus = focusInfo(map, focusId);
|
|
1555
2221
|
if (focus === void 0) return void 0;
|
|
@@ -1565,7 +2231,7 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1565
2231
|
`${g(status)} ${group.label} [${group.id}] \xB7 ${layerName2} \xB7 ${status} \xB7 ${members.length} member(s)${pinMark}`,
|
|
1566
2232
|
width
|
|
1567
2233
|
),
|
|
1568
|
-
sgr: `${
|
|
2234
|
+
sgr: `${statusSgr(status)};1`
|
|
1569
2235
|
},
|
|
1570
2236
|
{
|
|
1571
2237
|
text: fitWidth(`members: ${members.map((n) => `${g(n.status)} ${n.label}`).join(" ") || "\u2014"}`, width),
|
|
@@ -1596,7 +2262,7 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1596
2262
|
const lines = [
|
|
1597
2263
|
{
|
|
1598
2264
|
text: fitWidth(`${headParts.join(" \xB7 ")}${pin}`, width),
|
|
1599
|
-
sgr: neutral ? "1" : `${
|
|
2265
|
+
sgr: neutral ? "1" : `${statusSgr(node.status)};1`
|
|
1600
2266
|
},
|
|
1601
2267
|
{ text: fitWidth(`evidence: ${node.evidence ?? "\u2014"}`, width), sgr: "90" },
|
|
1602
2268
|
{ text: fitWidth(`${usesWord} ${right} ${uses.join(" ") || "\u2014"}`, width), sgr: "" },
|
|
@@ -1613,17 +2279,6 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1613
2279
|
}
|
|
1614
2280
|
return lines.slice(0, rows);
|
|
1615
2281
|
}
|
|
1616
|
-
var WATER_ROWS = 7;
|
|
1617
|
-
var WATER_COLS_MAX = 60;
|
|
1618
|
-
var SPLASH_SHADES = {
|
|
1619
|
-
unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
|
|
1620
|
-
ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
|
|
1621
|
-
};
|
|
1622
|
-
var WAVE_RAMP = [17, 18, 19, 61, 24, 25, 31, 37, 44, 45, 51, 87, 123, 159, 195];
|
|
1623
|
-
var SPINNER_FRAMES = {
|
|
1624
|
-
unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
1625
|
-
ascii: ["|", "/", "-", "\\"]
|
|
1626
|
-
};
|
|
1627
2282
|
function elapsedLabel(ms) {
|
|
1628
2283
|
const s = Math.max(0, Math.floor(ms / 1e3));
|
|
1629
2284
|
const m = Math.floor(s / 60);
|
|
@@ -1643,95 +2298,19 @@ function waitingInfo(s, width) {
|
|
|
1643
2298
|
lines.push("the map appears at the first mmap_declare");
|
|
1644
2299
|
return lines.map((l) => fitWidth(l, w));
|
|
1645
2300
|
}
|
|
1646
|
-
var WAVE_INTERVAL = 18;
|
|
1647
|
-
var WAVE_LIFETIME = 64;
|
|
1648
|
-
var WAVE_SPEED = 0.9;
|
|
1649
|
-
var WAVE_ENVELOPE = 10;
|
|
1650
|
-
var WAVE_NUMBER = 0.42;
|
|
1651
|
-
var WAVE_LEVELS = 7;
|
|
1652
|
-
var WAVE_GAIN = 4.5;
|
|
1653
|
-
function waveHash(n) {
|
|
1654
|
-
let h = Math.imul(n + 1, 2654435761) >>> 0;
|
|
1655
|
-
h ^= h >>> 15;
|
|
1656
|
-
h = Math.imul(h, 2246822519) >>> 0;
|
|
1657
|
-
h ^= h >>> 13;
|
|
1658
|
-
return h >>> 0;
|
|
1659
|
-
}
|
|
1660
|
-
var WAVE_CORNERS = [
|
|
1661
|
-
[0, 0],
|
|
1662
|
-
[1, 0],
|
|
1663
|
-
[0, 1],
|
|
1664
|
-
[1, 1]
|
|
1665
|
-
];
|
|
1666
|
-
function liveRipples(frame, width, height) {
|
|
1667
|
-
const out = [];
|
|
1668
|
-
const first = Math.floor((frame - WAVE_LIFETIME - WAVE_INTERVAL) / WAVE_INTERVAL);
|
|
1669
|
-
const last = Math.floor(frame / WAVE_INTERVAL);
|
|
1670
|
-
for (let n = Math.max(0, first); n <= last; n++) {
|
|
1671
|
-
const h = waveHash(n);
|
|
1672
|
-
const age = frame - (n * WAVE_INTERVAL + h % WAVE_INTERVAL);
|
|
1673
|
-
if (age < 0 || age > WAVE_LIFETIME) continue;
|
|
1674
|
-
const [fx, fy] = WAVE_CORNERS[h % WAVE_CORNERS.length];
|
|
1675
|
-
out.push({
|
|
1676
|
-
ox: fx * (width - 1),
|
|
1677
|
-
oy: fy * (height - 1),
|
|
1678
|
-
r: age * WAVE_SPEED,
|
|
1679
|
-
fade: 1 - age / WAVE_LIFETIME
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
return out;
|
|
1683
|
-
}
|
|
1684
|
-
function waveAt(ripples, x, y) {
|
|
1685
|
-
let value = 0;
|
|
1686
|
-
for (const w of ripples) {
|
|
1687
|
-
const front = Math.hypot(x - w.ox, (y - w.oy) * 2) - w.r;
|
|
1688
|
-
value += Math.cos(front * WAVE_NUMBER) * Math.exp(-(front * front) / (2 * WAVE_ENVELOPE ** 2)) * w.fade;
|
|
1689
|
-
}
|
|
1690
|
-
return value;
|
|
1691
|
-
}
|
|
1692
|
-
function waveLevel(value) {
|
|
1693
|
-
return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
|
|
1694
|
-
}
|
|
1695
2301
|
function splashFrame(notice, info, frame, width, height, unicode, color) {
|
|
1696
|
-
|
|
1697
|
-
if (fieldW < 24 || height < WATER_ROWS + info.length + 3) return void 0;
|
|
1698
|
-
const mode = unicode ? "unicode" : "ascii";
|
|
1699
|
-
const shades = SPLASH_SHADES[mode];
|
|
1700
|
-
const indent = " ".repeat(Math.max(0, Math.floor((width - fieldW) / 2)));
|
|
1701
|
-
const ripples = liveRipples(frame, fieldW, WATER_ROWS);
|
|
1702
|
-
const paintRow = (y) => {
|
|
1703
|
-
const levels = Array.from({ length: fieldW }, (_, x) => waveLevel(waveAt(ripples, x, y)));
|
|
1704
|
-
let out = "";
|
|
1705
|
-
for (let i = 0; i < fieldW; ) {
|
|
1706
|
-
const level = levels[i];
|
|
1707
|
-
let j = i;
|
|
1708
|
-
while (j < fieldW && levels[j] === level) j++;
|
|
1709
|
-
if (level === 0) out += " ".repeat(j - i);
|
|
1710
|
-
else {
|
|
1711
|
-
const ink = shades[Math.abs(level)].repeat(j - i);
|
|
1712
|
-
out += color ? `\x1B[38;5;${WAVE_RAMP[WAVE_LEVELS + level]}m${ink}${RESET}` : ink;
|
|
1713
|
-
}
|
|
1714
|
-
i = j;
|
|
1715
|
-
}
|
|
1716
|
-
return out;
|
|
1717
|
-
};
|
|
2302
|
+
if (width < 24 || height < info.length + 3) return void 0;
|
|
1718
2303
|
const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
|
|
1719
|
-
const spinner = SPINNER_FRAMES[
|
|
2304
|
+
const spinner = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
|
|
1720
2305
|
const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
|
|
1721
2306
|
const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
|
|
1722
2307
|
const infoWidth = Math.max(0, ...info.map((l) => displayWidth(l)));
|
|
1723
2308
|
const infoIndent = " ".repeat(Math.max(0, Math.floor((width - infoWidth) / 2)));
|
|
1724
|
-
const block = [
|
|
1725
|
-
...Array.from({ length: WATER_ROWS }, (_, y) => indent + paintRow(y)),
|
|
1726
|
-
"",
|
|
1727
|
-
statusIndent + dim(status),
|
|
1728
|
-
"",
|
|
1729
|
-
...info.map((l) => infoIndent + dim(l))
|
|
1730
|
-
];
|
|
2309
|
+
const block = [statusIndent + dim(status), "", ...info.map((l) => infoIndent + dim(l))];
|
|
1731
2310
|
return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
|
|
1732
2311
|
}
|
|
1733
2312
|
function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
1734
|
-
const g = (s) =>
|
|
2313
|
+
const g = (s) => statusGlyph(s, unicode);
|
|
1735
2314
|
const count = (s) => map.nodes.filter((n) => n.status === s).length;
|
|
1736
2315
|
const statuses = ["done", "in-progress", "planned", "regressed"];
|
|
1737
2316
|
const counts = statuses.filter((s) => count(s) > 0).map((s) => `${g(s)} ${count(s)} ${s}`).join(" ");
|
|
@@ -1748,9 +2327,20 @@ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
|
1748
2327
|
while (lines.length < rows) lines.push({ text: "", sgr: "" });
|
|
1749
2328
|
return lines.slice(0, rows);
|
|
1750
2329
|
}
|
|
2330
|
+
function viewerReportOf(pane, defaultFile) {
|
|
2331
|
+
const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
|
|
2332
|
+
return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
|
|
2333
|
+
}
|
|
1751
2334
|
function main() {
|
|
1752
|
-
const
|
|
1753
|
-
|
|
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);
|
|
1754
2344
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1755
2345
|
const mouseActive = interactive && cfg.mouse;
|
|
1756
2346
|
let lastFrame = "";
|
|
@@ -1762,13 +2352,11 @@ function main() {
|
|
|
1762
2352
|
let notice = standbyNotice;
|
|
1763
2353
|
let lastCols = process.stdout.columns ?? 0;
|
|
1764
2354
|
let lastRows = process.stdout.rows ?? 0;
|
|
1765
|
-
let
|
|
1766
|
-
|
|
2355
|
+
let pane = initialPaneState(
|
|
2356
|
+
cfg.follow,
|
|
2357
|
+
cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
|
|
2358
|
+
);
|
|
1767
2359
|
const pageViews = /* @__PURE__ */ new Map();
|
|
1768
|
-
let activeFile;
|
|
1769
|
-
let firstScan = true;
|
|
1770
|
-
let pendingFocusFile = cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page);
|
|
1771
|
-
let follow = cfg.follow;
|
|
1772
2360
|
let lastTabSegments = [];
|
|
1773
2361
|
let tabScroll = 0;
|
|
1774
2362
|
let offsetX = 0;
|
|
@@ -1784,62 +2372,72 @@ function main() {
|
|
|
1784
2372
|
let panelContentRows = PANEL_CONTENT_ROWS;
|
|
1785
2373
|
let dividerDrag = false;
|
|
1786
2374
|
let lastClick;
|
|
1787
|
-
const diveStack = [];
|
|
1788
2375
|
let flash;
|
|
1789
2376
|
let lastTabFiles = [];
|
|
1790
|
-
const
|
|
1791
|
-
const
|
|
1792
|
-
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);
|
|
1793
2379
|
const tabRows = () => topFiles().length > 1 || inSubmap() ? 1 : 0;
|
|
1794
2380
|
const climbBack = () => {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
}
|
|
1800
|
-
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) {
|
|
1801
2385
|
handSwitch(parent);
|
|
1802
2386
|
return true;
|
|
1803
2387
|
}
|
|
1804
2388
|
return false;
|
|
1805
2389
|
};
|
|
1806
|
-
const viewWidth = () => usableColumns(process.stdout.columns ??
|
|
1807
|
-
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());
|
|
1808
2392
|
const dividerY = () => tabRows() + viewHeight() + 1;
|
|
1809
2393
|
const pageTabsOf = (files) => files.map((f) => {
|
|
1810
|
-
const
|
|
2394
|
+
const entry = entryOf(pane, f);
|
|
2395
|
+
const m = mapOf(entry);
|
|
1811
2396
|
return {
|
|
1812
|
-
title: m?.title ?? (pageIdOfFile(cfg.file, f) ??
|
|
2397
|
+
title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? DEFAULT_PAGE_TAB_LABEL),
|
|
1813
2398
|
status: m !== void 0 ? mapStatus(m) : "planned",
|
|
1814
|
-
active: f === activeFile,
|
|
1815
|
-
fresh:
|
|
2399
|
+
active: f === pane.activeFile,
|
|
2400
|
+
fresh: entry?.fresh ?? false,
|
|
1816
2401
|
neutral: m !== void 0 && isNeutralKind(m)
|
|
1817
2402
|
};
|
|
1818
2403
|
});
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
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 });
|
|
1822
2420
|
const view = pageViews.get(file);
|
|
1823
2421
|
offsetX = view?.offsetX ?? 0;
|
|
1824
2422
|
offsetY = view?.offsetY ?? 0;
|
|
1825
2423
|
zoom = view?.zoom ?? ZOOM_DEFAULT;
|
|
1826
2424
|
selectedId = view?.selectedId;
|
|
1827
2425
|
hoverId = void 0;
|
|
1828
|
-
const entry = pageData.get(file);
|
|
1829
|
-
if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
|
|
1830
|
-
map = entry?.map;
|
|
1831
|
-
notice = map !== void 0 ? "" : entry?.error ?? (file === cfg.file ? standbyNotice : `waiting for ${file} ...`);
|
|
1832
2426
|
const top = topFiles();
|
|
1833
2427
|
const tabIndex = top.indexOf(file);
|
|
1834
|
-
if (tabIndex >= 0)
|
|
2428
|
+
if (tabIndex >= 0) {
|
|
2429
|
+
tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex, mouseActive);
|
|
2430
|
+
}
|
|
1835
2431
|
};
|
|
1836
2432
|
const handSwitch = (file) => {
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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 };
|
|
1841
2438
|
}
|
|
1842
|
-
|
|
2439
|
+
adoptView(previous);
|
|
2440
|
+
adoptPage();
|
|
1843
2441
|
};
|
|
1844
2442
|
const hitTest = (termX, termY) => {
|
|
1845
2443
|
const sx = termX - 1;
|
|
@@ -1850,41 +2448,57 @@ function main() {
|
|
|
1850
2448
|
const cy = sy + offsetY;
|
|
1851
2449
|
return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
|
|
1852
2450
|
};
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
process.stdout.write((mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + "\n");
|
|
1856
|
-
process.exit(0);
|
|
2451
|
+
const publishPresence = () => {
|
|
2452
|
+
publishViewer(cfg.file, process.pid, viewerReportOf(pane, cfg.file));
|
|
1857
2453
|
};
|
|
1858
|
-
process.
|
|
1859
|
-
process.on("
|
|
1860
|
-
|
|
1861
|
-
|
|
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;
|
|
1862
2470
|
const viewW = viewWidth();
|
|
1863
|
-
panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ??
|
|
2471
|
+
panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
|
|
1864
2472
|
const viewH = viewHeight();
|
|
1865
2473
|
const focus = hoverId ?? selectedId;
|
|
1866
2474
|
let body;
|
|
1867
2475
|
let panned = "";
|
|
1868
2476
|
let pannable = false;
|
|
1869
2477
|
if (map !== void 0) {
|
|
1870
|
-
const
|
|
2478
|
+
const rendered = renderWindow(
|
|
1871
2479
|
map,
|
|
1872
2480
|
{ color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
|
|
1873
2481
|
{ x: offsetX, y: offsetY, width: viewW, height: viewH }
|
|
1874
2482
|
);
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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})`;
|
|
1882
2501
|
}
|
|
1883
|
-
pannable = maxX > 0 || maxY > 0;
|
|
1884
|
-
body = windowed.lines;
|
|
1885
|
-
lastHits = windowed.hits;
|
|
1886
|
-
lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
|
|
1887
|
-
if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
|
|
1888
2502
|
} else {
|
|
1889
2503
|
const info = waitingInfo(
|
|
1890
2504
|
{
|
|
@@ -1892,7 +2506,9 @@ function main() {
|
|
|
1892
2506
|
pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
|
|
1893
2507
|
intervalMs: cfg.intervalMs,
|
|
1894
2508
|
elapsedMs: interactive ? Date.now() - startedAt : void 0,
|
|
1895
|
-
broken:
|
|
2509
|
+
broken: pane.pages.flatMap(
|
|
2510
|
+
(p) => p.state.kind === "faulted" && p.state.lastGood === void 0 ? [describePageFault(p.state.fault)] : []
|
|
2511
|
+
)
|
|
1896
2512
|
},
|
|
1897
2513
|
Math.max(1, viewW - 2)
|
|
1898
2514
|
);
|
|
@@ -1910,7 +2526,7 @@ function main() {
|
|
|
1910
2526
|
} else {
|
|
1911
2527
|
panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
|
|
1912
2528
|
}
|
|
1913
|
-
const separator = dividerRow(viewW, cfg.unicode, follow);
|
|
2529
|
+
const separator = dividerRow(viewW, cfg.unicode, pane.follow);
|
|
1914
2530
|
const panelRows = [
|
|
1915
2531
|
cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
|
|
1916
2532
|
...panel.map(
|
|
@@ -1919,12 +2535,12 @@ function main() {
|
|
|
1919
2535
|
];
|
|
1920
2536
|
let tabLine;
|
|
1921
2537
|
lastTabFiles = topFiles();
|
|
1922
|
-
if (inSubmap() && activeFile !== void 0) {
|
|
1923
|
-
const stackParent = [
|
|
1924
|
-
const
|
|
1925
|
-
const parentFile = stackParent ??
|
|
1926
|
-
const parentTitle = parentFile !== void 0 ?
|
|
1927
|
-
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 ?? "";
|
|
1928
2544
|
const crumbHead = ` ${cfg.unicode ? "\u232B" : "<"} ${parentTitle} ${cfg.unicode ? "\u25B8" : ">"} `;
|
|
1929
2545
|
const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead), action: { kind: "back" } };
|
|
1930
2546
|
const tailText = fitWidth(`${nodeLabel} `, Math.max(1, viewW - displayWidth(crumbHead)));
|
|
@@ -1938,14 +2554,14 @@ function main() {
|
|
|
1938
2554
|
lastTabSegments = [head, tail];
|
|
1939
2555
|
tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
1940
2556
|
} else if (tabRows() > 0) {
|
|
1941
|
-
const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll);
|
|
2557
|
+
const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll, mouseActive);
|
|
1942
2558
|
lastTabSegments = segments;
|
|
1943
2559
|
tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
1944
2560
|
} else {
|
|
1945
2561
|
lastTabSegments = [];
|
|
1946
2562
|
}
|
|
1947
2563
|
const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
|
|
1948
|
-
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";
|
|
1949
2565
|
const footerText = fitWidth(` ${hint}${panned}`, viewW);
|
|
1950
2566
|
const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
|
|
1951
2567
|
let frame = HOME;
|
|
@@ -1963,89 +2579,87 @@ function main() {
|
|
|
1963
2579
|
lastRows = process.stdout.rows ?? lastRows;
|
|
1964
2580
|
lastFrame = "";
|
|
1965
2581
|
process.stdout.write(CLEAR_ALL);
|
|
1966
|
-
|
|
2582
|
+
paint2();
|
|
1967
2583
|
};
|
|
1968
2584
|
const tick = () => {
|
|
2585
|
+
if (takeQuitRequest(cfg.file)) quit();
|
|
1969
2586
|
if ((process.stdout.columns ?? lastCols) !== lastCols || (process.stdout.rows ?? lastRows) !== lastRows) {
|
|
1970
2587
|
handleResize();
|
|
1971
2588
|
}
|
|
1972
2589
|
const discovered = listPageFiles(cfg.file);
|
|
1973
|
-
|
|
1974
|
-
for (const known of [...pageData.keys()]) {
|
|
1975
|
-
if (!pageFiles.includes(known)) {
|
|
1976
|
-
pageData.delete(known);
|
|
1977
|
-
pageViews.delete(known);
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
const changedFiles = [];
|
|
1981
|
-
for (const file of pageFiles) {
|
|
1982
|
-
let mtimeMs;
|
|
1983
|
-
try {
|
|
1984
|
-
mtimeMs = statSync(file).mtimeMs;
|
|
1985
|
-
} catch {
|
|
1986
|
-
continue;
|
|
1987
|
-
}
|
|
1988
|
-
const entry = pageData.get(file);
|
|
1989
|
-
if (mtimeMs === entry?.mtimeMs) continue;
|
|
1990
|
-
const loaded = loadMapFile(file);
|
|
1991
|
-
if (loaded.ok) {
|
|
1992
|
-
if (!firstScan) changedFiles.push(file);
|
|
1993
|
-
const becameFresh = !firstScan && file !== activeFile;
|
|
1994
|
-
pageData.set(file, { map: loaded.value, mtimeMs, fresh: becameFresh });
|
|
1995
|
-
if (file === activeFile) {
|
|
1996
|
-
map = loaded.value;
|
|
1997
|
-
notice = "";
|
|
1998
|
-
} else if (becameFresh && !topFiles().includes(file)) {
|
|
1999
|
-
const title = loaded.value.title ?? (pageIdOfFile(cfg.file, file) ?? "?");
|
|
2000
|
-
flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + 4e3 };
|
|
2001
|
-
}
|
|
2002
|
-
} else if (loaded.error.kind === "malformed-json") {
|
|
2003
|
-
pageData.set(file, {
|
|
2004
|
-
map: entry?.map,
|
|
2005
|
-
mtimeMs: entry?.mtimeMs ?? -1,
|
|
2006
|
-
fresh: entry?.fresh ?? false,
|
|
2007
|
-
error: describeStoreError(loaded.error)
|
|
2008
|
-
});
|
|
2009
|
-
if (file === activeFile && entry?.map === void 0) notice = describeStoreError(loaded.error);
|
|
2010
|
-
} else {
|
|
2011
|
-
pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false, error: describeStoreError(loaded.error) });
|
|
2012
|
-
if (file === activeFile) notice = describeStoreError(loaded.error);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2590
|
+
const files = discovered.length > 0 ? discovered : [cfg.file];
|
|
2015
2591
|
const request = takeFocusRequest(cfg.file);
|
|
2016
|
-
|
|
2017
|
-
|
|
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);
|
|
2018
2611
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
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 };
|
|
2024
2620
|
}
|
|
2025
|
-
if (
|
|
2026
|
-
|
|
2027
|
-
|
|
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;
|
|
2028
2637
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
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;
|
|
2032
2645
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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();
|
|
2036
2650
|
};
|
|
2037
2651
|
if (interactive) {
|
|
2038
2652
|
process.stdin.setRawMode(true);
|
|
2039
2653
|
process.stdin.resume();
|
|
2040
2654
|
process.stdin.setEncoding("utf8");
|
|
2041
2655
|
process.stdin.on("data", (chunk) => {
|
|
2042
|
-
const
|
|
2043
|
-
pendingInput =
|
|
2656
|
+
const parsed2 = parseInput(pendingInput + chunk);
|
|
2657
|
+
pendingInput = parsed2.rest;
|
|
2044
2658
|
let dirty = false;
|
|
2045
|
-
for (const event of
|
|
2659
|
+
for (const event of parsed2.events) {
|
|
2046
2660
|
switch (event.kind) {
|
|
2047
2661
|
case "quit":
|
|
2048
|
-
|
|
2662
|
+
quit();
|
|
2049
2663
|
return;
|
|
2050
2664
|
case "reset":
|
|
2051
2665
|
offsetX = 0;
|
|
@@ -2054,7 +2668,10 @@ function main() {
|
|
|
2054
2668
|
dirty = true;
|
|
2055
2669
|
break;
|
|
2056
2670
|
case "clear":
|
|
2057
|
-
if (
|
|
2671
|
+
if (pane.pendingDelete !== void 0) {
|
|
2672
|
+
pane = disarmDelete(pane);
|
|
2673
|
+
flash = void 0;
|
|
2674
|
+
} else if (selectedId !== void 0) selectedId = void 0;
|
|
2058
2675
|
else climbBack();
|
|
2059
2676
|
dirty = true;
|
|
2060
2677
|
break;
|
|
@@ -2074,11 +2691,16 @@ function main() {
|
|
|
2074
2691
|
const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
|
|
2075
2692
|
const before = lastHits.find((h) => h.id === anchorId);
|
|
2076
2693
|
zoom = next;
|
|
2077
|
-
const
|
|
2694
|
+
const measured = renderWindow(
|
|
2078
2695
|
map,
|
|
2079
2696
|
{ color: false, unicode: cfg.unicode, spinnerFrame: 0, zoom },
|
|
2080
2697
|
{ x: 0, y: 0, width: 0, height: 0 }
|
|
2081
2698
|
);
|
|
2699
|
+
if (!measured.ok) {
|
|
2700
|
+
dirty = true;
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2703
|
+
const sized = measured.value;
|
|
2082
2704
|
const after = before === void 0 ? void 0 : sized.hits.find((h) => h.id === before.id);
|
|
2083
2705
|
const moved = anchorOffsets(
|
|
2084
2706
|
before !== void 0 && after !== void 0 ? { before, after } : void 0,
|
|
@@ -2109,7 +2731,7 @@ function main() {
|
|
|
2109
2731
|
break;
|
|
2110
2732
|
case "mouse-drag":
|
|
2111
2733
|
if (dividerDrag) {
|
|
2112
|
-
const next = panelRowsFromDividerY(event.y, process.stdout.rows ??
|
|
2734
|
+
const next = panelRowsFromDividerY(event.y, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
|
|
2113
2735
|
if (next !== panelContentRows) {
|
|
2114
2736
|
panelContentRows = next;
|
|
2115
2737
|
dirty = true;
|
|
@@ -2139,22 +2761,25 @@ function main() {
|
|
|
2139
2761
|
climbBack();
|
|
2140
2762
|
} else if (tabHit.action.kind === "scroll") {
|
|
2141
2763
|
tabScroll = Math.max(0, Math.min(tabScroll + tabHit.action.delta, lastTabFiles.length - 1));
|
|
2764
|
+
} else if (tabHit.action.kind === "delete") {
|
|
2765
|
+
askDelete();
|
|
2142
2766
|
} else {
|
|
2143
2767
|
const target = lastTabFiles[tabHit.action.index];
|
|
2144
|
-
if (target !== void 0 && target !== activeFile) handSwitch(target);
|
|
2768
|
+
if (target !== void 0 && target !== pane.activeFile) handSwitch(target);
|
|
2145
2769
|
}
|
|
2146
2770
|
} else {
|
|
2147
2771
|
const id = hitTest(event.x, event.y);
|
|
2148
2772
|
const now = Date.now();
|
|
2149
|
-
if (id !== void 0 && lastClick?.id === id && now - lastClick.at <=
|
|
2773
|
+
if (id !== void 0 && lastClick?.id === id && now - lastClick.at <= DOUBLE_CLICK_MS) {
|
|
2150
2774
|
const submap = map?.nodes.find((n) => n.id === id)?.submap;
|
|
2151
|
-
if (submap !== void 0 && activeFile !== void 0) {
|
|
2775
|
+
if (submap !== void 0 && pane.activeFile !== void 0) {
|
|
2152
2776
|
const target = pageFilePath(cfg.file, submap);
|
|
2153
|
-
|
|
2154
|
-
|
|
2777
|
+
const files = filesOf(pane);
|
|
2778
|
+
if (files.includes(target) && target !== pane.activeFile) {
|
|
2779
|
+
pane = pushDive(pane, pane.activeFile);
|
|
2155
2780
|
handSwitch(target);
|
|
2156
|
-
} else if (!
|
|
2157
|
-
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 };
|
|
2158
2783
|
}
|
|
2159
2784
|
}
|
|
2160
2785
|
lastClick = void 0;
|
|
@@ -2171,11 +2796,11 @@ function main() {
|
|
|
2171
2796
|
case "next-page":
|
|
2172
2797
|
case "prev-page": {
|
|
2173
2798
|
const top = topFiles();
|
|
2174
|
-
if (top.length > 0 && activeFile !== void 0) {
|
|
2175
|
-
const current = top.indexOf(activeFile);
|
|
2799
|
+
if (top.length > 0 && pane.activeFile !== void 0) {
|
|
2800
|
+
const current = top.indexOf(pane.activeFile);
|
|
2176
2801
|
const step = event.kind === "next-page" ? 1 : -1;
|
|
2177
2802
|
const target = top[(current + step + top.length) % top.length];
|
|
2178
|
-
if (target !== activeFile) {
|
|
2803
|
+
if (target !== pane.activeFile) {
|
|
2179
2804
|
handSwitch(target);
|
|
2180
2805
|
dirty = true;
|
|
2181
2806
|
}
|
|
@@ -2184,7 +2809,7 @@ function main() {
|
|
|
2184
2809
|
}
|
|
2185
2810
|
case "page": {
|
|
2186
2811
|
const target = topFiles()[event.index];
|
|
2187
|
-
if (target !== void 0 && target !== activeFile) {
|
|
2812
|
+
if (target !== void 0 && target !== pane.activeFile) {
|
|
2188
2813
|
handSwitch(target);
|
|
2189
2814
|
dirty = true;
|
|
2190
2815
|
}
|
|
@@ -2194,24 +2819,30 @@ function main() {
|
|
|
2194
2819
|
if (climbBack()) dirty = true;
|
|
2195
2820
|
break;
|
|
2196
2821
|
case "follow-toggle":
|
|
2197
|
-
|
|
2198
|
-
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();
|
|
2199
2828
|
dirty = true;
|
|
2200
2829
|
break;
|
|
2201
2830
|
}
|
|
2202
2831
|
}
|
|
2203
|
-
if (dirty)
|
|
2832
|
+
if (dirty) paint2();
|
|
2204
2833
|
});
|
|
2205
2834
|
process.stdout.on("resize", handleResize);
|
|
2206
2835
|
}
|
|
2207
2836
|
tick();
|
|
2208
2837
|
setInterval(tick, cfg.intervalMs);
|
|
2838
|
+
publishPresence();
|
|
2839
|
+
setInterval(publishPresence, VIEWER_HEARTBEAT_MS);
|
|
2209
2840
|
if (interactive) {
|
|
2210
2841
|
setInterval(() => {
|
|
2211
2842
|
if (map !== void 0) return;
|
|
2212
2843
|
splashTick++;
|
|
2213
|
-
|
|
2214
|
-
},
|
|
2844
|
+
paint2();
|
|
2845
|
+
}, SPLASH_FRAME_MS);
|
|
2215
2846
|
}
|
|
2216
2847
|
}
|
|
2217
2848
|
function launchedAsEntry(argv1, moduleUrl) {
|
|
@@ -2227,30 +2858,30 @@ if (launchedAsEntry(process.argv[1], import.meta.url)) {
|
|
|
2227
2858
|
}
|
|
2228
2859
|
export {
|
|
2229
2860
|
PANEL_ROWS_MIN,
|
|
2230
|
-
|
|
2231
|
-
WAVE_NUMBER,
|
|
2861
|
+
USAGE,
|
|
2232
2862
|
anchorOffsets,
|
|
2233
2863
|
clampPanelRows,
|
|
2864
|
+
describeArgsError,
|
|
2865
|
+
describePageFault,
|
|
2234
2866
|
diveOrigin,
|
|
2235
2867
|
dividerRow,
|
|
2236
2868
|
elapsedLabel,
|
|
2237
2869
|
fitWidth,
|
|
2238
2870
|
launchedAsEntry,
|
|
2239
|
-
liveRipples,
|
|
2240
2871
|
mapPanel,
|
|
2241
|
-
mostRecentPageFile,
|
|
2242
2872
|
nearestHit,
|
|
2243
2873
|
nodePanel,
|
|
2244
2874
|
pageTabRow,
|
|
2245
2875
|
panelRowsFromDividerY,
|
|
2246
2876
|
parseArgs,
|
|
2877
|
+
readPage,
|
|
2878
|
+
renderWindow,
|
|
2247
2879
|
splashFrame,
|
|
2248
2880
|
tabScrollFor,
|
|
2881
|
+
terminalRestoreSequence,
|
|
2249
2882
|
topLevelFiles,
|
|
2250
2883
|
usableColumns,
|
|
2884
|
+
viewerReportOf,
|
|
2251
2885
|
waitingInfo,
|
|
2252
|
-
waveAt,
|
|
2253
|
-
waveHash,
|
|
2254
|
-
waveLevel,
|
|
2255
2886
|
wrapWidth
|
|
2256
2887
|
};
|