@zenodinh/pi-render 0.0.0-stage → 0.1.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/index.ts +279 -0
  4. package/package.json +66 -5
  5. package/src/commands/canvas.test.ts +150 -0
  6. package/src/commands/canvas.ts +57 -0
  7. package/src/core/code-theme.test.ts +266 -0
  8. package/src/core/code-theme.ts +275 -0
  9. package/src/core/log.test.ts +127 -0
  10. package/src/core/log.ts +57 -0
  11. package/src/core/paint.test.ts +322 -0
  12. package/src/core/paint.ts +64 -0
  13. package/src/core/registry.test.ts +307 -0
  14. package/src/core/registry.ts +135 -0
  15. package/src/core/settings.test.ts +183 -0
  16. package/src/core/settings.ts +119 -0
  17. package/src/core/types/code-theme.ts +24 -0
  18. package/src/core/types/host.ts +160 -0
  19. package/src/core/types/log.ts +28 -0
  20. package/src/core/types/paint.ts +54 -0
  21. package/src/core/types/registry.ts +44 -0
  22. package/src/core/types/settings.ts +31 -0
  23. package/src/core/types.ts +34 -0
  24. package/src/renderers/content/artifacts/artifacts.test.ts +199 -0
  25. package/src/renderers/content/artifacts/cache.ts +234 -0
  26. package/src/renderers/content/artifacts/cards.test.ts +216 -0
  27. package/src/renderers/content/artifacts/cards.ts +136 -0
  28. package/src/renderers/content/artifacts/engines-extra.test.ts +556 -0
  29. package/src/renderers/content/artifacts/engines.ts +396 -0
  30. package/src/renderers/content/artifacts/local-binary.test.ts +207 -0
  31. package/src/renderers/content/artifacts/local-binary.ts +80 -0
  32. package/src/renderers/content/artifacts/prereqs.ts +128 -0
  33. package/src/renderers/content/artifacts/server.ts +181 -0
  34. package/src/renderers/content/code-panel.ts +161 -0
  35. package/src/renderers/content/image-card.test.ts +170 -0
  36. package/src/renderers/content/image-card.ts +252 -0
  37. package/src/renderers/content/index.ts +79 -0
  38. package/src/renderers/content/json-panel.ts +116 -0
  39. package/src/renderers/content/panels.test.ts +188 -0
  40. package/src/renderers/content/table.test.ts +209 -0
  41. package/src/renderers/content/table.ts +174 -0
  42. package/src/renderers/content/transformer.test.ts +254 -0
  43. package/src/renderers/content/types.ts +20 -0
  44. package/src/renderers/tool/index.ts +113 -0
  45. package/src/renderers/tool/resolver.test.ts +257 -0
  46. package/src/renderers/tool/runtime.test.ts +313 -0
  47. package/src/renderers/tool/runtime.ts +267 -0
  48. package/src/renderers/tool/specs/bash.test.ts +110 -0
  49. package/src/renderers/tool/specs/bash.ts +168 -0
  50. package/src/renderers/tool/specs/codemode.test.ts +212 -0
  51. package/src/renderers/tool/specs/codemode.ts +248 -0
  52. package/src/renderers/tool/specs/edit.test.ts +260 -0
  53. package/src/renderers/tool/specs/edit.ts +213 -0
  54. package/src/renderers/tool/specs/ls.test.ts +173 -0
  55. package/src/renderers/tool/specs/ls.ts +136 -0
  56. package/src/renderers/tool/specs/read.test.ts +340 -0
  57. package/src/renderers/tool/specs/read.ts +296 -0
  58. package/src/renderers/tool/specs/search.test.ts +197 -0
  59. package/src/renderers/tool/specs/search.ts +325 -0
  60. package/src/renderers/tool/specs/write.test.ts +145 -0
  61. package/src/renderers/tool/specs/write.ts +142 -0
  62. package/src/renderers/tool/types.ts +45 -0
  63. package/themes/dracula-soft.json +81 -0
  64. package/themes/one-dark.json +80 -0
  65. package/themes/themes.test.ts +251 -0
@@ -0,0 +1,136 @@
1
+ // ported from pi-pretty-tui/src/features/canvas/{card,emit}.ts — survives because: the boxed card with
2
+ // a plain-markdown Open link is what the host turns into a real clickable target, and imageLineMetadata
3
+ // is the one place a raster artifact's row count is known (T-25's cache meta leaves rows to this step).
4
+ // The emit.ts terminal-image emission is NOT ported: pi-render draws no inline pixels (the artifact card
5
+ // is the transcript output), so those lines had no caller, and their iTerm2 branch carried a cursor-up
6
+ // escape literal — the repo keeps escapes in src/core/paint.ts alone.
7
+
8
+ /**
9
+ * cards.ts — the artifact card and the raster row count.
10
+ *
11
+ * Boundary: the Open link resolves through the ArtifactServer, which registers the path as carded and
12
+ * names the loopback URL; a degraded server is logged once by urlFor and the card falls back to a
13
+ * file:// URL, so a failed bind never reaches the render path as a throw.
14
+ *
15
+ * shape: none — no module-level state; every export is a straight-line function or one box builder.
16
+ */
17
+
18
+ import { pathToFileURL } from "node:url";
19
+ import { calculateImageRows, getImageDimensions, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
20
+ import type { ContentPaint } from "../../../core/types/paint.ts";
21
+ import type { ArtifactServer } from "./server.ts";
22
+
23
+ const OPEN_LABEL = "Open (⌘click)";
24
+ const COPY_LABEL = "Copy path (⌥C)";
25
+ /** The rendered link occupies brackets around the label. */
26
+ const OPEN_WIDTH = visibleWidth(OPEN_LABEL) + 2;
27
+ /** Cards read best narrow; the offered width is clamped into these bounds. */
28
+ const CARD_MIN_WIDTH = 24;
29
+ const CARD_MAX_WIDTH = 64;
30
+ /** pi-tui's default image width; the reference for a raster artifact's transcript row count. */
31
+ const DEFAULT_WIDTH_CELLS = 60;
32
+
33
+ /** The artifact card's own fields; the URL is resolved from the path, never supplied by the caller. */
34
+ export interface ArtifactCardSpec {
35
+ /** The fence form, e.g. "plantuml" — rendered as the card's type label. */
36
+ typeLabel: string;
37
+ /** The artifact's own name where it has one; see artifactTitle. */
38
+ title: string;
39
+ /** Absolute path of the artifact on disk; the Open link points at it. */
40
+ path: string;
41
+ /** Offered transcript width in columns. */
42
+ width: number;
43
+ }
44
+
45
+ /** Markdown would eat these inside a label; escaping keeps the title literal. */
46
+ function escapeMarkdown(text: string): string {
47
+ return text.replace(/([\\`*_[\]#])/g, "\\$1");
48
+ }
49
+
50
+ function clampWidth(text: string, width: number): string {
51
+ return visibleWidth(text) > width ? truncateToWidth(text, width) : text;
52
+ }
53
+
54
+ /**
55
+ * The artifact names itself where it can; the caller passes the fallback (`form`) otherwise.
56
+ *
57
+ * shape: none — dispatch object does not apply: one `||` form test plus ordered title extraction.
58
+ */
59
+ export function artifactTitle(form: string, source: string): string {
60
+ const plantumlStart = source.match(/@start\w+\s+"([^"\n]+)"/);
61
+ if (plantumlStart?.[1]) return plantumlStart[1].trim();
62
+ const plantumlTitle = source.match(/^\s*title\s+(.+)$/m);
63
+ if (plantumlTitle?.[1]) return plantumlTitle[1].trim();
64
+ if (form === "svg" || form === "html") {
65
+ const tag = source.match(/<title[^>]*>([^<]*)<\/title>/i);
66
+ if (tag?.[1]) return tag[1].trim();
67
+ const heading = source.match(/<h1[^>]*>([^<]+)<\/h1>/i);
68
+ if (heading?.[1]) return heading[1].trim();
69
+ }
70
+ return form;
71
+ }
72
+
73
+ /** shape: none — one box; every line's visible width is arithmetically bounded. */
74
+ function renderCard(spec: ArtifactCardSpec & { openUrl: string; pathLabel: string }, paint: ContentPaint): string {
75
+ const width = Math.max(CARD_MIN_WIDTH, Math.min(Math.floor(spec.width), CARD_MAX_WIDTH));
76
+ const inner = width - 2;
77
+ const contentWidth = Math.max(OPEN_WIDTH + 2, inner - 2);
78
+ const label = spec.typeLabel.toLowerCase();
79
+ const prefix = `─ ${label} `;
80
+ const dashes = Math.max(0, inner - visibleWidth(prefix));
81
+ const headerLine = paint.rule(`╭${prefix}${"─".repeat(dashes)}╮`);
82
+
83
+ const title = escapeMarkdown(spec.title);
84
+ // A link whose raw text cannot fit the line must not be emitted: the host wraps by RAW width, would
85
+ // split the token mid-way and print literal brackets with the URL dropped. It then degrades to the
86
+ // plain label, and the path line stays the way in.
87
+ const link = `[${OPEN_LABEL}](${spec.openUrl})`;
88
+ const linkFits = visibleWidth(link) <= Math.max(4, contentWidth - 1);
89
+ const titleBudget = contentWidth - (linkFits ? visibleWidth(link) : visibleWidth(OPEN_LABEL)) - 1;
90
+ const titleFitted = clampWidth(title, Math.max(1, titleBudget));
91
+ const actionText = linkFits ? link : OPEN_LABEL;
92
+ const titlePad = " ".repeat(Math.max(1, contentWidth - visibleWidth(titleFitted) - visibleWidth(actionText)));
93
+ // No escape codes inside the brackets: anything there breaks the link token.
94
+ const titleLine = `${paint.rule("│")} ${paint.quote(titleFitted)}${titlePad}${actionText} ${paint.rule("│")}`;
95
+ const copyPad = " ".repeat(Math.max(0, contentWidth - visibleWidth(COPY_LABEL)));
96
+ const copyLine = `${paint.rule("│")} ${paint.quote(COPY_LABEL)}${copyPad} ${paint.rule("│")}`;
97
+
98
+ // The full path is shown, wrapped — truncation is what made it useless to copy.
99
+ const safePath = spec.pathLabel.replace(/`/g, "'");
100
+ const chunkWidth = Math.max(8, contentWidth);
101
+ const chunks: string[] = [];
102
+ for (let index = 0; index < safePath.length; index += chunkWidth)
103
+ chunks.push(safePath.slice(index, index + chunkWidth));
104
+ if (chunks.length === 0) chunks.push("");
105
+ const pathLines = chunks
106
+ .map((chunk) => {
107
+ const pad = " ".repeat(Math.max(0, contentWidth - visibleWidth(chunk)));
108
+ return `${paint.rule("│")} ${paint.quote(chunk)}${pad} ${paint.rule("│")}`;
109
+ })
110
+ .join(" \n");
111
+ const bottomLine = paint.rule(`╰${"─".repeat(inner)}╯`);
112
+
113
+ // Hard breaks keep the box adjacent; the title line carries the link.
114
+ return `${headerLine} \n${titleLine} \n${copyLine} \n${pathLines} \n${bottomLine}\n`;
115
+ }
116
+
117
+ /**
118
+ * shape: none — one URL resolution plus one box build; the degrade branch lives in urlFor, not here.
119
+ */
120
+ export function renderArtifactCard(server: ArtifactServer, spec: ArtifactCardSpec, paint: ContentPaint): string {
121
+ // urlFor registers the path as carded and names the loopback URL; a degraded server logs once and
122
+ // the card falls back to a file:// link, so the session never sees the failure.
123
+ const openUrl = server.urlFor(spec.path) ?? pathToFileURL(spec.path).href;
124
+ return renderCard({ ...spec, openUrl, pathLabel: spec.path }, paint);
125
+ }
126
+
127
+ /**
128
+ * The transcript row count of a raster artifact: the height the host's image fit would occupy.
129
+ *
130
+ * shape: none — one parse plus one arithmetic; no dispatch.
131
+ */
132
+ export function imageLineMetadata(base64: string): { rows: number } {
133
+ const dimensions = getImageDimensions(base64, "image/png");
134
+ if (!dimensions) return { rows: 1 };
135
+ return { rows: Math.max(1, calculateImageRows(dimensions, DEFAULT_WIDTH_CELLS)) };
136
+ }