mellos-mapping 0.20.3 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -69
- package/README.zh-CN.md +126 -65
- package/dist/hook-session-start.mjs +28 -20
- package/dist/mmap.mjs +303 -171
- package/dist/preview.mjs +1457 -0
- package/dist/server.mjs +1984 -741
- package/dist/store-paths.mjs +69 -22
- package/dist/terminal-worker.mjs +3331 -0
- package/dist/watch.mjs +957 -571
- 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 +5646 -0
- package/docs/codex.md +189 -0
- package/docs/map-api.md +148 -0
- package/lib/domain/context.d.ts +11 -0
- package/lib/domain/context.js +27 -0
- package/lib/domain/text.d.ts +9 -0
- package/lib/domain/text.js +54 -0
- package/lib/domain/types.d.ts +3 -0
- 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 +21 -4
- 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/project.d.ts +2 -0
- package/lib/store/project.js +29 -0
- package/lib/store/store.d.ts +15 -257
- package/lib/store/store.js +16 -695
- package/lib/store/transaction.d.ts +12 -0
- package/lib/store/transaction.js +91 -0
- package/lib/store/viewers.d.ts +81 -0
- package/lib/store/viewers.js +186 -0
- package/package.json +27 -5
- package/scripts/codex-cli.mjs +42 -0
- package/scripts/codex-register.mjs +27 -94
- package/scripts/mmap.mjs +27 -17
- 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
package/dist/watch.mjs
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/watch/watch.ts
|
|
5
|
-
import { realpathSync, statSync as statSync2 } from "node:fs";
|
|
6
|
-
import { dirname as
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { dirname as dirname9, join as join8 } from "node:path";
|
|
8
7
|
|
|
9
8
|
// src/domain/types.ts
|
|
10
9
|
var ok = (value) => ({ ok: true, value });
|
|
@@ -235,6 +234,392 @@ function updateNode(map, input) {
|
|
|
235
234
|
return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
|
|
236
235
|
}
|
|
237
236
|
|
|
237
|
+
// src/store/project.ts
|
|
238
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
239
|
+
import { dirname, join, resolve } from "node:path";
|
|
240
|
+
import { homedir, tmpdir } from "node:os";
|
|
241
|
+
function resolveProjectDirectory(cwd, stopAt = [homedir(), tmpdir()]) {
|
|
242
|
+
let start = resolve(cwd);
|
|
243
|
+
try {
|
|
244
|
+
start = realpathSync(start);
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
let dir = start;
|
|
248
|
+
const boundaries = new Set(stopAt.map((path) => {
|
|
249
|
+
try {
|
|
250
|
+
return realpathSync(path);
|
|
251
|
+
} catch {
|
|
252
|
+
return resolve(path);
|
|
253
|
+
}
|
|
254
|
+
}));
|
|
255
|
+
while (true) {
|
|
256
|
+
if (dir !== start && boundaries.has(dir)) return start;
|
|
257
|
+
if (existsSync(join(dir, ".git")) || existsSync(join(dir, ".mellos", "map.json")) || existsSync(join(dir, ".mellos", "pages"))) return dir;
|
|
258
|
+
const parent = dirname(dir);
|
|
259
|
+
if (parent === dir) return start;
|
|
260
|
+
dir = parent;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/store/transaction.ts
|
|
265
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
266
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
267
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
268
|
+
|
|
269
|
+
// src/domain/context.ts
|
|
270
|
+
function sourceError(raw) {
|
|
271
|
+
if (!Array.isArray(raw) || raw.length > 100) return "sources must be an array of at most 100 file references";
|
|
272
|
+
for (const item of raw) {
|
|
273
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return "source must be an object";
|
|
274
|
+
const s = item;
|
|
275
|
+
if (Object.keys(s).some((k) => k !== "path" && k !== "sha256")) return "unknown source field";
|
|
276
|
+
if (typeof s.path !== "string" || s.path.length > 1024 || !s.path || /[\u0000-\u001f\u007f-\u009f\\:]/.test(s.path) || s.path.startsWith("/") || s.path.split("/").some((p) => !p || p === "." || p === "..")) return "source path must be relative to the project, with forward slashes and no traversal";
|
|
277
|
+
if (s.sha256 !== void 0 && (typeof s.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(s.sha256))) return "source sha256 must be a lowercase SHA256 hash";
|
|
278
|
+
}
|
|
279
|
+
return void 0;
|
|
280
|
+
}
|
|
281
|
+
function contextError(raw) {
|
|
282
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "context must be an object";
|
|
283
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
284
|
+
if (key !== "summary" && key !== "next") return "unknown context field";
|
|
285
|
+
if (typeof value !== "string" || value.length > 2e3 || /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/.test(value)) return "context fields must be text of at most 2000 characters";
|
|
286
|
+
}
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/domain/text.ts
|
|
291
|
+
var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
|
|
292
|
+
var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
|
|
293
|
+
var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
|
|
294
|
+
var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
|
|
295
|
+
function mapTextError(map) {
|
|
296
|
+
if (map.context !== void 0) {
|
|
297
|
+
const error2 = contextError(map.context);
|
|
298
|
+
if (error2) return error2;
|
|
299
|
+
}
|
|
300
|
+
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}`;
|
|
301
|
+
let error = check("title", map.title);
|
|
302
|
+
if (error) return error;
|
|
303
|
+
for (const [i, layer] of map.layers.entries()) {
|
|
304
|
+
error = check(`layers[${i}].name`, layer.name);
|
|
305
|
+
if (error) return error;
|
|
306
|
+
}
|
|
307
|
+
for (const name of ["lanes", "groups"]) {
|
|
308
|
+
for (const [i, item] of map[name].entries()) {
|
|
309
|
+
error = check(`${name}[${i}].label`, item.label);
|
|
310
|
+
if (error) return error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const [i, node] of map.nodes.entries()) {
|
|
314
|
+
if (node.sources !== void 0) {
|
|
315
|
+
const error2 = sourceError(node.sources);
|
|
316
|
+
if (error2) return `nodes[${i}]: ${error2}`;
|
|
317
|
+
}
|
|
318
|
+
for (const name of ["label", "evidence", "detail"]) {
|
|
319
|
+
error = check(`nodes[${i}].${name}`, node[name], name !== "label");
|
|
320
|
+
if (error) return error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const [i, edge] of map.edges.entries()) {
|
|
324
|
+
error = check(`edges[${i}].label`, edge.label);
|
|
325
|
+
if (error) return error;
|
|
326
|
+
}
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
329
|
+
function terminalText(text, multiline = false) {
|
|
330
|
+
return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/store/format.ts
|
|
334
|
+
var STATE_FILE_VERSION = 1;
|
|
335
|
+
function makePageId(raw) {
|
|
336
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
337
|
+
}
|
|
338
|
+
function describeStoreError(e) {
|
|
339
|
+
switch (e.kind) {
|
|
340
|
+
case "not-found":
|
|
341
|
+
return `no map file at ${e.path}`;
|
|
342
|
+
case "malformed-json":
|
|
343
|
+
return `map file ${e.path} is not valid JSON: ${e.detail}`;
|
|
344
|
+
case "bad-shape":
|
|
345
|
+
return `map file ${e.path} has an unexpected shape: ${e.detail}`;
|
|
346
|
+
case "invariant-violation":
|
|
347
|
+
return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
|
|
348
|
+
case "save-failed":
|
|
349
|
+
return `could not write ${e.path}: ${e.detail}`;
|
|
350
|
+
case "delete-failed":
|
|
351
|
+
return `could not delete ${e.path}: ${e.detail}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function isRecord(v) {
|
|
355
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
356
|
+
}
|
|
357
|
+
function describeValue(v) {
|
|
358
|
+
if (v === void 0) return "missing";
|
|
359
|
+
if (v === null) return "null";
|
|
360
|
+
if (Array.isArray(v)) return "an array";
|
|
361
|
+
return `a ${typeof v}`;
|
|
362
|
+
}
|
|
363
|
+
function badShape(path, where, expected, got) {
|
|
364
|
+
return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
|
|
365
|
+
}
|
|
366
|
+
function arrayField(raw, key, path, presence) {
|
|
367
|
+
const v = raw[key];
|
|
368
|
+
if (Array.isArray(v)) return ok(v);
|
|
369
|
+
if (v === void 0 && presence === "optional") return ok([]);
|
|
370
|
+
return badShape(path, `"${key}"`, "an array", v);
|
|
371
|
+
}
|
|
372
|
+
function requiredString(rec, key, where, path) {
|
|
373
|
+
const v = rec[key];
|
|
374
|
+
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
375
|
+
}
|
|
376
|
+
function optionalString(rec, key, where, path) {
|
|
377
|
+
const v = rec[key];
|
|
378
|
+
if (v === void 0) return ok(void 0);
|
|
379
|
+
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
380
|
+
}
|
|
381
|
+
function parseMap(raw, path) {
|
|
382
|
+
if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
|
|
383
|
+
if (raw["version"] !== STATE_FILE_VERSION && raw["version"] !== 2) {
|
|
384
|
+
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION} or 2` });
|
|
385
|
+
}
|
|
386
|
+
const layers = arrayField(raw, "layers", path, "required");
|
|
387
|
+
if (!layers.ok) return layers;
|
|
388
|
+
const nodes = arrayField(raw, "nodes", path, "required");
|
|
389
|
+
if (!nodes.ok) return nodes;
|
|
390
|
+
const edges = arrayField(raw, "edges", path, "required");
|
|
391
|
+
if (!edges.ok) return edges;
|
|
392
|
+
const lanes = arrayField(raw, "lanes", path, "optional");
|
|
393
|
+
if (!lanes.ok) return lanes;
|
|
394
|
+
const groups = arrayField(raw, "groups", path, "optional");
|
|
395
|
+
if (!groups.ok) return groups;
|
|
396
|
+
let map = EMPTY_MAP;
|
|
397
|
+
if (raw["context"] !== void 0) {
|
|
398
|
+
const error = contextError(raw["context"]);
|
|
399
|
+
if (error) return err({ kind: "bad-shape", path, detail: error });
|
|
400
|
+
map = { ...map, context: raw["context"] };
|
|
401
|
+
}
|
|
402
|
+
const title = optionalString(raw, "title", "map", path);
|
|
403
|
+
if (!title.ok) return title;
|
|
404
|
+
if (title.value !== void 0) map = setTitle(map, title.value);
|
|
405
|
+
const rawKind = optionalString(raw, "kind", "map", path);
|
|
406
|
+
if (!rawKind.ok) return rawKind;
|
|
407
|
+
if (rawKind.value !== void 0) {
|
|
408
|
+
const kind = makeMapKind(rawKind.value);
|
|
409
|
+
if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
|
|
410
|
+
map = setKind(map, kind.value);
|
|
411
|
+
}
|
|
412
|
+
for (const [i, rawLayer] of layers.value.entries()) {
|
|
413
|
+
const where = `layers[${i}]`;
|
|
414
|
+
if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
|
|
415
|
+
const rawId = requiredString(rawLayer, "id", where, path);
|
|
416
|
+
if (!rawId.ok) return rawId;
|
|
417
|
+
const id = makeLayerId(rawId.value);
|
|
418
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
419
|
+
const name = requiredString(rawLayer, "name", where, path);
|
|
420
|
+
if (!name.ok) return name;
|
|
421
|
+
const rawRank = rawLayer["rank"];
|
|
422
|
+
if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
|
|
423
|
+
const rank = makeRank(rawRank);
|
|
424
|
+
if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
|
|
425
|
+
const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
|
|
426
|
+
if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
|
|
427
|
+
map = next.value;
|
|
428
|
+
}
|
|
429
|
+
for (const [i, rawLane] of lanes.value.entries()) {
|
|
430
|
+
const where = `lanes[${i}]`;
|
|
431
|
+
if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
|
|
432
|
+
const rawId = requiredString(rawLane, "id", where, path);
|
|
433
|
+
if (!rawId.ok) return rawId;
|
|
434
|
+
const id = makeLaneId(rawId.value);
|
|
435
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
436
|
+
const label = requiredString(rawLane, "label", where, path);
|
|
437
|
+
if (!label.ok) return label;
|
|
438
|
+
const declared = declareLane(map, { id: id.value, label: label.value });
|
|
439
|
+
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
440
|
+
map = declared.value;
|
|
441
|
+
}
|
|
442
|
+
for (const [i, rawGroup] of groups.value.entries()) {
|
|
443
|
+
const where = `groups[${i}]`;
|
|
444
|
+
if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
|
|
445
|
+
const rawId = requiredString(rawGroup, "id", where, path);
|
|
446
|
+
if (!rawId.ok) return rawId;
|
|
447
|
+
const id = makeGroupId(rawId.value);
|
|
448
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
449
|
+
const rawLayer = requiredString(rawGroup, "layer", where, path);
|
|
450
|
+
if (!rawLayer.ok) return rawLayer;
|
|
451
|
+
const layer = makeLayerId(rawLayer.value);
|
|
452
|
+
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
453
|
+
const label = requiredString(rawGroup, "label", where, path);
|
|
454
|
+
if (!label.ok) return label;
|
|
455
|
+
const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
|
|
456
|
+
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
457
|
+
map = declared.value;
|
|
458
|
+
}
|
|
459
|
+
for (const [i, rawNode] of nodes.value.entries()) {
|
|
460
|
+
const where = `nodes[${i}]`;
|
|
461
|
+
if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
|
|
462
|
+
const rawId = requiredString(rawNode, "id", where, path);
|
|
463
|
+
if (!rawId.ok) return rawId;
|
|
464
|
+
const id = makeNodeId(rawId.value);
|
|
465
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
466
|
+
const rawLayer = requiredString(rawNode, "layer", where, path);
|
|
467
|
+
if (!rawLayer.ok) return rawLayer;
|
|
468
|
+
const layer = makeLayerId(rawLayer.value);
|
|
469
|
+
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
470
|
+
const rawStatus = requiredString(rawNode, "status", where, path);
|
|
471
|
+
if (!rawStatus.ok) return rawStatus;
|
|
472
|
+
const status = makeNodeStatus(rawStatus.value);
|
|
473
|
+
if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
|
|
474
|
+
const label = requiredString(rawNode, "label", where, path);
|
|
475
|
+
if (!label.ok) return label;
|
|
476
|
+
const detail = optionalString(rawNode, "detail", where, path);
|
|
477
|
+
if (!detail.ok) return detail;
|
|
478
|
+
const rawGroup = optionalString(rawNode, "group", where, path);
|
|
479
|
+
if (!rawGroup.ok) return rawGroup;
|
|
480
|
+
let group;
|
|
481
|
+
if (rawGroup.value !== void 0) {
|
|
482
|
+
const made = makeGroupId(rawGroup.value);
|
|
483
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
484
|
+
group = made.value;
|
|
485
|
+
}
|
|
486
|
+
const rawNodeKind = optionalString(rawNode, "kind", where, path);
|
|
487
|
+
if (!rawNodeKind.ok) return rawNodeKind;
|
|
488
|
+
let nodeKind;
|
|
489
|
+
if (rawNodeKind.value !== void 0) {
|
|
490
|
+
const made = makeNodeKind(rawNodeKind.value);
|
|
491
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
492
|
+
nodeKind = made.value;
|
|
493
|
+
}
|
|
494
|
+
const rawLane = optionalString(rawNode, "lane", where, path);
|
|
495
|
+
if (!rawLane.ok) return rawLane;
|
|
496
|
+
let lane;
|
|
497
|
+
if (rawLane.value !== void 0) {
|
|
498
|
+
const made = makeLaneId(rawLane.value);
|
|
499
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
500
|
+
lane = made.value;
|
|
501
|
+
}
|
|
502
|
+
const rawSubmap = optionalString(rawNode, "submap", where, path);
|
|
503
|
+
if (!rawSubmap.ok) return rawSubmap;
|
|
504
|
+
let submap;
|
|
505
|
+
if (rawSubmap.value !== void 0) {
|
|
506
|
+
const made = makeSubmapRef(rawSubmap.value);
|
|
507
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
508
|
+
submap = made.value;
|
|
509
|
+
}
|
|
510
|
+
const declared = declareNode(map, {
|
|
511
|
+
id: id.value,
|
|
512
|
+
label: label.value,
|
|
513
|
+
layer: layer.value,
|
|
514
|
+
status: status.value,
|
|
515
|
+
...detail.value !== void 0 ? { detail: detail.value } : {},
|
|
516
|
+
...group !== void 0 ? { group } : {},
|
|
517
|
+
...nodeKind !== void 0 ? { kind: nodeKind } : {},
|
|
518
|
+
...lane !== void 0 ? { lane } : {},
|
|
519
|
+
...submap !== void 0 ? { submap } : {}
|
|
520
|
+
});
|
|
521
|
+
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
522
|
+
map = declared.value;
|
|
523
|
+
const evidence = optionalString(rawNode, "evidence", where, path);
|
|
524
|
+
if (!evidence.ok) return evidence;
|
|
525
|
+
if (evidence.value !== void 0) {
|
|
526
|
+
const updated = updateNode(map, { id: id.value, evidence: evidence.value });
|
|
527
|
+
if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
|
|
528
|
+
map = updated.value;
|
|
529
|
+
}
|
|
530
|
+
if (rawNode["sources"] !== void 0) {
|
|
531
|
+
const error = sourceError(rawNode["sources"]);
|
|
532
|
+
if (error) return err({ kind: "bad-shape", path, detail: `${where}: ${error}` });
|
|
533
|
+
map = { ...map, nodes: map.nodes.map((n) => n.id === id.value ? { ...n, sources: rawNode["sources"] } : n) };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const [i, rawEdge] of edges.value.entries()) {
|
|
537
|
+
const where = `edges[${i}]`;
|
|
538
|
+
if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
|
|
539
|
+
const rawFrom = requiredString(rawEdge, "from", where, path);
|
|
540
|
+
if (!rawFrom.ok) return rawFrom;
|
|
541
|
+
const from = makeNodeId(rawFrom.value);
|
|
542
|
+
if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
|
|
543
|
+
const rawTo = requiredString(rawEdge, "to", where, path);
|
|
544
|
+
if (!rawTo.ok) return rawTo;
|
|
545
|
+
const to = makeNodeId(rawTo.value);
|
|
546
|
+
if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
|
|
547
|
+
const label = optionalString(rawEdge, "label", where, path);
|
|
548
|
+
if (!label.ok) return label;
|
|
549
|
+
const linked = linkNodes(map, from.value, to.value, label.value);
|
|
550
|
+
if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
|
|
551
|
+
map = linked.value;
|
|
552
|
+
}
|
|
553
|
+
const textError = mapTextError(map);
|
|
554
|
+
return textError ? err({ kind: "bad-shape", path, detail: textError }) : ok(map);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/store/transaction.ts
|
|
558
|
+
var LedgerError = class extends Error {
|
|
559
|
+
constructor(code, message, details = {}) {
|
|
560
|
+
super(message);
|
|
561
|
+
this.code = code;
|
|
562
|
+
this.details = details;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
function storeDirectory(file) {
|
|
566
|
+
const dir = dirname2(file);
|
|
567
|
+
return dir.endsWith("/pages") || dir.endsWith("\\pages") ? dirname2(dir) : dir;
|
|
568
|
+
}
|
|
569
|
+
function deadOwner(lock) {
|
|
570
|
+
try {
|
|
571
|
+
const owner = JSON.parse(readFileSync(join2(lock, "owner.json"), "utf8"));
|
|
572
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
|
|
573
|
+
try {
|
|
574
|
+
process.kill(owner.pid, 0);
|
|
575
|
+
return false;
|
|
576
|
+
} catch (e) {
|
|
577
|
+
return e.code === "ESRCH";
|
|
578
|
+
}
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function withStoreLock(file, action) {
|
|
584
|
+
const dir = storeDirectory(file);
|
|
585
|
+
mkdirSync(dir, { recursive: true });
|
|
586
|
+
const lock = join2(dir, ".write-lock");
|
|
587
|
+
const owner = join2(lock, "owner.json");
|
|
588
|
+
const token = randomUUID();
|
|
589
|
+
try {
|
|
590
|
+
mkdirSync(lock);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (error.code !== "EEXIST") throw error;
|
|
593
|
+
if (!deadOwner(lock)) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes. An orphan without owner metadata needs manual inspection.`);
|
|
594
|
+
try {
|
|
595
|
+
mkdirSync(join2(lock, ".reap"));
|
|
596
|
+
} catch {
|
|
597
|
+
throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
|
|
598
|
+
}
|
|
599
|
+
if (!deadOwner(lock)) {
|
|
600
|
+
rmSync(join2(lock, ".reap"), { recursive: true, force: true });
|
|
601
|
+
throw new LedgerError("BUSY", "Writer ownership changed.");
|
|
602
|
+
}
|
|
603
|
+
rmSync(lock, { recursive: true });
|
|
604
|
+
try {
|
|
605
|
+
mkdirSync(lock);
|
|
606
|
+
} catch {
|
|
607
|
+
throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
let initialized = false;
|
|
611
|
+
try {
|
|
612
|
+
writeFileSync(owner, JSON.stringify({ pid: process.pid, token }), { flag: "wx" });
|
|
613
|
+
initialized = true;
|
|
614
|
+
return action();
|
|
615
|
+
} finally {
|
|
616
|
+
try {
|
|
617
|
+
if (!initialized || JSON.parse(readFileSync(owner, "utf8")).token === token) rmSync(lock, { recursive: true });
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
238
623
|
// src/semantics/vocabulary.ts
|
|
239
624
|
var STATUS_GLYPHS = {
|
|
240
625
|
planned: ["\xB7", "."],
|
|
@@ -573,6 +958,7 @@ function displayWidth(text) {
|
|
|
573
958
|
return w;
|
|
574
959
|
}
|
|
575
960
|
function fitWidth(s, width) {
|
|
961
|
+
s = terminalText(s);
|
|
576
962
|
if (displayWidth(s) <= width) return s;
|
|
577
963
|
let out = "";
|
|
578
964
|
let w = 0;
|
|
@@ -588,7 +974,7 @@ function wrapWidth(s, width) {
|
|
|
588
974
|
const lines = [];
|
|
589
975
|
let line = "";
|
|
590
976
|
let w = 0;
|
|
591
|
-
for (const ch of s.replace(/\r/g, "")) {
|
|
977
|
+
for (const ch of terminalText(s.replace(/\r/g, "").replace(/\t/g, " "), true)) {
|
|
592
978
|
if (ch === "\n") {
|
|
593
979
|
lines.push(line);
|
|
594
980
|
line = "";
|
|
@@ -617,7 +1003,9 @@ var SGR = {
|
|
|
617
1003
|
greenDim: "32;2",
|
|
618
1004
|
// done, but nothing behind the claim: green, not fully lit
|
|
619
1005
|
red: "31",
|
|
620
|
-
faint: "90"
|
|
1006
|
+
faint: "90",
|
|
1007
|
+
focus: "97"
|
|
1008
|
+
// bright foreground without changing font weight
|
|
621
1009
|
};
|
|
622
1010
|
var ANSI_RESET = "\x1B[0m";
|
|
623
1011
|
var UP = 1;
|
|
@@ -678,7 +1066,7 @@ var Canvas = class {
|
|
|
678
1066
|
/** Write literal text starting at (x, y). Returns the column just past it. */
|
|
679
1067
|
text(x, y, s, style, bold = false) {
|
|
680
1068
|
let cx = x;
|
|
681
|
-
for (const ch of s) {
|
|
1069
|
+
for (const ch of terminalText(s)) {
|
|
682
1070
|
const w = charWidth(ch.codePointAt(0));
|
|
683
1071
|
if (w === 0) {
|
|
684
1072
|
const base = this.cell(Math.max(0, cx - 1), y);
|
|
@@ -737,7 +1125,7 @@ var Canvas = class {
|
|
|
737
1125
|
} else if (charWidth(ch.codePointAt(0)) === 2 && x + 1 >= vp.x + vp.width) {
|
|
738
1126
|
ch = " ";
|
|
739
1127
|
}
|
|
740
|
-
const params = ch === " " ? "" : isWire ? c.bright ?
|
|
1128
|
+
const params = ch === " " ? "" : isWire ? c.bright ? SGR.focus : SGR.faint : [SGR[c.style], c.bold ? "1" : ""].filter(Boolean).join(";");
|
|
741
1129
|
if (opts.color && params !== open) {
|
|
742
1130
|
line += (open !== "" ? ANSI_RESET : "") + (params !== "" ? `\x1B[${params}m` : "");
|
|
743
1131
|
open = params;
|
|
@@ -1086,6 +1474,7 @@ function drawBands(canvas, columns, rows, wiredWidth, totalWidth) {
|
|
|
1086
1474
|
function drawBox(canvas, box, opts, neutral, face, focused = false) {
|
|
1087
1475
|
const { node, x, y, w } = box;
|
|
1088
1476
|
const skin = neutral ? neutralSkin(opts.unicode) : skinFor(face, opts.unicode);
|
|
1477
|
+
const borderStyle = focused ? "focus" : skin.style;
|
|
1089
1478
|
const slotGlyph = neutral ? neutralGlyph(node, opts.unicode) : glyphFor(face, opts);
|
|
1090
1479
|
if (box.borderless) {
|
|
1091
1480
|
canvas.text(x + 1, y, slotGlyph, skin.style, true);
|
|
@@ -1093,18 +1482,18 @@ function drawBox(canvas, box, opts, neutral, face, focused = false) {
|
|
|
1093
1482
|
}
|
|
1094
1483
|
const inner = w - 2;
|
|
1095
1484
|
const pad = box.pad === 1 ? " " : "";
|
|
1096
|
-
canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1],
|
|
1097
|
-
canvas.text(x, y + 1, skin.v,
|
|
1485
|
+
canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], borderStyle);
|
|
1486
|
+
canvas.text(x, y + 1, skin.v, borderStyle);
|
|
1098
1487
|
canvas.text(x + 1, y + 1, `${pad}${slotGlyph} ${box.label}${pad}`, skin.style, true);
|
|
1099
|
-
canvas.text(x + w - 1, y + 1, skin.v,
|
|
1488
|
+
canvas.text(x + w - 1, y + 1, skin.v, borderStyle);
|
|
1100
1489
|
for (let i = 0; i < box.extra.length; i++) {
|
|
1101
1490
|
const row = box.extra[i];
|
|
1102
1491
|
const yy = y + 2 + i;
|
|
1103
|
-
canvas.text(x, yy, skin.v,
|
|
1492
|
+
canvas.text(x, yy, skin.v, borderStyle);
|
|
1104
1493
|
canvas.text(x + 1, yy, row.text, row.style);
|
|
1105
|
-
canvas.text(x + w - 1, yy, skin.v,
|
|
1494
|
+
canvas.text(x + w - 1, yy, skin.v, borderStyle);
|
|
1106
1495
|
}
|
|
1107
|
-
canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3],
|
|
1496
|
+
canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], borderStyle);
|
|
1108
1497
|
}
|
|
1109
1498
|
function drawEdges(canvas, edges, rows, opts) {
|
|
1110
1499
|
for (const edge of edges) {
|
|
@@ -1234,322 +1623,133 @@ function layoutColumns(map, geo, unicode, neutral) {
|
|
|
1234
1623
|
}
|
|
1235
1624
|
}
|
|
1236
1625
|
}
|
|
1237
|
-
const placed = /* @__PURE__ */ new Map();
|
|
1238
|
-
for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
|
|
1239
|
-
const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
|
|
1240
|
-
const boxOf = /* @__PURE__ */ new Map();
|
|
1241
|
-
for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
|
|
1242
|
-
const bandLabel = bands.map((l, i) => {
|
|
1243
|
-
const row = bandBoxes[i];
|
|
1244
|
-
const done = row.filter((b) => b.node.status === "done").length;
|
|
1245
|
-
return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
|
|
1246
|
-
});
|
|
1247
|
-
let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
|
|
1248
|
-
for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
|
|
1249
|
-
for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
|
|
1250
|
-
return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
|
|
1251
|
-
}
|
|
1252
|
-
function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
|
|
1253
|
-
let y = 0;
|
|
1254
|
-
if (hasTitle) y += 1 + geo.titleGap;
|
|
1255
|
-
let laneHeaderY;
|
|
1256
|
-
if (hasLanes) {
|
|
1257
|
-
laneHeaderY = y;
|
|
1258
|
-
y += 1 + geo.barGap;
|
|
1259
|
-
}
|
|
1260
|
-
const barY = [];
|
|
1261
|
-
const gapTrackStartY = [];
|
|
1262
|
-
const bandBoxes = [];
|
|
1263
|
-
const placed = /* @__PURE__ */ new Map();
|
|
1264
|
-
const gapCount = columns.bands.length - 1;
|
|
1265
|
-
for (let b = 0; b < columns.bands.length; b++) {
|
|
1266
|
-
barY.push(y);
|
|
1267
|
-
y += 1 + geo.barGap;
|
|
1268
|
-
const row = columns.bandBoxes[b];
|
|
1269
|
-
for (const box of row) placed.set(box, { ...box, y });
|
|
1270
|
-
bandBoxes.push(row.map((box) => placed.get(box)));
|
|
1271
|
-
y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
|
|
1272
|
-
if (b < gapCount) {
|
|
1273
|
-
y += geo.breathe;
|
|
1274
|
-
gapTrackStartY.push(y);
|
|
1275
|
-
y += gapRowCount[b];
|
|
1276
|
-
y += geo.breathe;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
const boxOf = /* @__PURE__ */ new Map();
|
|
1280
|
-
for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
|
|
1281
|
-
return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
// src/render/render.ts
|
|
1285
|
-
function renderMapWindow(map, opts, viewport) {
|
|
1286
|
-
const built = buildCanvas(map, opts);
|
|
1287
|
-
return {
|
|
1288
|
-
lines: built.canvas.emit(opts, viewport),
|
|
1289
|
-
contentWidth: built.canvas.width,
|
|
1290
|
-
contentHeight: built.canvas.height,
|
|
1291
|
-
hits: built.hits
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
function buildCanvas(map, opts) {
|
|
1295
|
-
const oriented = flipForSequence(map);
|
|
1296
|
-
const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
|
|
1297
|
-
const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
|
|
1298
|
-
const drawn = aggregated ?? oriented;
|
|
1299
|
-
return paint(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo, unverifiedDoneIds(oriented, drawn));
|
|
1300
|
-
}
|
|
1301
|
-
function paint(map, opts, geo, unverified) {
|
|
1302
|
-
const canvas = new Canvas();
|
|
1303
|
-
if (map.layers.length === 0) {
|
|
1304
|
-
canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
|
|
1305
|
-
canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
|
|
1306
|
-
return { canvas, hits: [] };
|
|
1307
|
-
}
|
|
1308
|
-
const neutral = isNeutralKind(map);
|
|
1309
|
-
const columns = layoutColumns(map, geo, opts.unicode, neutral);
|
|
1310
|
-
const routing = routeEdges(map, columns);
|
|
1311
|
-
const rows = layoutRows(columns, geo, routing.gapRowCount, map.title !== void 0, map.lanes.length > 0);
|
|
1312
|
-
const wiredWidth = routing.fallbackCount > 0 ? columns.contentWidth + 2 + routing.fallbackCount * 2 : columns.contentWidth;
|
|
1313
|
-
const totalWidth = wiredWidth + Math.max(...columns.bandLabel.map(displayWidth));
|
|
1314
|
-
if (map.title !== void 0) drawTitle(canvas, map.title);
|
|
1315
|
-
drawLaneHeaders(canvas, map, columns, rows);
|
|
1316
|
-
drawBands(canvas, columns, rows, wiredWidth, totalWidth);
|
|
1317
|
-
const faceOf = (id, status) => unverified.has(id) ? "done-unverified" : status;
|
|
1318
|
-
for (const box of rows.boxOf.values()) {
|
|
1319
|
-
const id = box.node.id;
|
|
1320
|
-
drawBox(canvas, box, opts, neutral, faceOf(id, box.node.status), opts.focus !== void 0 && id === opts.focus);
|
|
1321
|
-
}
|
|
1322
|
-
drawEdges(canvas, routing.edges, rows, opts);
|
|
1323
|
-
drawLegend(canvas, map, opts, rows.legendY, neutral, unverified.size > 0);
|
|
1324
|
-
const hits = [...rows.boxOf.values()].map((b) => ({
|
|
1325
|
-
id: b.node.id,
|
|
1326
|
-
x: b.x,
|
|
1327
|
-
y: b.y,
|
|
1328
|
-
w: b.w,
|
|
1329
|
-
h: b.h
|
|
1330
|
-
}));
|
|
1331
|
-
return { canvas, hits };
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// src/store/store.ts
|
|
1335
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1336
|
-
import { basename, dirname, join } from "node:path";
|
|
1337
|
-
|
|
1338
|
-
// src/store/format.ts
|
|
1339
|
-
var STATE_FILE_VERSION = 1;
|
|
1340
|
-
function makePageId(raw) {
|
|
1341
|
-
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
1342
|
-
}
|
|
1343
|
-
function describeStoreError(e) {
|
|
1344
|
-
switch (e.kind) {
|
|
1345
|
-
case "not-found":
|
|
1346
|
-
return `no map file at ${e.path}`;
|
|
1347
|
-
case "malformed-json":
|
|
1348
|
-
return `map file ${e.path} is not valid JSON: ${e.detail}`;
|
|
1349
|
-
case "bad-shape":
|
|
1350
|
-
return `map file ${e.path} has an unexpected shape: ${e.detail}`;
|
|
1351
|
-
case "invariant-violation":
|
|
1352
|
-
return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
|
|
1353
|
-
case "save-failed":
|
|
1354
|
-
return `could not write ${e.path}: ${e.detail}`;
|
|
1355
|
-
case "delete-failed":
|
|
1356
|
-
return `could not delete ${e.path}: ${e.detail}`;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
function isRecord(v) {
|
|
1360
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1361
|
-
}
|
|
1362
|
-
function describeValue(v) {
|
|
1363
|
-
if (v === void 0) return "missing";
|
|
1364
|
-
if (v === null) return "null";
|
|
1365
|
-
if (Array.isArray(v)) return "an array";
|
|
1366
|
-
return `a ${typeof v}`;
|
|
1367
|
-
}
|
|
1368
|
-
function badShape(path, where, expected, got) {
|
|
1369
|
-
return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
|
|
1370
|
-
}
|
|
1371
|
-
function arrayField(raw, key, path, presence) {
|
|
1372
|
-
const v = raw[key];
|
|
1373
|
-
if (Array.isArray(v)) return ok(v);
|
|
1374
|
-
if (v === void 0 && presence === "optional") return ok([]);
|
|
1375
|
-
return badShape(path, `"${key}"`, "an array", v);
|
|
1376
|
-
}
|
|
1377
|
-
function requiredString(rec, key, where, path) {
|
|
1378
|
-
const v = rec[key];
|
|
1379
|
-
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
1380
|
-
}
|
|
1381
|
-
function optionalString(rec, key, where, path) {
|
|
1382
|
-
const v = rec[key];
|
|
1383
|
-
if (v === void 0) return ok(void 0);
|
|
1384
|
-
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
1385
|
-
}
|
|
1386
|
-
function parseMap(raw, path) {
|
|
1387
|
-
if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
|
|
1388
|
-
if (raw["version"] !== STATE_FILE_VERSION) {
|
|
1389
|
-
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
|
|
1390
|
-
}
|
|
1391
|
-
const layers = arrayField(raw, "layers", path, "required");
|
|
1392
|
-
if (!layers.ok) return layers;
|
|
1393
|
-
const nodes = arrayField(raw, "nodes", path, "required");
|
|
1394
|
-
if (!nodes.ok) return nodes;
|
|
1395
|
-
const edges = arrayField(raw, "edges", path, "required");
|
|
1396
|
-
if (!edges.ok) return edges;
|
|
1397
|
-
const lanes = arrayField(raw, "lanes", path, "optional");
|
|
1398
|
-
if (!lanes.ok) return lanes;
|
|
1399
|
-
const groups = arrayField(raw, "groups", path, "optional");
|
|
1400
|
-
if (!groups.ok) return groups;
|
|
1401
|
-
let map = EMPTY_MAP;
|
|
1402
|
-
const title = optionalString(raw, "title", "map", path);
|
|
1403
|
-
if (!title.ok) return title;
|
|
1404
|
-
if (title.value !== void 0) map = setTitle(map, title.value);
|
|
1405
|
-
const rawKind = optionalString(raw, "kind", "map", path);
|
|
1406
|
-
if (!rawKind.ok) return rawKind;
|
|
1407
|
-
if (rawKind.value !== void 0) {
|
|
1408
|
-
const kind = makeMapKind(rawKind.value);
|
|
1409
|
-
if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
|
|
1410
|
-
map = setKind(map, kind.value);
|
|
1411
|
-
}
|
|
1412
|
-
for (const [i, rawLayer] of layers.value.entries()) {
|
|
1413
|
-
const where = `layers[${i}]`;
|
|
1414
|
-
if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
|
|
1415
|
-
const rawId = requiredString(rawLayer, "id", where, path);
|
|
1416
|
-
if (!rawId.ok) return rawId;
|
|
1417
|
-
const id = makeLayerId(rawId.value);
|
|
1418
|
-
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1419
|
-
const name = requiredString(rawLayer, "name", where, path);
|
|
1420
|
-
if (!name.ok) return name;
|
|
1421
|
-
const rawRank = rawLayer["rank"];
|
|
1422
|
-
if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
|
|
1423
|
-
const rank = makeRank(rawRank);
|
|
1424
|
-
if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
|
|
1425
|
-
const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
|
|
1426
|
-
if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
|
|
1427
|
-
map = next.value;
|
|
1428
|
-
}
|
|
1429
|
-
for (const [i, rawLane] of lanes.value.entries()) {
|
|
1430
|
-
const where = `lanes[${i}]`;
|
|
1431
|
-
if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
|
|
1432
|
-
const rawId = requiredString(rawLane, "id", where, path);
|
|
1433
|
-
if (!rawId.ok) return rawId;
|
|
1434
|
-
const id = makeLaneId(rawId.value);
|
|
1435
|
-
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1436
|
-
const label = requiredString(rawLane, "label", where, path);
|
|
1437
|
-
if (!label.ok) return label;
|
|
1438
|
-
const declared = declareLane(map, { id: id.value, label: label.value });
|
|
1439
|
-
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1440
|
-
map = declared.value;
|
|
1441
|
-
}
|
|
1442
|
-
for (const [i, rawGroup] of groups.value.entries()) {
|
|
1443
|
-
const where = `groups[${i}]`;
|
|
1444
|
-
if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
|
|
1445
|
-
const rawId = requiredString(rawGroup, "id", where, path);
|
|
1446
|
-
if (!rawId.ok) return rawId;
|
|
1447
|
-
const id = makeGroupId(rawId.value);
|
|
1448
|
-
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1449
|
-
const rawLayer = requiredString(rawGroup, "layer", where, path);
|
|
1450
|
-
if (!rawLayer.ok) return rawLayer;
|
|
1451
|
-
const layer = makeLayerId(rawLayer.value);
|
|
1452
|
-
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
1453
|
-
const label = requiredString(rawGroup, "label", where, path);
|
|
1454
|
-
if (!label.ok) return label;
|
|
1455
|
-
const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
|
|
1456
|
-
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1457
|
-
map = declared.value;
|
|
1458
|
-
}
|
|
1459
|
-
for (const [i, rawNode] of nodes.value.entries()) {
|
|
1460
|
-
const where = `nodes[${i}]`;
|
|
1461
|
-
if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
|
|
1462
|
-
const rawId = requiredString(rawNode, "id", where, path);
|
|
1463
|
-
if (!rawId.ok) return rawId;
|
|
1464
|
-
const id = makeNodeId(rawId.value);
|
|
1465
|
-
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
1466
|
-
const rawLayer = requiredString(rawNode, "layer", where, path);
|
|
1467
|
-
if (!rawLayer.ok) return rawLayer;
|
|
1468
|
-
const layer = makeLayerId(rawLayer.value);
|
|
1469
|
-
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
1470
|
-
const rawStatus = requiredString(rawNode, "status", where, path);
|
|
1471
|
-
if (!rawStatus.ok) return rawStatus;
|
|
1472
|
-
const status = makeNodeStatus(rawStatus.value);
|
|
1473
|
-
if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
|
|
1474
|
-
const label = requiredString(rawNode, "label", where, path);
|
|
1475
|
-
if (!label.ok) return label;
|
|
1476
|
-
const detail = optionalString(rawNode, "detail", where, path);
|
|
1477
|
-
if (!detail.ok) return detail;
|
|
1478
|
-
const rawGroup = optionalString(rawNode, "group", where, path);
|
|
1479
|
-
if (!rawGroup.ok) return rawGroup;
|
|
1480
|
-
let group;
|
|
1481
|
-
if (rawGroup.value !== void 0) {
|
|
1482
|
-
const made = makeGroupId(rawGroup.value);
|
|
1483
|
-
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1484
|
-
group = made.value;
|
|
1485
|
-
}
|
|
1486
|
-
const rawNodeKind = optionalString(rawNode, "kind", where, path);
|
|
1487
|
-
if (!rawNodeKind.ok) return rawNodeKind;
|
|
1488
|
-
let nodeKind;
|
|
1489
|
-
if (rawNodeKind.value !== void 0) {
|
|
1490
|
-
const made = makeNodeKind(rawNodeKind.value);
|
|
1491
|
-
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1492
|
-
nodeKind = made.value;
|
|
1493
|
-
}
|
|
1494
|
-
const rawLane = optionalString(rawNode, "lane", where, path);
|
|
1495
|
-
if (!rawLane.ok) return rawLane;
|
|
1496
|
-
let lane;
|
|
1497
|
-
if (rawLane.value !== void 0) {
|
|
1498
|
-
const made = makeLaneId(rawLane.value);
|
|
1499
|
-
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
1500
|
-
lane = made.value;
|
|
1626
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1627
|
+
for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
|
|
1628
|
+
const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
|
|
1629
|
+
const boxOf = /* @__PURE__ */ new Map();
|
|
1630
|
+
for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
|
|
1631
|
+
const bandLabel = bands.map((l, i) => {
|
|
1632
|
+
const row = bandBoxes[i];
|
|
1633
|
+
const done = row.filter((b) => b.node.status === "done").length;
|
|
1634
|
+
return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
|
|
1635
|
+
});
|
|
1636
|
+
let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
|
|
1637
|
+
for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
|
|
1638
|
+
for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
|
|
1639
|
+
return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
|
|
1640
|
+
}
|
|
1641
|
+
function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
|
|
1642
|
+
let y = 0;
|
|
1643
|
+
if (hasTitle) y += 1 + geo.titleGap;
|
|
1644
|
+
let laneHeaderY;
|
|
1645
|
+
if (hasLanes) {
|
|
1646
|
+
laneHeaderY = y;
|
|
1647
|
+
y += 1 + geo.barGap;
|
|
1648
|
+
}
|
|
1649
|
+
const barY = [];
|
|
1650
|
+
const gapTrackStartY = [];
|
|
1651
|
+
const bandBoxes = [];
|
|
1652
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1653
|
+
const gapCount = columns.bands.length - 1;
|
|
1654
|
+
for (let b = 0; b < columns.bands.length; b++) {
|
|
1655
|
+
barY.push(y);
|
|
1656
|
+
y += 1 + geo.barGap;
|
|
1657
|
+
const row = columns.bandBoxes[b];
|
|
1658
|
+
for (const box of row) placed.set(box, { ...box, y });
|
|
1659
|
+
bandBoxes.push(row.map((box) => placed.get(box)));
|
|
1660
|
+
y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
|
|
1661
|
+
if (b < gapCount) {
|
|
1662
|
+
y += geo.breathe;
|
|
1663
|
+
gapTrackStartY.push(y);
|
|
1664
|
+
y += gapRowCount[b];
|
|
1665
|
+
y += geo.breathe;
|
|
1501
1666
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1667
|
+
}
|
|
1668
|
+
const boxOf = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
|
|
1670
|
+
return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/render/render.ts
|
|
1674
|
+
function renderMapWindow(map, opts, viewport) {
|
|
1675
|
+
return renderSceneWindow(prepareScene(map, opts), opts, viewport);
|
|
1676
|
+
}
|
|
1677
|
+
function createWindowRenderer() {
|
|
1678
|
+
let previous;
|
|
1679
|
+
let frame;
|
|
1680
|
+
return (map, opts, viewport) => {
|
|
1681
|
+
const zoom = opts.zoom ?? ZOOM_DEFAULT;
|
|
1682
|
+
if (previous?.map !== map || previous.unicode !== opts.unicode || previous.zoom !== zoom) {
|
|
1683
|
+
previous = { map, unicode: opts.unicode, zoom, scene: prepareScene(map, opts) };
|
|
1509
1684
|
}
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
layer: layer.value,
|
|
1514
|
-
status: status.value,
|
|
1515
|
-
...detail.value !== void 0 ? { detail: detail.value } : {},
|
|
1516
|
-
...group !== void 0 ? { group } : {},
|
|
1517
|
-
...nodeKind !== void 0 ? { kind: nodeKind } : {},
|
|
1518
|
-
...lane !== void 0 ? { lane } : {},
|
|
1519
|
-
...submap !== void 0 ? { submap } : {}
|
|
1520
|
-
});
|
|
1521
|
-
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
1522
|
-
map = declared.value;
|
|
1523
|
-
const evidence = optionalString(rawNode, "evidence", where, path);
|
|
1524
|
-
if (!evidence.ok) return evidence;
|
|
1525
|
-
if (evidence.value !== void 0) {
|
|
1526
|
-
const updated = updateNode(map, { id: id.value, evidence: evidence.value });
|
|
1527
|
-
if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
|
|
1528
|
-
map = updated.value;
|
|
1685
|
+
const scene = previous.scene;
|
|
1686
|
+
if (frame?.scene !== scene || frame.spinner !== opts.spinnerFrame || frame.focus !== opts.focus || frame.color !== opts.color) {
|
|
1687
|
+
frame = { scene, spinner: opts.spinnerFrame, focus: opts.focus, color: opts.color, built: paint(scene, opts) };
|
|
1529
1688
|
}
|
|
1689
|
+
return emitWindow(frame.built, opts, viewport);
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
function renderSceneWindow(scene, opts, viewport) {
|
|
1693
|
+
return emitWindow(paint(scene, opts), opts, viewport);
|
|
1694
|
+
}
|
|
1695
|
+
function emitWindow(built, opts, viewport) {
|
|
1696
|
+
return {
|
|
1697
|
+
lines: built.canvas.emit(opts, viewport),
|
|
1698
|
+
contentWidth: built.canvas.width,
|
|
1699
|
+
contentHeight: built.canvas.height,
|
|
1700
|
+
hits: built.hits
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function prepareScene(map, opts) {
|
|
1704
|
+
const oriented = flipForSequence(map);
|
|
1705
|
+
const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
|
|
1706
|
+
const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
|
|
1707
|
+
const drawn = aggregated ?? oriented;
|
|
1708
|
+
const unverified = unverifiedDoneIds(oriented, drawn);
|
|
1709
|
+
if (drawn.layers.length === 0) return { map: drawn, unverified };
|
|
1710
|
+
return { map: drawn, unverified, geometry: prepareGeometry(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo) };
|
|
1711
|
+
}
|
|
1712
|
+
function prepareGeometry(map, opts, geo) {
|
|
1713
|
+
const neutral = isNeutralKind(map);
|
|
1714
|
+
const columns = layoutColumns(map, geo, opts.unicode, neutral);
|
|
1715
|
+
const routing = routeEdges(map, columns);
|
|
1716
|
+
const rows = layoutRows(columns, geo, routing.gapRowCount, map.title !== void 0, map.lanes.length > 0);
|
|
1717
|
+
const wiredWidth = routing.fallbackCount > 0 ? columns.contentWidth + 2 + routing.fallbackCount * 2 : columns.contentWidth;
|
|
1718
|
+
const totalWidth = wiredWidth + Math.max(...columns.bandLabel.map(displayWidth));
|
|
1719
|
+
const hits = [...rows.boxOf.values()].map((b) => ({
|
|
1720
|
+
id: b.node.id,
|
|
1721
|
+
x: b.x,
|
|
1722
|
+
y: b.y,
|
|
1723
|
+
w: b.w,
|
|
1724
|
+
h: b.h
|
|
1725
|
+
}));
|
|
1726
|
+
return { neutral, columns, routing, rows, wiredWidth, totalWidth, hits };
|
|
1727
|
+
}
|
|
1728
|
+
function paint(scene, opts) {
|
|
1729
|
+
const { map, unverified, geometry } = scene;
|
|
1730
|
+
const canvas = new Canvas();
|
|
1731
|
+
if (geometry === void 0) {
|
|
1732
|
+
canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
|
|
1733
|
+
canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
|
|
1734
|
+
return { canvas, hits: [] };
|
|
1530
1735
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
if (!rawTo.ok) return rawTo;
|
|
1540
|
-
const to = makeNodeId(rawTo.value);
|
|
1541
|
-
if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
|
|
1542
|
-
const label = optionalString(rawEdge, "label", where, path);
|
|
1543
|
-
if (!label.ok) return label;
|
|
1544
|
-
const linked = linkNodes(map, from.value, to.value, label.value);
|
|
1545
|
-
if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
|
|
1546
|
-
map = linked.value;
|
|
1736
|
+
const { neutral, columns, routing, rows, wiredWidth, totalWidth, hits } = geometry;
|
|
1737
|
+
if (map.title !== void 0) drawTitle(canvas, map.title);
|
|
1738
|
+
drawLaneHeaders(canvas, map, columns, rows);
|
|
1739
|
+
drawBands(canvas, columns, rows, wiredWidth, totalWidth);
|
|
1740
|
+
const faceOf = (id, status) => unverified.has(id) ? "done-unverified" : status;
|
|
1741
|
+
for (const box of rows.boxOf.values()) {
|
|
1742
|
+
const id = box.node.id;
|
|
1743
|
+
drawBox(canvas, box, opts, neutral, faceOf(id, box.node.status), opts.focus !== void 0 && id === opts.focus);
|
|
1547
1744
|
}
|
|
1548
|
-
|
|
1745
|
+
drawEdges(canvas, routing.edges, rows, opts);
|
|
1746
|
+
drawLegend(canvas, map, opts, rows.legendY, neutral, unverified.size > 0);
|
|
1747
|
+
return { canvas, hits };
|
|
1549
1748
|
}
|
|
1550
1749
|
|
|
1551
|
-
// src/store/
|
|
1552
|
-
|
|
1750
|
+
// src/store/atomic.ts
|
|
1751
|
+
import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1752
|
+
import { dirname as dirname3 } from "node:path";
|
|
1553
1753
|
var RENAME_BACKOFF_STEP_MS = 10;
|
|
1554
1754
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
|
|
1555
1755
|
function sleepSync(ms) {
|
|
@@ -1557,18 +1757,18 @@ function sleepSync(ms) {
|
|
|
1557
1757
|
}
|
|
1558
1758
|
function discardTemp(tmp) {
|
|
1559
1759
|
try {
|
|
1560
|
-
|
|
1760
|
+
rmSync2(tmp, { force: true });
|
|
1561
1761
|
} catch {
|
|
1562
1762
|
}
|
|
1563
1763
|
}
|
|
1564
1764
|
function errnoOf(e) {
|
|
1565
1765
|
return e.code ?? e.message;
|
|
1566
1766
|
}
|
|
1567
|
-
function
|
|
1767
|
+
function writeAtomic(path, contents, maxAttempts) {
|
|
1568
1768
|
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
1569
1769
|
try {
|
|
1570
|
-
|
|
1571
|
-
|
|
1770
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1771
|
+
writeFileSync2(tmp, contents, "utf8");
|
|
1572
1772
|
} catch (e) {
|
|
1573
1773
|
discardTemp(tmp);
|
|
1574
1774
|
return err({ kind: "save-failed", path, detail: `writing the temp file failed: ${errnoOf(e)}` });
|
|
@@ -1580,7 +1780,7 @@ function writeFileAtomic(path, contents) {
|
|
|
1580
1780
|
return ok(void 0);
|
|
1581
1781
|
} catch (e) {
|
|
1582
1782
|
const code = errnoOf(e);
|
|
1583
|
-
if (!TRANSIENT_RENAME_CODES.has(code) || attempt >=
|
|
1783
|
+
if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= maxAttempts) {
|
|
1584
1784
|
discardTemp(tmp);
|
|
1585
1785
|
return err({ kind: "save-failed", path, detail: `${code} after ${attempt} attempt(s)` });
|
|
1586
1786
|
}
|
|
@@ -1589,17 +1789,15 @@ function writeFileAtomic(path, contents) {
|
|
|
1589
1789
|
}
|
|
1590
1790
|
}
|
|
1591
1791
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1597
|
-
}
|
|
1792
|
+
|
|
1793
|
+
// src/store/pages.ts
|
|
1794
|
+
import { existsSync as existsSync2, readdirSync, rmSync as rmSync3 } from "node:fs";
|
|
1795
|
+
import { basename, dirname as dirname4, join as join3 } from "node:path";
|
|
1598
1796
|
var STORE_DIR_NAME = ".mellos";
|
|
1599
|
-
var STATE_FILE_RELATIVE_PATH =
|
|
1797
|
+
var STATE_FILE_RELATIVE_PATH = join3(STORE_DIR_NAME, "map.json");
|
|
1600
1798
|
var PAGES_DIR_NAME = "pages";
|
|
1601
1799
|
function pageFilePath(defaultFile, page) {
|
|
1602
|
-
return page === void 0 ? defaultFile :
|
|
1800
|
+
return page === void 0 ? defaultFile : join3(dirname4(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
1603
1801
|
}
|
|
1604
1802
|
function pageIdOfFile(defaultFile, path) {
|
|
1605
1803
|
if (path === defaultFile) return void 0;
|
|
@@ -1608,126 +1806,161 @@ function pageIdOfFile(defaultFile, path) {
|
|
|
1608
1806
|
}
|
|
1609
1807
|
function listPageFiles(defaultFile) {
|
|
1610
1808
|
const out = [];
|
|
1611
|
-
if (
|
|
1809
|
+
if (existsSync2(defaultFile)) out.push(defaultFile);
|
|
1612
1810
|
let entries = [];
|
|
1613
1811
|
try {
|
|
1614
|
-
entries = readdirSync(
|
|
1812
|
+
entries = readdirSync(join3(dirname4(defaultFile), PAGES_DIR_NAME));
|
|
1615
1813
|
} catch {
|
|
1616
1814
|
}
|
|
1617
1815
|
for (const e of entries.sort()) {
|
|
1618
|
-
if (e.endsWith(".json")) out.push(
|
|
1816
|
+
if (e.endsWith(".json")) out.push(join3(dirname4(defaultFile), PAGES_DIR_NAME, e));
|
|
1619
1817
|
}
|
|
1620
1818
|
return out;
|
|
1621
1819
|
}
|
|
1622
1820
|
function deletePageFile(path) {
|
|
1623
1821
|
try {
|
|
1624
|
-
|
|
1822
|
+
rmSync3(path, { force: true });
|
|
1625
1823
|
return ok(void 0);
|
|
1626
1824
|
} catch (e) {
|
|
1627
1825
|
return err({ kind: "delete-failed", path, detail: errnoOf(e) });
|
|
1628
1826
|
}
|
|
1629
1827
|
}
|
|
1828
|
+
|
|
1829
|
+
// src/store/channels.ts
|
|
1830
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync5 } from "node:fs";
|
|
1831
|
+
import { dirname as dirname6, join as join5 } from "node:path";
|
|
1832
|
+
|
|
1833
|
+
// src/store/json-text.ts
|
|
1834
|
+
function isRecord2(v) {
|
|
1835
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1836
|
+
}
|
|
1837
|
+
function stripBom(text) {
|
|
1838
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/store/viewers.ts
|
|
1842
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, rmSync as rmSync4 } from "node:fs";
|
|
1843
|
+
import { dirname as dirname5, join as join4 } from "node:path";
|
|
1844
|
+
var VIEWERS_DIR_NAME = "viewers";
|
|
1845
|
+
var VIEWER_FILE_VERSION = 1;
|
|
1846
|
+
var VIEWER_HEARTBEAT_MS = 1e3;
|
|
1847
|
+
function viewersDirPath(defaultFile) {
|
|
1848
|
+
return join4(dirname5(defaultFile), VIEWERS_DIR_NAME);
|
|
1849
|
+
}
|
|
1850
|
+
function viewerFilePath(defaultFile, pid) {
|
|
1851
|
+
return join4(viewersDirPath(defaultFile), `${pid}.json`);
|
|
1852
|
+
}
|
|
1853
|
+
function publishViewer(defaultFile, pid, report) {
|
|
1854
|
+
const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow, owner: report.owner };
|
|
1855
|
+
return writeAtomic(viewerFilePath(defaultFile, pid), `${JSON.stringify(body, null, 2)}
|
|
1856
|
+
`, 1);
|
|
1857
|
+
}
|
|
1858
|
+
function retireViewer(defaultFile, pid) {
|
|
1859
|
+
try {
|
|
1860
|
+
rmSync4(viewerFilePath(defaultFile, pid), { force: true });
|
|
1861
|
+
} catch {
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/store/channels.ts
|
|
1630
1866
|
var FOCUS_FILE_NAME = "focus";
|
|
1631
|
-
function focusFilePath(defaultFile) {
|
|
1632
|
-
return
|
|
1867
|
+
function focusFilePath(defaultFile, pid) {
|
|
1868
|
+
return paneChannelPath(defaultFile, FOCUS_FILE_NAME, pid);
|
|
1633
1869
|
}
|
|
1634
|
-
function
|
|
1635
|
-
|
|
1870
|
+
function paneChannelPath(defaultFile, channel, pid) {
|
|
1871
|
+
if (pid === void 0) return join5(dirname6(defaultFile), channel);
|
|
1872
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("Invalid pane process id");
|
|
1873
|
+
return join5(viewersDirPath(defaultFile), `${pid}.${channel}`);
|
|
1874
|
+
}
|
|
1875
|
+
function takeFocusRequest(defaultFile, pid) {
|
|
1876
|
+
const targeted = pid === void 0 ? void 0 : focusFilePath(defaultFile, pid);
|
|
1877
|
+
const path = targeted !== void 0 && existsSync3(targeted) ? targeted : focusFilePath(defaultFile);
|
|
1636
1878
|
let raw;
|
|
1637
1879
|
try {
|
|
1638
|
-
raw =
|
|
1880
|
+
raw = readFileSync3(path, "utf8");
|
|
1639
1881
|
} catch {
|
|
1640
1882
|
return void 0;
|
|
1641
1883
|
}
|
|
1642
1884
|
try {
|
|
1643
|
-
|
|
1885
|
+
rmSync5(path, { force: true });
|
|
1644
1886
|
} catch {
|
|
1645
1887
|
}
|
|
1646
|
-
let
|
|
1888
|
+
let parsed2;
|
|
1647
1889
|
try {
|
|
1648
|
-
|
|
1890
|
+
parsed2 = JSON.parse(raw);
|
|
1649
1891
|
} catch {
|
|
1650
1892
|
return void 0;
|
|
1651
1893
|
}
|
|
1652
|
-
if (typeof
|
|
1653
|
-
const page =
|
|
1894
|
+
if (typeof parsed2 !== "object" || parsed2 === null) return void 0;
|
|
1895
|
+
const page = parsed2.page;
|
|
1654
1896
|
if (page === void 0 || page === null) return { page: void 0 };
|
|
1655
1897
|
if (typeof page !== "string") return void 0;
|
|
1656
1898
|
const id = makePageId(page);
|
|
1657
1899
|
return id.ok ? { page: id.value } : void 0;
|
|
1658
1900
|
}
|
|
1659
1901
|
var QUIT_FILE_NAME = "quit";
|
|
1660
|
-
function quitFilePath(defaultFile) {
|
|
1661
|
-
return
|
|
1902
|
+
function quitFilePath(defaultFile, pid) {
|
|
1903
|
+
return paneChannelPath(defaultFile, QUIT_FILE_NAME, pid);
|
|
1662
1904
|
}
|
|
1663
|
-
function takeQuitRequest(defaultFile) {
|
|
1664
|
-
const
|
|
1905
|
+
function takeQuitRequest(defaultFile, pid) {
|
|
1906
|
+
const targeted = pid === void 0 ? void 0 : quitFilePath(defaultFile, pid);
|
|
1907
|
+
const path = targeted !== void 0 && existsSync3(targeted) ? targeted : quitFilePath(defaultFile);
|
|
1665
1908
|
let raw;
|
|
1666
1909
|
try {
|
|
1667
|
-
raw =
|
|
1910
|
+
raw = readFileSync3(path, "utf8");
|
|
1668
1911
|
} catch {
|
|
1669
1912
|
return false;
|
|
1670
1913
|
}
|
|
1671
|
-
sweepQuitRequest(defaultFile);
|
|
1672
|
-
let
|
|
1914
|
+
sweepQuitRequest(defaultFile, path === targeted ? pid : void 0);
|
|
1915
|
+
let parsed2;
|
|
1673
1916
|
try {
|
|
1674
|
-
|
|
1917
|
+
parsed2 = JSON.parse(stripBom(raw));
|
|
1675
1918
|
} catch {
|
|
1676
1919
|
return false;
|
|
1677
1920
|
}
|
|
1678
|
-
return isRecord2(
|
|
1679
|
-
}
|
|
1680
|
-
function sweepQuitRequest(defaultFile) {
|
|
1681
|
-
try {
|
|
1682
|
-
rmSync(quitFilePath(defaultFile), { force: true });
|
|
1683
|
-
} catch {
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
var VIEWERS_DIR_NAME = "viewers";
|
|
1687
|
-
var VIEWER_FILE_VERSION = 1;
|
|
1688
|
-
var VIEWER_HEARTBEAT_MS = 1e3;
|
|
1689
|
-
function viewersDirPath(defaultFile) {
|
|
1690
|
-
return join(dirname(defaultFile), VIEWERS_DIR_NAME);
|
|
1691
|
-
}
|
|
1692
|
-
function viewerFilePath(defaultFile, pid) {
|
|
1693
|
-
return join(viewersDirPath(defaultFile), `${pid}.json`);
|
|
1921
|
+
return isRecord2(parsed2);
|
|
1694
1922
|
}
|
|
1695
|
-
function
|
|
1696
|
-
const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow };
|
|
1697
|
-
return writeFileAtomic(viewerFilePath(defaultFile, pid), `${JSON.stringify(body, null, 2)}
|
|
1698
|
-
`);
|
|
1699
|
-
}
|
|
1700
|
-
function retireViewer(defaultFile, pid) {
|
|
1923
|
+
function sweepQuitRequest(defaultFile, pid) {
|
|
1701
1924
|
try {
|
|
1702
|
-
|
|
1925
|
+
rmSync5(quitFilePath(defaultFile, pid), { force: true });
|
|
1703
1926
|
} catch {
|
|
1704
1927
|
}
|
|
1705
1928
|
}
|
|
1929
|
+
|
|
1930
|
+
// src/store/policy.ts
|
|
1931
|
+
import { dirname as dirname7, join as join6 } from "node:path";
|
|
1706
1932
|
var CONFIG_FILE_NAME = "config.json";
|
|
1707
1933
|
function configFilePath(defaultFile) {
|
|
1708
|
-
return
|
|
1934
|
+
return join6(dirname7(defaultFile), CONFIG_FILE_NAME);
|
|
1709
1935
|
}
|
|
1710
|
-
|
|
1936
|
+
|
|
1937
|
+
// src/store/migration.ts
|
|
1938
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "node:fs";
|
|
1939
|
+
import { dirname as dirname8, join as join7 } from "node:path";
|
|
1940
|
+
var LEGACY_STATE_FILE_RELATIVE_PATH = join7(".claude", "mellos-mapping.json");
|
|
1711
1941
|
var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
|
|
1712
1942
|
var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
|
|
1713
1943
|
function migrateLegacyStore(defaultFile) {
|
|
1714
|
-
const projectRoot =
|
|
1715
|
-
const legacyDefault =
|
|
1716
|
-
const legacyPages =
|
|
1717
|
-
const legacyConfig =
|
|
1718
|
-
const hasLegacy =
|
|
1719
|
-
const hasCurrent =
|
|
1944
|
+
const projectRoot = dirname8(dirname8(defaultFile));
|
|
1945
|
+
const legacyDefault = join7(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
|
|
1946
|
+
const legacyPages = join7(dirname8(legacyDefault), LEGACY_PAGES_DIR_NAME);
|
|
1947
|
+
const legacyConfig = join7(dirname8(legacyDefault), LEGACY_CONFIG_FILE_NAME);
|
|
1948
|
+
const hasLegacy = existsSync4(legacyDefault) || existsSync4(legacyPages) || existsSync4(legacyConfig);
|
|
1949
|
+
const hasCurrent = existsSync4(defaultFile) || existsSync4(join7(dirname8(defaultFile), PAGES_DIR_NAME)) || existsSync4(configFilePath(defaultFile));
|
|
1720
1950
|
if (!hasLegacy || hasCurrent) return false;
|
|
1721
|
-
|
|
1722
|
-
if (
|
|
1723
|
-
if (
|
|
1724
|
-
if (
|
|
1951
|
+
mkdirSync3(dirname8(defaultFile), { recursive: true });
|
|
1952
|
+
if (existsSync4(legacyDefault)) renameSync2(legacyDefault, defaultFile);
|
|
1953
|
+
if (existsSync4(legacyPages)) renameSync2(legacyPages, join7(dirname8(defaultFile), PAGES_DIR_NAME));
|
|
1954
|
+
if (existsSync4(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
|
|
1725
1955
|
return true;
|
|
1726
1956
|
}
|
|
1957
|
+
|
|
1958
|
+
// src/store/maps.ts
|
|
1959
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1727
1960
|
function loadMapFile(path) {
|
|
1728
1961
|
let text;
|
|
1729
1962
|
try {
|
|
1730
|
-
text =
|
|
1963
|
+
text = readFileSync4(path, "utf8");
|
|
1731
1964
|
} catch (e) {
|
|
1732
1965
|
const code = e.code;
|
|
1733
1966
|
if (code === "ENOENT") return err({ kind: "not-found", path });
|
|
@@ -1843,6 +2076,175 @@ function parseInput(chunk) {
|
|
|
1843
2076
|
return { events, rest: "" };
|
|
1844
2077
|
}
|
|
1845
2078
|
|
|
2079
|
+
// src/watch/view-state.ts
|
|
2080
|
+
var defaultPage = () => ({ offsetX: 0, offsetY: 0, zoom: ZOOM_DEFAULT, selectedId: void 0 });
|
|
2081
|
+
function initialViewState() {
|
|
2082
|
+
return {
|
|
2083
|
+
...defaultPage(),
|
|
2084
|
+
pageViews: /* @__PURE__ */ new Map(),
|
|
2085
|
+
hoverId: void 0,
|
|
2086
|
+
dragAnchor: void 0,
|
|
2087
|
+
press: void 0,
|
|
2088
|
+
dividerDrag: false,
|
|
2089
|
+
lastClick: void 0
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
function reduceView(state, event) {
|
|
2093
|
+
switch (event.kind) {
|
|
2094
|
+
case "reset":
|
|
2095
|
+
return { ...state, offsetX: 0, offsetY: 0, zoom: ZOOM_DEFAULT };
|
|
2096
|
+
case "pan":
|
|
2097
|
+
return { ...state, offsetX: state.offsetX + event.dx, offsetY: state.offsetY + event.dy };
|
|
2098
|
+
case "position":
|
|
2099
|
+
return { ...state, offsetX: event.x, offsetY: event.y };
|
|
2100
|
+
case "zoom":
|
|
2101
|
+
return { ...state, zoom: event.value };
|
|
2102
|
+
case "hover":
|
|
2103
|
+
return event.id === state.hoverId ? state : { ...state, hoverId: event.id };
|
|
2104
|
+
case "select":
|
|
2105
|
+
return { ...state, selectedId: event.id };
|
|
2106
|
+
case "down":
|
|
2107
|
+
return {
|
|
2108
|
+
...state,
|
|
2109
|
+
dividerDrag: event.divider,
|
|
2110
|
+
dragAnchor: event.divider ? void 0 : { x: event.x, y: event.y, ox: state.offsetX, oy: state.offsetY },
|
|
2111
|
+
press: event.divider ? void 0 : { moved: false }
|
|
2112
|
+
};
|
|
2113
|
+
case "drag": {
|
|
2114
|
+
if (state.dividerDrag || !state.dragAnchor) return state;
|
|
2115
|
+
const { x, y, ox, oy } = state.dragAnchor;
|
|
2116
|
+
const offsetX = ox - (event.x - x);
|
|
2117
|
+
const offsetY = oy - (event.y - y);
|
|
2118
|
+
return offsetX === state.offsetX && offsetY === state.offsetY ? state : { ...state, offsetX, offsetY, press: { moved: true } };
|
|
2119
|
+
}
|
|
2120
|
+
case "up":
|
|
2121
|
+
return { ...state, dragAnchor: void 0, press: void 0, dividerDrag: false };
|
|
2122
|
+
case "pages": {
|
|
2123
|
+
const pageViews = new Map([...state.pageViews].filter(([file]) => event.files.includes(file)));
|
|
2124
|
+
if (event.previous === event.active || event.active === void 0) return { ...state, pageViews };
|
|
2125
|
+
if (event.previous !== void 0 && event.files.includes(event.previous)) pageViews.set(event.previous, {
|
|
2126
|
+
offsetX: state.offsetX,
|
|
2127
|
+
offsetY: state.offsetY,
|
|
2128
|
+
zoom: state.zoom,
|
|
2129
|
+
selectedId: state.selectedId
|
|
2130
|
+
});
|
|
2131
|
+
return {
|
|
2132
|
+
...state,
|
|
2133
|
+
...pageViews.get(event.active) ?? defaultPage(),
|
|
2134
|
+
pageViews,
|
|
2135
|
+
hoverId: void 0,
|
|
2136
|
+
dragAnchor: void 0,
|
|
2137
|
+
press: void 0,
|
|
2138
|
+
dividerDrag: false,
|
|
2139
|
+
lastClick: void 0
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function isPointerClick(state) {
|
|
2145
|
+
return !state.dividerDrag && state.press !== void 0 && !state.press.moved;
|
|
2146
|
+
}
|
|
2147
|
+
function clickNode(state, id, now, doubleClickMs) {
|
|
2148
|
+
const double = id !== void 0 && state.lastClick?.id === id && now - state.lastClick.at <= doubleClickMs;
|
|
2149
|
+
return {
|
|
2150
|
+
state: { ...state, selectedId: id, lastClick: double || id === void 0 ? void 0 : { id, at: now } },
|
|
2151
|
+
diveId: double ? id : void 0
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/watch/io.ts
|
|
2156
|
+
var nativeWatcherIO = () => ({
|
|
2157
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
2158
|
+
input: process.stdin,
|
|
2159
|
+
output: process.stdout
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
// src/watch/terminal-session.ts
|
|
2163
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
2164
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
2165
|
+
var ENTER_SCREEN = "\x1B[?1049h";
|
|
2166
|
+
var LEAVE_SCREEN = "\x1B[?1049l";
|
|
2167
|
+
var CLEAR_SCREEN = "\x1B[H\x1B[2J";
|
|
2168
|
+
var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
|
|
2169
|
+
var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
|
|
2170
|
+
var RESET = "\x1B[0m";
|
|
2171
|
+
function terminalRestoreSequence(mouseActive, alternateScreen = false) {
|
|
2172
|
+
return (mouseActive ? MOUSE_OFF : "") + RESET + (alternateScreen ? LEAVE_SCREEN : "") + SHOW_CURSOR + (alternateScreen ? "" : "\n");
|
|
2173
|
+
}
|
|
2174
|
+
function openTerminalSession(options) {
|
|
2175
|
+
const { interactive, write } = options;
|
|
2176
|
+
const mouseActive = interactive && options.mouse;
|
|
2177
|
+
write((interactive ? ENTER_SCREEN : "") + HIDE_CURSOR + CLEAR_SCREEN + (mouseActive ? MOUSE_ON : ""));
|
|
2178
|
+
const timer = mouseActive ? setInterval(() => write(HIDE_CURSOR + MOUSE_ON), 1e3) : void 0;
|
|
2179
|
+
timer?.unref();
|
|
2180
|
+
let closed = false;
|
|
2181
|
+
return {
|
|
2182
|
+
close() {
|
|
2183
|
+
if (closed) return;
|
|
2184
|
+
closed = true;
|
|
2185
|
+
clearInterval(timer);
|
|
2186
|
+
write(terminalRestoreSequence(mouseActive, interactive));
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/watch/frame-output.ts
|
|
2192
|
+
function frameDifference(previous, next) {
|
|
2193
|
+
const compatible = previous?.columns === next.columns;
|
|
2194
|
+
let output = "";
|
|
2195
|
+
for (let row = 0; row < Math.max(next.rows.length, previous?.rows.length ?? 0); row++) {
|
|
2196
|
+
const text = next.rows[row] ?? "";
|
|
2197
|
+
if (compatible && text === previous?.rows[row]) continue;
|
|
2198
|
+
output += `\x1B[${row + 1};1H${text}\x1B[0m\x1B[K`;
|
|
2199
|
+
}
|
|
2200
|
+
return output;
|
|
2201
|
+
}
|
|
2202
|
+
function createFrameOutput(port) {
|
|
2203
|
+
let previous;
|
|
2204
|
+
let latest;
|
|
2205
|
+
let pending;
|
|
2206
|
+
let unsubscribe;
|
|
2207
|
+
let closed = false;
|
|
2208
|
+
const flush = () => {
|
|
2209
|
+
if (closed || unsubscribe !== void 0 || pending === void 0) return;
|
|
2210
|
+
const frame = pending;
|
|
2211
|
+
pending = void 0;
|
|
2212
|
+
const output = frameDifference(previous, frame);
|
|
2213
|
+
if (output === "") return;
|
|
2214
|
+
previous = frame;
|
|
2215
|
+
if (!port.write(output)) {
|
|
2216
|
+
unsubscribe = port.onDrain(() => {
|
|
2217
|
+
unsubscribe?.();
|
|
2218
|
+
unsubscribe = void 0;
|
|
2219
|
+
flush();
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
const refresh = setInterval(() => {
|
|
2224
|
+
previous = void 0;
|
|
2225
|
+
pending = latest;
|
|
2226
|
+
flush();
|
|
2227
|
+
}, 1e3);
|
|
2228
|
+
refresh.unref();
|
|
2229
|
+
return {
|
|
2230
|
+
present(frame) {
|
|
2231
|
+
if (closed) return;
|
|
2232
|
+
latest = pending = frame;
|
|
2233
|
+
flush();
|
|
2234
|
+
},
|
|
2235
|
+
invalidate() {
|
|
2236
|
+
previous = void 0;
|
|
2237
|
+
},
|
|
2238
|
+
close() {
|
|
2239
|
+
closed = true;
|
|
2240
|
+
clearInterval(refresh);
|
|
2241
|
+
unsubscribe?.();
|
|
2242
|
+
unsubscribe = void 0;
|
|
2243
|
+
pending = latest = previous = void 0;
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
|
|
1846
2248
|
// src/watch/pane-state.ts
|
|
1847
2249
|
function describePageFault(fault) {
|
|
1848
2250
|
return fault.kind === "unreadable" ? `cannot read ${fault.path}: ${fault.detail}` : describeStoreError(fault);
|
|
@@ -2011,13 +2413,14 @@ function describeArgsError(e) {
|
|
|
2011
2413
|
}
|
|
2012
2414
|
var USAGE = "usage: mellos-mapping-watch [--file <map.json>] [--page <slug>] [--interval <ms>] [--ascii] [--no-color] [--no-mouse] [--no-follow]";
|
|
2013
2415
|
function parseArgs(argv, cwd) {
|
|
2014
|
-
let file =
|
|
2416
|
+
let file = join8(resolveProjectDirectory(cwd), STATE_FILE_RELATIVE_PATH);
|
|
2015
2417
|
let intervalMs = POLL_INTERVAL_DEFAULT_MS;
|
|
2016
2418
|
let unicode = true;
|
|
2017
2419
|
let color = true;
|
|
2018
2420
|
let mouse = true;
|
|
2019
2421
|
let page;
|
|
2020
2422
|
let follow = true;
|
|
2423
|
+
let owner;
|
|
2021
2424
|
const valueOf = (flag, raw) => raw === void 0 || raw.startsWith("--") ? err({ kind: "missing-value", flag }) : ok(raw);
|
|
2022
2425
|
for (let i = 0; i < argv.length; i++) {
|
|
2023
2426
|
const flag = argv[i];
|
|
@@ -2031,9 +2434,17 @@ function parseArgs(argv, cwd) {
|
|
|
2031
2434
|
case "--page": {
|
|
2032
2435
|
const value = valueOf(flag, argv[++i]);
|
|
2033
2436
|
if (!value.ok) return value;
|
|
2034
|
-
const
|
|
2035
|
-
if (!
|
|
2036
|
-
page =
|
|
2437
|
+
const parsed2 = makePageId(value.value);
|
|
2438
|
+
if (!parsed2.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed2.error.rule });
|
|
2439
|
+
page = parsed2.value;
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
2442
|
+
case "--owner": {
|
|
2443
|
+
const value = valueOf(flag, argv[++i]);
|
|
2444
|
+
if (!value.ok) return value;
|
|
2445
|
+
const parsed2 = makePageId(value.value);
|
|
2446
|
+
if (!parsed2.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed2.error.rule });
|
|
2447
|
+
owner = parsed2.value;
|
|
2037
2448
|
break;
|
|
2038
2449
|
}
|
|
2039
2450
|
case "--interval": {
|
|
@@ -2062,7 +2473,7 @@ function parseArgs(argv, cwd) {
|
|
|
2062
2473
|
return err({ kind: "unknown-flag", flag });
|
|
2063
2474
|
}
|
|
2064
2475
|
}
|
|
2065
|
-
return ok({ file, intervalMs, unicode, color, mouse, page, follow });
|
|
2476
|
+
return ok({ file, intervalMs, unicode, color, mouse, page, follow, ...owner === void 0 ? {} : { owner } });
|
|
2066
2477
|
}
|
|
2067
2478
|
function readPage(file) {
|
|
2068
2479
|
try {
|
|
@@ -2071,9 +2482,9 @@ function readPage(file) {
|
|
|
2071
2482
|
return err({ kind: "unreadable", path: file, detail: e.code ?? e.message });
|
|
2072
2483
|
}
|
|
2073
2484
|
}
|
|
2074
|
-
function renderWindow(map, opts, viewport) {
|
|
2485
|
+
function renderWindow(map, opts, viewport, render = renderMapWindow) {
|
|
2075
2486
|
try {
|
|
2076
|
-
return ok(
|
|
2487
|
+
return ok(render(map, opts, viewport));
|
|
2077
2488
|
} catch (e) {
|
|
2078
2489
|
return err(`this map could not be drawn: ${e.message}`);
|
|
2079
2490
|
}
|
|
@@ -2090,17 +2501,10 @@ function dividerRow(width, unicode, follow) {
|
|
|
2090
2501
|
}
|
|
2091
2502
|
return bar;
|
|
2092
2503
|
}
|
|
2093
|
-
var HIDE_CURSOR = "\x1B[?25l";
|
|
2094
|
-
var SHOW_CURSOR = "\x1B[?25h";
|
|
2095
2504
|
var CLEAR_ALL = "\x1B[H\x1B[2J";
|
|
2096
2505
|
var HOME = "\x1B[H";
|
|
2097
2506
|
var ERASE_LINE_END = "\x1B[K";
|
|
2098
|
-
var
|
|
2099
|
-
var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
|
|
2100
|
-
var RESET = "\x1B[0m";
|
|
2101
|
-
function terminalRestoreSequence(mouseActive) {
|
|
2102
|
-
return (mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + RESET + "\n";
|
|
2103
|
-
}
|
|
2507
|
+
var RESET2 = "\x1B[0m";
|
|
2104
2508
|
var PANEL_CONTENT_ROWS = 6;
|
|
2105
2509
|
var PANEL_ROWS_MIN = 2;
|
|
2106
2510
|
var MAP_ROWS_MIN = 4;
|
|
@@ -2291,7 +2695,7 @@ function waitingInfo(s, width) {
|
|
|
2291
2695
|
const clock = s.elapsedMs !== void 0 ? ` \xB7 waiting ${elapsedLabel(s.elapsedMs)}` : "";
|
|
2292
2696
|
const lines = [
|
|
2293
2697
|
`watching ${s.defaultFile}`,
|
|
2294
|
-
` and ${
|
|
2698
|
+
` and ${join8(s.pagesDir, "*.json")}`,
|
|
2295
2699
|
`polling every ${s.intervalMs} ms${clock}`
|
|
2296
2700
|
];
|
|
2297
2701
|
for (const b of s.broken) lines.push(`! ${b}`);
|
|
@@ -2300,7 +2704,7 @@ function waitingInfo(s, width) {
|
|
|
2300
2704
|
}
|
|
2301
2705
|
function splashFrame(notice, info, frame, width, height, unicode, color) {
|
|
2302
2706
|
if (width < 24 || height < info.length + 3) return void 0;
|
|
2303
|
-
const dim = (s) => color ? `\x1B[90m${s}${
|
|
2707
|
+
const dim = (s) => color ? `\x1B[90m${s}${RESET2}` : s;
|
|
2304
2708
|
const spinner = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
|
|
2305
2709
|
const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
|
|
2306
2710
|
const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
|
|
@@ -2331,17 +2735,12 @@ function viewerReportOf(pane, defaultFile) {
|
|
|
2331
2735
|
const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
|
|
2332
2736
|
return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
|
|
2333
2737
|
}
|
|
2334
|
-
function
|
|
2335
|
-
const
|
|
2336
|
-
if (!parsed.ok) {
|
|
2337
|
-
console.error(`mellos-mapping-watch: ${describeArgsError(parsed.error)}
|
|
2338
|
-
${USAGE}`);
|
|
2339
|
-
process.exit(1);
|
|
2340
|
-
}
|
|
2341
|
-
const cfg = parsed.value;
|
|
2738
|
+
function runWatcher(cfg, io = nativeWatcherIO()) {
|
|
2739
|
+
const renderScene = createWindowRenderer();
|
|
2342
2740
|
if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
|
|
2343
2741
|
sweepQuitRequest(cfg.file);
|
|
2344
|
-
|
|
2742
|
+
sweepQuitRequest(cfg.file, process.pid);
|
|
2743
|
+
const interactive = io.interactive;
|
|
2345
2744
|
const mouseActive = interactive && cfg.mouse;
|
|
2346
2745
|
let lastFrame = "";
|
|
2347
2746
|
let spinnerFrame = 0;
|
|
@@ -2350,28 +2749,19 @@ ${USAGE}`);
|
|
|
2350
2749
|
const standbyNotice = "waiting for the first mmap_declare ...";
|
|
2351
2750
|
let map;
|
|
2352
2751
|
let notice = standbyNotice;
|
|
2353
|
-
let lastCols =
|
|
2354
|
-
let lastRows =
|
|
2752
|
+
let lastCols = io.output.columns ?? 0;
|
|
2753
|
+
let lastRows = io.output.rows ?? 0;
|
|
2355
2754
|
let pane = initialPaneState(
|
|
2356
2755
|
cfg.follow,
|
|
2357
2756
|
cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
|
|
2358
2757
|
);
|
|
2359
|
-
|
|
2758
|
+
let view = initialViewState();
|
|
2360
2759
|
let lastTabSegments = [];
|
|
2361
2760
|
let tabScroll = 0;
|
|
2362
|
-
let offsetX = 0;
|
|
2363
|
-
let offsetY = 0;
|
|
2364
|
-
let zoom = ZOOM_DEFAULT;
|
|
2365
|
-
let dragAnchor;
|
|
2366
|
-
let press;
|
|
2367
|
-
let hoverId;
|
|
2368
|
-
let selectedId;
|
|
2369
2761
|
let lastHits = [];
|
|
2370
2762
|
let lastContent = { w: 0, h: 0 };
|
|
2371
2763
|
let pendingInput = "";
|
|
2372
2764
|
let panelContentRows = PANEL_CONTENT_ROWS;
|
|
2373
|
-
let dividerDrag = false;
|
|
2374
|
-
let lastClick;
|
|
2375
2765
|
let flash;
|
|
2376
2766
|
let lastTabFiles = [];
|
|
2377
2767
|
const topFiles = () => topLevelFiles(cfg.file, filesOf(pane), mapsOf(pane));
|
|
@@ -2387,8 +2777,8 @@ ${USAGE}`);
|
|
|
2387
2777
|
}
|
|
2388
2778
|
return false;
|
|
2389
2779
|
};
|
|
2390
|
-
const viewWidth = () => usableColumns(
|
|
2391
|
-
const viewHeight = () => Math.max(1, (
|
|
2780
|
+
const viewWidth = () => usableColumns(io.output.columns ?? FALLBACK_COLUMNS);
|
|
2781
|
+
const viewHeight = () => Math.max(1, (io.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
|
|
2392
2782
|
const dividerY = () => tabRows() + viewHeight() + 1;
|
|
2393
2783
|
const pageTabsOf = (files) => files.map((f) => {
|
|
2394
2784
|
const entry = entryOf(pane, f);
|
|
@@ -2415,14 +2805,8 @@ ${USAGE}`);
|
|
|
2415
2805
|
};
|
|
2416
2806
|
const adoptView = (previous) => {
|
|
2417
2807
|
const file = pane.activeFile;
|
|
2808
|
+
view = reduceView(view, { kind: "pages", previous, active: file, files: filesOf(pane) });
|
|
2418
2809
|
if (file === void 0 || file === previous) return;
|
|
2419
|
-
if (previous !== void 0) pageViews.set(previous, { offsetX, offsetY, zoom, selectedId });
|
|
2420
|
-
const view = pageViews.get(file);
|
|
2421
|
-
offsetX = view?.offsetX ?? 0;
|
|
2422
|
-
offsetY = view?.offsetY ?? 0;
|
|
2423
|
-
zoom = view?.zoom ?? ZOOM_DEFAULT;
|
|
2424
|
-
selectedId = view?.selectedId;
|
|
2425
|
-
hoverId = void 0;
|
|
2426
2810
|
const top = topFiles();
|
|
2427
2811
|
const tabIndex = top.indexOf(file);
|
|
2428
2812
|
if (tabIndex >= 0) {
|
|
@@ -2444,16 +2828,33 @@ ${USAGE}`);
|
|
|
2444
2828
|
const sy = termY - 1 - tabRows();
|
|
2445
2829
|
if (sx < 0 || sx >= viewWidth()) return void 0;
|
|
2446
2830
|
if (sy < 0 || sy >= viewHeight()) return void 0;
|
|
2447
|
-
const cx = sx + offsetX;
|
|
2448
|
-
const cy = sy + offsetY;
|
|
2831
|
+
const cx = sx + view.offsetX;
|
|
2832
|
+
const cy = sy + view.offsetY;
|
|
2449
2833
|
return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
|
|
2450
2834
|
};
|
|
2451
2835
|
const publishPresence = () => {
|
|
2452
|
-
|
|
2836
|
+
io.report?.(viewerReportOf(pane, cfg.file));
|
|
2837
|
+
publishViewer(cfg.file, process.pid, {
|
|
2838
|
+
...viewerReportOf(pane, cfg.file),
|
|
2839
|
+
...cfg.owner === void 0 ? {} : { owner: cfg.owner }
|
|
2840
|
+
});
|
|
2453
2841
|
};
|
|
2454
|
-
|
|
2842
|
+
if (interactive) io.input.setRawMode(true);
|
|
2843
|
+
const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io.output.write(text) });
|
|
2844
|
+
const frameOutput = interactive ? createFrameOutput({
|
|
2845
|
+
write: (text) => io.output.write(text),
|
|
2846
|
+
onDrain: (ready) => {
|
|
2847
|
+
io.output.once("drain", ready);
|
|
2848
|
+
return () => {
|
|
2849
|
+
io.output.off("drain", ready);
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
}) : void 0;
|
|
2853
|
+
let paintTimer;
|
|
2455
2854
|
process.on("exit", () => {
|
|
2456
|
-
|
|
2855
|
+
clearTimeout(paintTimer);
|
|
2856
|
+
frameOutput?.close();
|
|
2857
|
+
terminal.close();
|
|
2457
2858
|
retireViewer(cfg.file, process.pid);
|
|
2458
2859
|
});
|
|
2459
2860
|
const quit = () => process.exit(0);
|
|
@@ -2465,20 +2866,31 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2465
2866
|
`);
|
|
2466
2867
|
process.exit(1);
|
|
2467
2868
|
});
|
|
2869
|
+
const sceneOptions = () => ({
|
|
2870
|
+
color: cfg.color,
|
|
2871
|
+
unicode: cfg.unicode,
|
|
2872
|
+
zoom: view.zoom,
|
|
2873
|
+
focus: view.hoverId ?? view.selectedId,
|
|
2874
|
+
// A spinner on another page must not invalidate this completed picture.
|
|
2875
|
+
spinnerFrame: map?.nodes.some((node) => node.status === "in-progress") ? spinnerFrame : 0
|
|
2876
|
+
});
|
|
2468
2877
|
const paint2 = () => {
|
|
2469
|
-
|
|
2878
|
+
clearTimeout(paintTimer);
|
|
2879
|
+
paintTimer = void 0;
|
|
2880
|
+
const cols = io.output.columns ?? FALLBACK_COLUMNS;
|
|
2470
2881
|
const viewW = viewWidth();
|
|
2471
|
-
panelContentRows = clampPanelRows(panelContentRows,
|
|
2882
|
+
panelContentRows = clampPanelRows(panelContentRows, io.output.rows ?? FALLBACK_ROWS, tabRows());
|
|
2472
2883
|
const viewH = viewHeight();
|
|
2473
|
-
const focus = hoverId ?? selectedId;
|
|
2884
|
+
const focus = view.hoverId ?? view.selectedId;
|
|
2474
2885
|
let body;
|
|
2475
2886
|
let panned = "";
|
|
2476
2887
|
let pannable = false;
|
|
2477
2888
|
if (map !== void 0) {
|
|
2478
2889
|
const rendered = renderWindow(
|
|
2479
2890
|
map,
|
|
2480
|
-
|
|
2481
|
-
{ x: offsetX, y: offsetY, width: viewW, height: viewH }
|
|
2891
|
+
sceneOptions(),
|
|
2892
|
+
{ x: view.offsetX, y: view.offsetY, width: viewW, height: viewH },
|
|
2893
|
+
renderScene
|
|
2482
2894
|
);
|
|
2483
2895
|
if (!rendered.ok) {
|
|
2484
2896
|
body = ["", fitWidth(` ! ${rendered.error}`, viewW), ""];
|
|
@@ -2487,9 +2899,12 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2487
2899
|
const windowed = rendered.value;
|
|
2488
2900
|
const maxX = Math.max(0, windowed.contentWidth - viewW);
|
|
2489
2901
|
const maxY = Math.max(0, windowed.contentHeight - viewH);
|
|
2490
|
-
if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
|
|
2491
|
-
|
|
2492
|
-
|
|
2902
|
+
if (view.offsetX > maxX || view.offsetY > maxY || view.offsetX < 0 || view.offsetY < 0) {
|
|
2903
|
+
view = reduceView(view, {
|
|
2904
|
+
kind: "position",
|
|
2905
|
+
x: Math.min(Math.max(0, view.offsetX), maxX),
|
|
2906
|
+
y: Math.min(Math.max(0, view.offsetY), maxY)
|
|
2907
|
+
});
|
|
2493
2908
|
paint2();
|
|
2494
2909
|
return;
|
|
2495
2910
|
}
|
|
@@ -2497,13 +2912,13 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2497
2912
|
body = windowed.lines;
|
|
2498
2913
|
lastHits = windowed.hits;
|
|
2499
2914
|
lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
|
|
2500
|
-
if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
|
|
2915
|
+
if (view.offsetX !== 0 || view.offsetY !== 0) panned = ` (+${view.offsetX},+${view.offsetY})`;
|
|
2501
2916
|
}
|
|
2502
2917
|
} else {
|
|
2503
2918
|
const info = waitingInfo(
|
|
2504
2919
|
{
|
|
2505
2920
|
defaultFile: cfg.file,
|
|
2506
|
-
pagesDir:
|
|
2921
|
+
pagesDir: join8(dirname9(cfg.file), PAGES_DIR_NAME),
|
|
2507
2922
|
intervalMs: cfg.intervalMs,
|
|
2508
2923
|
elapsedMs: interactive ? Date.now() - startedAt : void 0,
|
|
2509
2924
|
broken: pane.pages.flatMap(
|
|
@@ -2522,15 +2937,15 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2522
2937
|
if (map === void 0) {
|
|
2523
2938
|
panel = Array.from({ length: panelContentRows }, () => ({ text: "", sgr: "" }));
|
|
2524
2939
|
} else if (focus !== void 0) {
|
|
2525
|
-
panel = nodePanel(map, focus, cfg.unicode, panelWidth, selectedId === focus, panelContentRows) ?? mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
|
|
2940
|
+
panel = nodePanel(map, focus, cfg.unicode, panelWidth, view.selectedId === focus, panelContentRows) ?? mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
|
|
2526
2941
|
} else {
|
|
2527
2942
|
panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
|
|
2528
2943
|
}
|
|
2529
2944
|
const separator = dividerRow(viewW, cfg.unicode, pane.follow);
|
|
2530
2945
|
const panelRows = [
|
|
2531
|
-
cfg.color ? `\x1B[90m${separator}${
|
|
2946
|
+
cfg.color ? `\x1B[90m${separator}${RESET2}` : separator,
|
|
2532
2947
|
...panel.map(
|
|
2533
|
-
(l) => cfg.color && l.sgr !== "" && l.text !== "" ? ` \x1B[${l.sgr}m${l.text}${
|
|
2948
|
+
(l) => cfg.color && l.sgr !== "" && l.text !== "" ? ` \x1B[${l.sgr}m${l.text}${RESET2}` : ` ${l.text}`
|
|
2534
2949
|
)
|
|
2535
2950
|
];
|
|
2536
2951
|
let tabLine;
|
|
@@ -2552,43 +2967,53 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2552
2967
|
action: { kind: "back" }
|
|
2553
2968
|
};
|
|
2554
2969
|
lastTabSegments = [head, tail];
|
|
2555
|
-
tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${
|
|
2970
|
+
tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET2}` : s.text).join("");
|
|
2556
2971
|
} else if (tabRows() > 0) {
|
|
2557
2972
|
const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll, mouseActive);
|
|
2558
2973
|
lastTabSegments = segments;
|
|
2559
|
-
tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${
|
|
2974
|
+
tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET2}` : s.text).join("");
|
|
2560
2975
|
} else {
|
|
2561
2976
|
lastTabSegments = [];
|
|
2562
2977
|
}
|
|
2563
|
-
const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
|
|
2978
|
+
const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(view.zoom)}`;
|
|
2564
2979
|
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";
|
|
2565
2980
|
const footerText = fitWidth(` ${hint}${panned}`, viewW);
|
|
2566
|
-
const footer = cfg.color ? `\x1B[90m${footerText}${
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2981
|
+
const footer = cfg.color ? `\x1B[90m${footerText}${RESET2}` : footerText;
|
|
2982
|
+
const rows = [
|
|
2983
|
+
...tabLine === void 0 ? [] : [tabLine],
|
|
2984
|
+
...Array.from({ length: viewH }, (_, i) => body[i] ?? ""),
|
|
2985
|
+
...panelRows,
|
|
2986
|
+
footer
|
|
2987
|
+
];
|
|
2988
|
+
if (frameOutput !== void 0) {
|
|
2989
|
+
frameOutput.present({ columns: cols, rows });
|
|
2990
|
+
} else {
|
|
2991
|
+
const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
|
|
2992
|
+
if (frame !== lastFrame) {
|
|
2993
|
+
io.output.write(frame);
|
|
2994
|
+
lastFrame = frame;
|
|
2995
|
+
}
|
|
2575
2996
|
}
|
|
2576
2997
|
};
|
|
2998
|
+
const requestPaint = () => {
|
|
2999
|
+
paintTimer ??= setTimeout(paint2, 16);
|
|
3000
|
+
};
|
|
2577
3001
|
const handleResize = () => {
|
|
2578
|
-
lastCols =
|
|
2579
|
-
lastRows =
|
|
3002
|
+
lastCols = io.output.columns ?? lastCols;
|
|
3003
|
+
lastRows = io.output.rows ?? lastRows;
|
|
2580
3004
|
lastFrame = "";
|
|
2581
|
-
|
|
3005
|
+
frameOutput?.invalidate();
|
|
3006
|
+
io.output.write(CLEAR_ALL);
|
|
2582
3007
|
paint2();
|
|
2583
3008
|
};
|
|
2584
3009
|
const tick = () => {
|
|
2585
|
-
if (takeQuitRequest(cfg.file)) quit();
|
|
2586
|
-
if ((
|
|
3010
|
+
if (takeQuitRequest(cfg.file, process.pid)) quit();
|
|
3011
|
+
if ((io.output.columns ?? lastCols) !== lastCols || (io.output.rows ?? lastRows) !== lastRows) {
|
|
2587
3012
|
handleResize();
|
|
2588
3013
|
}
|
|
2589
3014
|
const discovered = listPageFiles(cfg.file);
|
|
2590
3015
|
const files = discovered.length > 0 ? discovered : [cfg.file];
|
|
2591
|
-
const request = takeFocusRequest(cfg.file);
|
|
3016
|
+
const request = takeFocusRequest(cfg.file, process.pid);
|
|
2592
3017
|
const previous = pane.activeFile;
|
|
2593
3018
|
const scanned = scan(pane, {
|
|
2594
3019
|
files,
|
|
@@ -2603,12 +3028,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2603
3028
|
focusRequest: request === void 0 ? void 0 : pageFilePath(cfg.file, request.page),
|
|
2604
3029
|
// a drag in progress holds auto-follow off: the user is engaged with
|
|
2605
3030
|
// THIS page, and a missed switch is re-triggered by the next save
|
|
2606
|
-
engaged: dragAnchor !== void 0
|
|
3031
|
+
engaged: view.dragAnchor !== void 0
|
|
2607
3032
|
});
|
|
2608
3033
|
pane = scanned.state;
|
|
2609
|
-
for (const known of [...pageViews.keys()]) {
|
|
2610
|
-
if (!files.includes(known)) pageViews.delete(known);
|
|
2611
|
-
}
|
|
2612
3034
|
adoptView(previous);
|
|
2613
3035
|
adoptPage();
|
|
2614
3036
|
if (flash?.confirm === true && pane.pendingDelete === void 0) flash = void 0;
|
|
@@ -2644,15 +3066,20 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2644
3066
|
return;
|
|
2645
3067
|
}
|
|
2646
3068
|
const file = asked.request.file;
|
|
2647
|
-
|
|
3069
|
+
let removed;
|
|
3070
|
+
try {
|
|
3071
|
+
removed = withStoreLock(file, () => deletePageFile(file));
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
flash = { text: String(error), until: now + FLASH_NOTICE_MS };
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
2648
3076
|
flash = removed.ok ? { text: `deleted ${pageName(file)}`, until: now + FLASH_ACK_MS } : { text: describeStoreError(removed.error), until: now + FLASH_NOTICE_MS };
|
|
2649
3077
|
tick();
|
|
2650
3078
|
};
|
|
2651
3079
|
if (interactive) {
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
process.stdin.on("data", (chunk) => {
|
|
3080
|
+
io.input.resume();
|
|
3081
|
+
io.input.setEncoding("utf8");
|
|
3082
|
+
io.input.on("data", (chunk) => {
|
|
2656
3083
|
const parsed2 = parseInput(pendingInput + chunk);
|
|
2657
3084
|
pendingInput = parsed2.rest;
|
|
2658
3085
|
let dirty = false;
|
|
@@ -2662,22 +3089,19 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2662
3089
|
quit();
|
|
2663
3090
|
return;
|
|
2664
3091
|
case "reset":
|
|
2665
|
-
|
|
2666
|
-
offsetY = 0;
|
|
2667
|
-
zoom = ZOOM_DEFAULT;
|
|
3092
|
+
view = reduceView(view, { kind: "reset" });
|
|
2668
3093
|
dirty = true;
|
|
2669
3094
|
break;
|
|
2670
3095
|
case "clear":
|
|
2671
3096
|
if (pane.pendingDelete !== void 0) {
|
|
2672
3097
|
pane = disarmDelete(pane);
|
|
2673
3098
|
flash = void 0;
|
|
2674
|
-
} else if (selectedId !== void 0)
|
|
3099
|
+
} else if (view.selectedId !== void 0) view = reduceView(view, { kind: "select", id: void 0 });
|
|
2675
3100
|
else climbBack();
|
|
2676
3101
|
dirty = true;
|
|
2677
3102
|
break;
|
|
2678
3103
|
case "pan":
|
|
2679
|
-
|
|
2680
|
-
offsetY += event.dy;
|
|
3104
|
+
view = reduceView(view, event);
|
|
2681
3105
|
dirty = true;
|
|
2682
3106
|
break;
|
|
2683
3107
|
case "zoom": {
|
|
@@ -2686,15 +3110,16 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2686
3110
|
dirty = true;
|
|
2687
3111
|
break;
|
|
2688
3112
|
}
|
|
2689
|
-
const next = clampZoom(zoom + event.delta);
|
|
2690
|
-
if (next === zoom || map === void 0) break;
|
|
2691
|
-
const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
|
|
3113
|
+
const next = clampZoom(view.zoom + event.delta);
|
|
3114
|
+
if (next === view.zoom || map === void 0) break;
|
|
3115
|
+
const anchorId = view.hoverId ?? view.selectedId ?? nearestHit(lastHits, view.offsetX + viewWidth() / 2, view.offsetY + viewHeight() / 2)?.id;
|
|
2692
3116
|
const before = lastHits.find((h) => h.id === anchorId);
|
|
2693
|
-
|
|
3117
|
+
view = reduceView(view, { kind: "zoom", value: next });
|
|
2694
3118
|
const measured = renderWindow(
|
|
2695
3119
|
map,
|
|
2696
|
-
|
|
2697
|
-
{ x: 0, y: 0, width: 0, height: 0 }
|
|
3120
|
+
sceneOptions(),
|
|
3121
|
+
{ x: 0, y: 0, width: 0, height: 0 },
|
|
3122
|
+
renderScene
|
|
2698
3123
|
);
|
|
2699
3124
|
if (!measured.ok) {
|
|
2700
3125
|
dirty = true;
|
|
@@ -2704,57 +3129,48 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2704
3129
|
const after = before === void 0 ? void 0 : sized.hits.find((h) => h.id === before.id);
|
|
2705
3130
|
const moved = anchorOffsets(
|
|
2706
3131
|
before !== void 0 && after !== void 0 ? { before, after } : void 0,
|
|
2707
|
-
{ x: offsetX, y: offsetY },
|
|
3132
|
+
{ x: view.offsetX, y: view.offsetY },
|
|
2708
3133
|
lastContent,
|
|
2709
3134
|
{ w: sized.contentWidth, h: sized.contentHeight }
|
|
2710
3135
|
);
|
|
2711
|
-
|
|
2712
|
-
|
|
3136
|
+
view = reduceView(view, { kind: "position", ...moved });
|
|
3137
|
+
lastHits = sized.hits;
|
|
3138
|
+
lastContent = { w: sized.contentWidth, h: sized.contentHeight };
|
|
2713
3139
|
dirty = true;
|
|
2714
3140
|
break;
|
|
2715
3141
|
}
|
|
2716
3142
|
case "mouse-move": {
|
|
2717
3143
|
const over = hitTest(event.x, event.y);
|
|
2718
|
-
if (over !== hoverId) {
|
|
2719
|
-
|
|
3144
|
+
if (over !== view.hoverId) {
|
|
3145
|
+
view = reduceView(view, { kind: "hover", id: over });
|
|
2720
3146
|
dirty = true;
|
|
2721
3147
|
}
|
|
2722
3148
|
break;
|
|
2723
3149
|
}
|
|
2724
3150
|
case "mouse-down":
|
|
2725
|
-
|
|
2726
|
-
dividerDrag = true;
|
|
2727
|
-
break;
|
|
2728
|
-
}
|
|
2729
|
-
dragAnchor = { x: event.x, y: event.y, ox: offsetX, oy: offsetY };
|
|
2730
|
-
press = { moved: false };
|
|
3151
|
+
view = reduceView(view, { kind: "down", x: event.x, y: event.y, divider: event.y === dividerY() });
|
|
2731
3152
|
break;
|
|
2732
3153
|
case "mouse-drag":
|
|
2733
|
-
if (dividerDrag) {
|
|
2734
|
-
const next = panelRowsFromDividerY(event.y,
|
|
3154
|
+
if (view.dividerDrag) {
|
|
3155
|
+
const next = panelRowsFromDividerY(event.y, io.output.rows ?? FALLBACK_ROWS, tabRows());
|
|
2735
3156
|
if (next !== panelContentRows) {
|
|
2736
3157
|
panelContentRows = next;
|
|
2737
3158
|
dirty = true;
|
|
2738
3159
|
}
|
|
2739
3160
|
break;
|
|
2740
3161
|
}
|
|
2741
|
-
|
|
2742
|
-
const
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
offsetX = nx;
|
|
2746
|
-
offsetY = ny;
|
|
2747
|
-
if (press) press.moved = true;
|
|
2748
|
-
dirty = true;
|
|
2749
|
-
}
|
|
3162
|
+
{
|
|
3163
|
+
const nextView = reduceView(view, { kind: "drag", x: event.x, y: event.y });
|
|
3164
|
+
dirty ||= nextView !== view;
|
|
3165
|
+
view = nextView;
|
|
2750
3166
|
}
|
|
2751
3167
|
break;
|
|
2752
3168
|
case "mouse-up":
|
|
2753
|
-
if (dividerDrag) {
|
|
2754
|
-
|
|
3169
|
+
if (view.dividerDrag) {
|
|
3170
|
+
view = reduceView(view, { kind: "up" });
|
|
2755
3171
|
break;
|
|
2756
3172
|
}
|
|
2757
|
-
if (
|
|
3173
|
+
if (isPointerClick(view)) {
|
|
2758
3174
|
const tabHit = tabRows() > 0 && event.y === 1 ? lastTabSegments.find((s) => event.x >= s.lo && event.x <= s.hi) : void 0;
|
|
2759
3175
|
if (tabHit !== void 0) {
|
|
2760
3176
|
if (tabHit.action.kind === "back") {
|
|
@@ -2770,7 +3186,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2770
3186
|
} else {
|
|
2771
3187
|
const id = hitTest(event.x, event.y);
|
|
2772
3188
|
const now = Date.now();
|
|
2773
|
-
|
|
3189
|
+
const clicked = clickNode(view, id, now, DOUBLE_CLICK_MS);
|
|
3190
|
+
view = clicked.state;
|
|
3191
|
+
if (clicked.diveId !== void 0) {
|
|
2774
3192
|
const submap = map?.nodes.find((n) => n.id === id)?.submap;
|
|
2775
3193
|
if (submap !== void 0 && pane.activeFile !== void 0) {
|
|
2776
3194
|
const target = pageFilePath(cfg.file, submap);
|
|
@@ -2782,16 +3200,11 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2782
3200
|
flash = { text: `submap "${submap}" has no page yet`, until: now + FLASH_ACK_MS };
|
|
2783
3201
|
}
|
|
2784
3202
|
}
|
|
2785
|
-
lastClick = void 0;
|
|
2786
|
-
} else {
|
|
2787
|
-
lastClick = id !== void 0 ? { id, at: now } : void 0;
|
|
2788
3203
|
}
|
|
2789
|
-
selectedId = id;
|
|
2790
3204
|
}
|
|
2791
3205
|
dirty = true;
|
|
2792
3206
|
}
|
|
2793
|
-
|
|
2794
|
-
press = void 0;
|
|
3207
|
+
view = reduceView(view, { kind: "up" });
|
|
2795
3208
|
break;
|
|
2796
3209
|
case "next-page":
|
|
2797
3210
|
case "prev-page": {
|
|
@@ -2829,9 +3242,13 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2829
3242
|
break;
|
|
2830
3243
|
}
|
|
2831
3244
|
}
|
|
2832
|
-
if (dirty)
|
|
3245
|
+
if (dirty) {
|
|
3246
|
+
const motionOnly = parsed2.events.every((event) => event.kind === "mouse-move" || event.kind === "mouse-drag");
|
|
3247
|
+
if (motionOnly) requestPaint();
|
|
3248
|
+
else paint2();
|
|
3249
|
+
}
|
|
2833
3250
|
});
|
|
2834
|
-
|
|
3251
|
+
io.output.on("resize", handleResize);
|
|
2835
3252
|
}
|
|
2836
3253
|
tick();
|
|
2837
3254
|
setInterval(tick, cfg.intervalMs);
|
|
@@ -2845,43 +3262,12 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2845
3262
|
}, SPLASH_FRAME_MS);
|
|
2846
3263
|
}
|
|
2847
3264
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
|
-
main();
|
|
2858
|
-
}
|
|
2859
|
-
export {
|
|
2860
|
-
PANEL_ROWS_MIN,
|
|
2861
|
-
USAGE,
|
|
2862
|
-
anchorOffsets,
|
|
2863
|
-
clampPanelRows,
|
|
2864
|
-
describeArgsError,
|
|
2865
|
-
describePageFault,
|
|
2866
|
-
diveOrigin,
|
|
2867
|
-
dividerRow,
|
|
2868
|
-
elapsedLabel,
|
|
2869
|
-
fitWidth,
|
|
2870
|
-
launchedAsEntry,
|
|
2871
|
-
mapPanel,
|
|
2872
|
-
nearestHit,
|
|
2873
|
-
nodePanel,
|
|
2874
|
-
pageTabRow,
|
|
2875
|
-
panelRowsFromDividerY,
|
|
2876
|
-
parseArgs,
|
|
2877
|
-
readPage,
|
|
2878
|
-
renderWindow,
|
|
2879
|
-
splashFrame,
|
|
2880
|
-
tabScrollFor,
|
|
2881
|
-
terminalRestoreSequence,
|
|
2882
|
-
topLevelFiles,
|
|
2883
|
-
usableColumns,
|
|
2884
|
-
viewerReportOf,
|
|
2885
|
-
waitingInfo,
|
|
2886
|
-
wrapWidth
|
|
2887
|
-
};
|
|
3265
|
+
|
|
3266
|
+
// src/watch/cli.ts
|
|
3267
|
+
var parsed = parseArgs(process.argv.slice(2), process.cwd());
|
|
3268
|
+
if (!parsed.ok) {
|
|
3269
|
+
console.error(`mellos-mapping-watch: ${describeArgsError(parsed.error)}
|
|
3270
|
+
${USAGE}`);
|
|
3271
|
+
process.exit(1);
|
|
3272
|
+
}
|
|
3273
|
+
runWatcher(parsed.value);
|