@oh-my-pi/pi-utils 18.1.4 → 18.1.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.5] - 2026-09-03
6
+
7
+ ### Added
8
+
9
+ - Added `TerminalQueryResponder` to `@oh-my-pi/pi-utils/vterm`, enabling headless PTY consumers to answer common terminal queries for cursor position, device status and attributes, and foreground/background colors without maintaining a screen buffer.
10
+
5
11
  ## [18.1.3] - 2026-09-02
6
12
 
7
13
  ### Fixed
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Wrap a promise with a timeout and optional abort signal.
3
- * Rejects with the given message if the timeout fires first.
4
- * Cleans up all listeners on settlement.
3
+ * Rejects with the given error or a new error containing the given message if
4
+ * the timeout fires first. Cleans up all listeners on settlement.
5
5
  */
6
- export declare function withTimeout<T>(promise: Promise<T>, ms: number, message: string, signal?: AbortSignal): Promise<T>;
6
+ export declare function withTimeout<T>(promise: Promise<T>, ms: number, timeout: string | Error, signal?: AbortSignal): Promise<T>;
7
7
  /**
8
8
  * Coalesces rapid-fire writes into one deferred batch. `push` queues a value
9
9
  * and returns a promise for the batch flush; the first push of a batch arms a
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Answers terminal capability queries emitted by programs on a headless PTY.
3
+ *
4
+ * A PTY that advertises `TERM=xterm-256color` but has no terminal behind it
5
+ * leaves capability probes unanswered: a program writes a query escape to
6
+ * stdout and blocks on stdin until the reply arrives (or its timeout expires,
7
+ * seconds later). This scanner watches raw PTY output for the standard queries
8
+ * and returns the bytes a real xterm-class terminal would send back, so the
9
+ * caller can write them into the PTY. It keeps no screen state — cursor
10
+ * position reports are answered with the home position — which makes it cheap
11
+ * enough to run on every byte of a long-lived supervised process. Use the full
12
+ * {@link Terminal} when the caller also renders the output.
13
+ *
14
+ * Queries can straddle chunk boundaries, so an unfinished trailing escape is
15
+ * carried into the next {@link feed}.
16
+ */
17
+ export declare class TerminalQueryResponder {
18
+ #private;
19
+ /**
20
+ * Feed one raw PTY output chunk. Returns the reply bytes to write back into
21
+ * the PTY, or an empty string when the chunk held no answerable query.
22
+ */
23
+ feed(chunk: string): string;
24
+ }
@@ -1,5 +1,6 @@
1
1
  /** Behavior-compatible reimplementation of @xterm/headless's used surface. */
2
2
  export * from "./vterm/buffer.js";
3
+ export * from "./vterm/query-responder.js";
3
4
  export * from "./vterm/terminal.js";
4
5
  import { Terminal } from "./vterm/terminal.js";
5
6
  declare const vterm: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "18.1.4",
4
+ "version": "18.1.5",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -31,7 +31,7 @@
31
31
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "18.1.4"
34
+ "@oh-my-pi/pi-natives": "18.1.5"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/bun": "^1.3.14"
package/src/async.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * Wrap a promise with a timeout and optional abort signal.
3
- * Rejects with the given message if the timeout fires first.
4
- * Cleans up all listeners on settlement.
3
+ * Rejects with the given error or a new error containing the given message if
4
+ * the timeout fires first. Cleans up all listeners on settlement.
5
5
  */
6
- export function withTimeout<T>(promise: Promise<T>, ms: number, message: string, signal?: AbortSignal): Promise<T> {
6
+ export function withTimeout<T>(
7
+ promise: Promise<T>,
8
+ ms: number,
9
+ timeout: string | Error,
10
+ signal?: AbortSignal,
11
+ ): Promise<T> {
7
12
  if (signal?.aborted) {
8
13
  const reason = signal.reason instanceof Error ? signal.reason : new Error("Aborted");
9
14
  return Promise.reject(reason);
@@ -15,7 +20,7 @@ export function withTimeout<T>(promise: Promise<T>, ms: number, message: string,
15
20
  if (settled) return;
16
21
  settled = true;
17
22
  if (signal) signal.removeEventListener("abort", onAbort);
18
- reject(new Error(message));
23
+ reject(typeof timeout === "string" ? new Error(timeout) : timeout);
19
24
  }, ms);
20
25
 
21
26
  const onAbort = () => {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Answers terminal capability queries emitted by programs on a headless PTY.
3
+ *
4
+ * A PTY that advertises `TERM=xterm-256color` but has no terminal behind it
5
+ * leaves capability probes unanswered: a program writes a query escape to
6
+ * stdout and blocks on stdin until the reply arrives (or its timeout expires,
7
+ * seconds later). This scanner watches raw PTY output for the standard queries
8
+ * and returns the bytes a real xterm-class terminal would send back, so the
9
+ * caller can write them into the PTY. It keeps no screen state — cursor
10
+ * position reports are answered with the home position — which makes it cheap
11
+ * enough to run on every byte of a long-lived supervised process. Use the full
12
+ * {@link Terminal} when the caller also renders the output.
13
+ *
14
+ * Queries can straddle chunk boundaries, so an unfinished trailing escape is
15
+ * carried into the next {@link feed}.
16
+ */
17
+ export class TerminalQueryResponder {
18
+ /** Trailing bytes that may be the start of an unfinished query escape. */
19
+ #residual = "";
20
+
21
+ /**
22
+ * Feed one raw PTY output chunk. Returns the reply bytes to write back into
23
+ * the PTY, or an empty string when the chunk held no answerable query.
24
+ */
25
+ feed(chunk: string): string {
26
+ const buffer = this.#residual + chunk;
27
+ let replies = "";
28
+ let lastEnd = 0;
29
+ QUERY.lastIndex = 0;
30
+ for (let match = QUERY.exec(buffer); match !== null; match = QUERY.exec(buffer)) {
31
+ lastEnd = match.index + match[0].length;
32
+ replies += replyFor(match);
33
+ }
34
+ // Keep only a short unmatched trailing escape: a query split across
35
+ // chunks completes on the next feed, while a long tail is ordinary output
36
+ // that can never become a query.
37
+ const tailEscape = buffer.lastIndexOf("\x1b");
38
+ this.#residual =
39
+ tailEscape >= lastEnd && buffer.length - tailEscape <= MAX_PARTIAL_QUERY ? buffer.slice(tailEscape) : "";
40
+ return replies;
41
+ }
42
+ }
43
+
44
+ /** Longest query escape we answer, bounding the cross-chunk residual. */
45
+ const MAX_PARTIAL_QUERY = 32;
46
+
47
+ /**
48
+ * CSI DSR/DA queries (final byte `n` or `c`) and OSC 10/11 color queries. Only
49
+ * forms with canned answers are matched; everything else stays plain output.
50
+ */
51
+ const QUERY = /\x1b\[([?>=]?)([0-9;]*)([nc])|\x1b\](10|11);\?(\x07|\x1b\\)/gu;
52
+
53
+ /** Reply a real xterm-class terminal would send for one matched query. */
54
+ function replyFor(match: RegExpExecArray): string {
55
+ const final = match[3];
56
+ if (final !== undefined) {
57
+ const intermediate = match[1];
58
+ const params = match[2] ?? "";
59
+ if (final === "c") {
60
+ if (intermediate === ">") return "\x1b[>0;10;1c"; // secondary DA: VT100-class, firmware 10
61
+ if (intermediate === "" || intermediate === "0") return "\x1b[?1;2c"; // primary DA: VT100 with AVO
62
+ return ""; // tertiary (`=`) DA has no widely expected reply
63
+ }
64
+ if (intermediate !== "") return ""; // private DSR forms (DECXCPR, appearance) stay unanswered
65
+ const selector = params.split(";", 1)[0];
66
+ if (selector === "6") return "\x1b[1;1R"; // cursor position: home, there is no screen
67
+ if (selector === "5") return "\x1b[0n"; // device status: OK
68
+ return "";
69
+ }
70
+ // OSC color queries: neutral colors, terminated the way the request was.
71
+ const selector = match[4];
72
+ const terminator = match[5] ?? "\x07";
73
+ if (selector === "10") return `\x1b]10;rgb:ffff/ffff/ffff${terminator}`; // foreground
74
+ if (selector === "11") return `\x1b]11;rgb:0000/0000/0000${terminator}`; // background
75
+ return "";
76
+ }
package/src/vterm.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Behavior-compatible reimplementation of @xterm/headless's used surface. */
2
2
  export * from "./vterm/buffer";
3
+ export * from "./vterm/query-responder";
3
4
  export * from "./vterm/terminal";
4
5
 
5
6
  import { Terminal } from "./vterm/terminal";