@archastro/astroshot-review 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/bin/astroshot-review.mjs +17 -0
  4. package/dist/cli.d.ts +12 -0
  5. package/dist/cli.js +240 -0
  6. package/dist/data/friction.d.ts +22 -0
  7. package/dist/data/friction.js +278 -0
  8. package/dist/data/hash-cache.d.ts +17 -0
  9. package/dist/data/hash-cache.js +51 -0
  10. package/dist/data/index-cache.d.ts +22 -0
  11. package/dist/data/index-cache.js +64 -0
  12. package/dist/data/manifest.d.ts +41 -0
  13. package/dist/data/manifest.js +105 -0
  14. package/dist/data/model.d.ts +96 -0
  15. package/dist/data/model.js +1 -0
  16. package/dist/data/paths.d.ts +27 -0
  17. package/dist/data/paths.js +98 -0
  18. package/dist/data/review-store.d.ts +67 -0
  19. package/dist/data/review-store.js +237 -0
  20. package/dist/data/scan.d.ts +32 -0
  21. package/dist/data/scan.js +227 -0
  22. package/dist/data/store.d.ts +92 -0
  23. package/dist/data/store.js +408 -0
  24. package/dist/data/watcher.d.ts +37 -0
  25. package/dist/data/watcher.js +126 -0
  26. package/dist/images/halfblocks.d.ts +10 -0
  27. package/dist/images/halfblocks.js +21 -0
  28. package/dist/images/png.d.ts +14 -0
  29. package/dist/images/png.js +45 -0
  30. package/dist/images/scale.d.ts +31 -0
  31. package/dist/images/scale.js +102 -0
  32. package/dist/images/service.d.ts +54 -0
  33. package/dist/images/service.js +163 -0
  34. package/dist/images/worker.d.ts +21 -0
  35. package/dist/images/worker.js +30 -0
  36. package/dist/index.d.ts +12 -0
  37. package/dist/index.js +9 -0
  38. package/dist/terminal/graphics-stdout.d.ts +9 -0
  39. package/dist/terminal/graphics-stdout.js +36 -0
  40. package/dist/terminal/herdr.d.ts +61 -0
  41. package/dist/terminal/herdr.js +327 -0
  42. package/dist/terminal/image-layer.d.ts +122 -0
  43. package/dist/terminal/image-layer.js +471 -0
  44. package/dist/terminal/kitty.d.ts +74 -0
  45. package/dist/terminal/kitty.js +112 -0
  46. package/dist/terminal/probe.d.ts +49 -0
  47. package/dist/terminal/probe.js +206 -0
  48. package/dist/ui/app.d.ts +6 -0
  49. package/dist/ui/app.js +695 -0
  50. package/dist/ui/chrome.d.ts +53 -0
  51. package/dist/ui/chrome.js +69 -0
  52. package/dist/ui/context.d.ts +16 -0
  53. package/dist/ui/context.js +8 -0
  54. package/dist/ui/detail.d.ts +33 -0
  55. package/dist/ui/detail.js +39 -0
  56. package/dist/ui/friction.d.ts +42 -0
  57. package/dist/ui/friction.js +84 -0
  58. package/dist/ui/help.d.ts +4 -0
  59. package/dist/ui/help.js +61 -0
  60. package/dist/ui/hooks.d.ts +9 -0
  61. package/dist/ui/hooks.js +32 -0
  62. package/dist/ui/movie-player.d.ts +29 -0
  63. package/dist/ui/movie-player.js +119 -0
  64. package/dist/ui/picture.d.ts +19 -0
  65. package/dist/ui/picture.js +100 -0
  66. package/dist/ui/selectors.d.ts +26 -0
  67. package/dist/ui/selectors.js +69 -0
  68. package/dist/ui/settings.d.ts +5 -0
  69. package/dist/ui/settings.js +17 -0
  70. package/dist/ui/stream.d.ts +38 -0
  71. package/dist/ui/stream.js +118 -0
  72. package/dist/ui/system.d.ts +4 -0
  73. package/dist/ui/system.js +34 -0
  74. package/dist/ui/takeover.d.ts +26 -0
  75. package/dist/ui/takeover.js +29 -0
  76. package/dist/ui/text-input.d.ts +8 -0
  77. package/dist/ui/text-input.js +74 -0
  78. package/dist/ui/theme.d.ts +21 -0
  79. package/dist/ui/theme.js +63 -0
  80. package/dist/video/ffmpeg.d.ts +54 -0
  81. package/dist/video/ffmpeg.js +206 -0
  82. package/package.json +71 -0
@@ -0,0 +1,49 @@
1
+ export type GraphicsProtocol = "kitty" | "herdr" | "halfblocks" | "none";
2
+ export interface TerminalCapabilities {
3
+ graphics: GraphicsProtocol;
4
+ /** Whether the terminal accepted a file-path transmission (t=f). */
5
+ fileMedium: boolean;
6
+ /** Pixel size of one cell. Falls back to a 1:2 guess when unreported. */
7
+ cellWidth: number;
8
+ cellHeight: number;
9
+ cellSource: "query" | "env" | "fallback";
10
+ /** Why graphics are off, for the Settings pane. */
11
+ reason?: string;
12
+ insideTmux: boolean;
13
+ insideSsh: boolean;
14
+ insideMosh: boolean;
15
+ insideHerdr: boolean;
16
+ /** A multiplexer/transport is intercepting output, so pixel graphics can't reach the screen. */
17
+ intercepted: string | null;
18
+ }
19
+ export declare const FALLBACK_CELL: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /** Whether the terminal can show 24-bit color, which half-block art needs. */
24
+ export declare function supportsTrueColor(env: NodeJS.ProcessEnv): boolean;
25
+ export interface ProbeStreams {
26
+ stdin: NodeJS.ReadStream;
27
+ stdout: NodeJS.WriteStream;
28
+ }
29
+ export interface ProbeOptions {
30
+ timeoutMs?: number;
31
+ env?: NodeJS.ProcessEnv;
32
+ probeFileMedium?: boolean;
33
+ }
34
+ export declare function parseCellSizeEnv(value: string | undefined): {
35
+ width: number;
36
+ height: number;
37
+ } | null;
38
+ export interface ProbeReport {
39
+ kittyOk: boolean;
40
+ fileOk: boolean;
41
+ cellWidth?: number;
42
+ cellHeight?: number;
43
+ windowWidth?: number;
44
+ windowHeight?: number;
45
+ sawDeviceAttributes: boolean;
46
+ }
47
+ /** Interpret the raw bytes a terminal wrote back during the probe. */
48
+ export declare function parseProbeResponse(text: string): ProbeReport;
49
+ export declare function probeTerminal(streams: ProbeStreams, options?: ProbeOptions): Promise<TerminalCapabilities>;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Detect what the host terminal can draw before Ink takes over stdin/stdout.
3
+ *
4
+ * Sends the kitty graphics query, the cell-size and window-size reports, and a
5
+ * primary device attributes request whose reply always arrives last, so a
6
+ * terminal that ignores the graphics query still terminates the probe.
7
+ */
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import { encodeFileQuery, encodeQuery } from "./kitty.js";
12
+ export const FALLBACK_CELL = { width: 10, height: 20 };
13
+ /** Whether the terminal can show 24-bit color, which half-block art needs. */
14
+ export function supportsTrueColor(env) {
15
+ const colorterm = (env.COLORTERM ?? "").toLowerCase();
16
+ if (colorterm.includes("truecolor") || colorterm.includes("24bit"))
17
+ return true;
18
+ const term = (env.TERM ?? "").toLowerCase();
19
+ return term.includes("256color") || term.includes("kitty") || term.includes("direct");
20
+ }
21
+ const QUERY_ID = 31;
22
+ const FILE_QUERY_ID = 32;
23
+ // Smallest valid PNG: 1×1 transparent pixel.
24
+ const PROBE_PNG = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64");
25
+ export function parseCellSizeEnv(value) {
26
+ if (!value)
27
+ return null;
28
+ const match = /^(\d+)\s*[x×]\s*(\d+)$/i.exec(value.trim());
29
+ if (!match)
30
+ return null;
31
+ const width = Number(match[1]);
32
+ const height = Number(match[2]);
33
+ if (width <= 0 || height <= 0)
34
+ return null;
35
+ return { width, height };
36
+ }
37
+ /** Interpret the raw bytes a terminal wrote back during the probe. */
38
+ export function parseProbeResponse(text) {
39
+ const report = {
40
+ kittyOk: false,
41
+ fileOk: false,
42
+ sawDeviceAttributes: /\x1b\[\?[\d;]*c/.test(text),
43
+ };
44
+ const graphics = /\x1b_G([^\x1b]*)\x1b\\/g;
45
+ for (const match of text.matchAll(graphics)) {
46
+ const body = match[1] ?? "";
47
+ if (body.includes(`i=${QUERY_ID}`) && /;OK/.test(body))
48
+ report.kittyOk = true;
49
+ if (body.includes(`i=${FILE_QUERY_ID}`) && /;OK/.test(body))
50
+ report.fileOk = true;
51
+ }
52
+ const cell = /\x1b\[6;(\d+);(\d+)t/.exec(text);
53
+ if (cell) {
54
+ report.cellHeight = Number(cell[1]);
55
+ report.cellWidth = Number(cell[2]);
56
+ }
57
+ const window = /\x1b\[4;(\d+);(\d+)t/.exec(text);
58
+ if (window) {
59
+ report.windowHeight = Number(window[1]);
60
+ report.windowWidth = Number(window[2]);
61
+ }
62
+ return report;
63
+ }
64
+ function readResponse(stdin, timeoutMs) {
65
+ return new Promise((resolve) => {
66
+ let buffer = "";
67
+ let settled = false;
68
+ const wasRaw = stdin.isRaw;
69
+ const finish = () => {
70
+ if (settled)
71
+ return;
72
+ settled = true;
73
+ stdin.off("data", onData);
74
+ clearTimeout(timer);
75
+ if (stdin.isTTY && !wasRaw)
76
+ stdin.setRawMode(false);
77
+ stdin.pause();
78
+ resolve(buffer);
79
+ };
80
+ const onData = (chunk) => {
81
+ buffer += chunk.toString();
82
+ // Primary DA reply terminates the probe.
83
+ if (/\x1b\[\?[\d;]*c/.test(buffer))
84
+ finish();
85
+ };
86
+ const timer = setTimeout(finish, timeoutMs);
87
+ if (stdin.isTTY && !wasRaw)
88
+ stdin.setRawMode(true);
89
+ stdin.on("data", onData);
90
+ stdin.resume();
91
+ });
92
+ }
93
+ export async function probeTerminal(streams, options = {}) {
94
+ const env = options.env ?? process.env;
95
+ const timeoutMs = options.timeoutMs ?? 600;
96
+ const insideTmux = Boolean(env.TMUX);
97
+ const insideSsh = Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
98
+ const insideMosh = Boolean(env.MOSH_SERVER_NETWORK_TMOUT || env.MOSH_CONNECTION || env.MOSH_KEY);
99
+ const insideHerdr = Boolean(env.HERDR_PANE_ID || env.HERDR_SOCKET_PATH);
100
+ const envCell = parseCellSizeEnv(env.ASTROSHOT_REVIEW_CELL_PX);
101
+ const forced = env.ASTROSHOT_REVIEW_GRAPHICS;
102
+ const trueColor = supportsTrueColor(env);
103
+ // A multiplexer or transport that emulates the terminal itself never forwards
104
+ // another program's pixel escapes: mosh has no image support at all, tmux
105
+ // needs explicit passthrough, and herdr renders only through its own socket.
106
+ const intercepted = insideMosh
107
+ ? "mosh (no image protocol)"
108
+ : insideHerdr
109
+ ? "herdr"
110
+ : insideTmux
111
+ ? "tmux"
112
+ : null;
113
+ const base = {
114
+ graphics: "none",
115
+ fileMedium: false,
116
+ cellWidth: envCell?.width ?? FALLBACK_CELL.width,
117
+ cellHeight: envCell?.height ?? FALLBACK_CELL.height,
118
+ cellSource: envCell ? "env" : "fallback",
119
+ insideTmux,
120
+ insideSsh,
121
+ insideMosh,
122
+ insideHerdr,
123
+ intercepted,
124
+ };
125
+ // Colored half-block text renders as ordinary output, so it survives mosh,
126
+ // tmux, and herdr where pixel protocols do not.
127
+ const halfblocks = (reason) => trueColor
128
+ ? { ...base, graphics: "halfblocks", reason }
129
+ : { ...base, reason: `${reason}; and no truecolor for half-block art` };
130
+ if (forced === "none") {
131
+ return { ...base, reason: "ASTROSHOT_REVIEW_GRAPHICS=none" };
132
+ }
133
+ if (forced === "kitty") {
134
+ return { ...base, graphics: "kitty", fileMedium: env.ASTROSHOT_REVIEW_FILE_MEDIUM === "1" };
135
+ }
136
+ if (forced === "halfblocks" || forced === "half-blocks" || forced === "text") {
137
+ return halfblocks("ASTROSHOT_REVIEW_GRAPHICS=halfblocks");
138
+ }
139
+ if (!streams.stdout.isTTY || !streams.stdin.isTTY) {
140
+ return { ...base, reason: "stdin/stdout is not a terminal" };
141
+ }
142
+ // Inside an interceptor, don't even probe for kitty (the emulator may answer
143
+ // OK yet never paint); go straight to half-block text.
144
+ if (intercepted) {
145
+ return halfblocks(`${intercepted} intercepts pixel graphics; using half-block text`);
146
+ }
147
+ let probeFile = null;
148
+ const wantFileProbe = options.probeFileMedium ?? !insideSsh;
149
+ if (wantFileProbe) {
150
+ try {
151
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "astroshot-review-probe-"));
152
+ probeFile = path.join(directory, "probe.png");
153
+ fs.writeFileSync(probeFile, PROBE_PNG);
154
+ }
155
+ catch {
156
+ probeFile = null;
157
+ }
158
+ }
159
+ const query = encodeQuery(QUERY_ID) +
160
+ (probeFile ? encodeFileQuery(FILE_QUERY_ID, probeFile) : "") +
161
+ "\x1b[16t" +
162
+ "\x1b[14t" +
163
+ "\x1b[c";
164
+ const pending = readResponse(streams.stdin, timeoutMs);
165
+ streams.stdout.write(query);
166
+ const response = await pending;
167
+ if (probeFile) {
168
+ fs.rmSync(path.dirname(probeFile), { recursive: true, force: true });
169
+ }
170
+ const report = parseProbeResponse(response);
171
+ let cellWidth = base.cellWidth;
172
+ let cellHeight = base.cellHeight;
173
+ let cellSource = base.cellSource;
174
+ if (!envCell) {
175
+ if (report.cellWidth && report.cellHeight) {
176
+ cellWidth = report.cellWidth;
177
+ cellHeight = report.cellHeight;
178
+ cellSource = "query";
179
+ }
180
+ else if (report.windowWidth && report.windowHeight) {
181
+ const columns = streams.stdout.columns || 80;
182
+ const rows = streams.stdout.rows || 24;
183
+ cellWidth = Math.max(1, Math.floor(report.windowWidth / columns));
184
+ cellHeight = Math.max(1, Math.floor(report.windowHeight / rows));
185
+ cellSource = "query";
186
+ }
187
+ }
188
+ if (!report.kittyOk) {
189
+ const why = report.sawDeviceAttributes
190
+ ? "no kitty graphics protocol (try Ghostty, kitty, or WezTerm for pixel-perfect images)"
191
+ : "terminal did not answer the capability probe";
192
+ return { ...halfblocks(why), cellWidth, cellHeight, cellSource };
193
+ }
194
+ return {
195
+ graphics: "kitty",
196
+ fileMedium: report.fileOk,
197
+ cellWidth,
198
+ cellHeight,
199
+ cellSource,
200
+ insideTmux,
201
+ insideSsh,
202
+ insideMosh,
203
+ insideHerdr,
204
+ intercepted,
205
+ };
206
+ }
@@ -0,0 +1,6 @@
1
+ import type { FrictionLog } from "../data/model.js";
2
+ export interface AppProps {
3
+ onQuit?: () => void;
4
+ }
5
+ export declare function App({ onQuit }: AppProps): import("react").JSX.Element;
6
+ export declare function frictionLogDirectory(log: FrictionLog): string;