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