@oh-my-pi/pi-utils 18.6.1 → 18.6.3

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.6.3] - 2026-10-06
6
+
7
+ ### Breaking Changes
8
+
9
+ - Replaced the `cursorPosition` option of `TerminalQueryResponder` with `hostCursorHandshake`, which leaves only the PTY host's own session-start cursor query unanswered instead of every cursor-position query
10
+
5
11
  ## [18.5.1] - 2026-10-03
6
12
 
7
13
  ### Added
@@ -3,13 +3,14 @@
3
3
  */
4
4
  export interface TerminalQueryResponderOptions {
5
5
  /**
6
- * Reply to `CSI 6 n` (report cursor position) with the home position. Set to
7
- * `false` when the PTY host answers cursor reports itself: the only `CSI 6 n`
8
- * left on the stream is then the host's own session-start handshake, whose
9
- * reply would be delivered to the supervised program as unsolicited input
10
- * instead of being consumed by the host. Defaults to `true`.
6
+ * The PTY host opens the session with its own cursor-position query
7
+ * (`CSI 6 n`), which the PTY layer has already answered — ConPTY's
8
+ * `PSEUDOCONSOLE_INHERIT_CURSOR` handshake. Leave that first cursor query
9
+ * unanswered: the host consumes exactly one report, so a second one would
10
+ * reach the program as input it never asked for. Every later cursor query
11
+ * is answered. Defaults to `false`.
11
12
  */
12
- cursorPosition?: boolean;
13
+ hostCursorHandshake?: boolean;
13
14
  }
14
15
  /**
15
16
  * Answers terminal capability queries emitted by programs on a headless PTY.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.6.1",
3
+ "version": "18.6.3",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,7 +57,7 @@
57
57
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
58
58
  },
59
59
  "dependencies": {
60
- "@oh-my-pi/pi-natives": "18.6.1"
60
+ "@oh-my-pi/pi-natives": "18.6.3"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
@@ -3,13 +3,14 @@
3
3
  */
4
4
  export interface TerminalQueryResponderOptions {
5
5
  /**
6
- * Reply to `CSI 6 n` (report cursor position) with the home position. Set to
7
- * `false` when the PTY host answers cursor reports itself: the only `CSI 6 n`
8
- * left on the stream is then the host's own session-start handshake, whose
9
- * reply would be delivered to the supervised program as unsolicited input
10
- * instead of being consumed by the host. Defaults to `true`.
6
+ * The PTY host opens the session with its own cursor-position query
7
+ * (`CSI 6 n`), which the PTY layer has already answered — ConPTY's
8
+ * `PSEUDOCONSOLE_INHERIT_CURSOR` handshake. Leave that first cursor query
9
+ * unanswered: the host consumes exactly one report, so a second one would
10
+ * reach the program as input it never asked for. Every later cursor query
11
+ * is answered. Defaults to `false`.
11
12
  */
12
- cursorPosition?: boolean;
13
+ hostCursorHandshake?: boolean;
13
14
  }
14
15
 
15
16
  /**
@@ -31,10 +32,11 @@ export interface TerminalQueryResponderOptions {
31
32
  export class TerminalQueryResponder {
32
33
  /** Trailing bytes that may be the start of an unfinished query escape. */
33
34
  #residual = "";
34
- readonly #cursorPosition: boolean;
35
+ /** Whether the next cursor query is the host's already-answered session-start handshake. */
36
+ #hostCursorHandshakePending: boolean;
35
37
 
36
38
  constructor(options: TerminalQueryResponderOptions = {}) {
37
- this.#cursorPosition = options.cursorPosition ?? true;
39
+ this.#hostCursorHandshakePending = options.hostCursorHandshake ?? false;
38
40
  }
39
41
 
40
42
  /**
@@ -48,7 +50,12 @@ export class TerminalQueryResponder {
48
50
  QUERY.lastIndex = 0;
49
51
  for (let match = QUERY.exec(buffer); match !== null; match = QUERY.exec(buffer)) {
50
52
  lastEnd = match.index + match[0].length;
51
- replies += replyFor(match, this.#cursorPosition);
53
+ const reply = replyFor(match);
54
+ if (reply === CURSOR_POSITION_REPORT && this.#hostCursorHandshakePending) {
55
+ this.#hostCursorHandshakePending = false;
56
+ continue;
57
+ }
58
+ replies += reply;
52
59
  }
53
60
  // Keep only a short unmatched trailing escape: a query split across
54
61
  // chunks completes on the next feed, while a long tail is ordinary output
@@ -60,6 +67,9 @@ export class TerminalQueryResponder {
60
67
  }
61
68
  }
62
69
 
70
+ /** Cursor position report for the home position: there is no screen to locate the cursor on. */
71
+ const CURSOR_POSITION_REPORT = "\x1b[1;1R";
72
+
63
73
  /** Longest query escape we answer, bounding the cross-chunk residual. */
64
74
  const MAX_PARTIAL_QUERY = 32;
65
75
 
@@ -70,7 +80,7 @@ const MAX_PARTIAL_QUERY = 32;
70
80
  const QUERY = /\x1b\[([?>=]?)([0-9;]*)([nc])|\x1b\](10|11);\?(\x07|\x1b\\)/gu;
71
81
 
72
82
  /** Reply a real xterm-class terminal would send for one matched query. */
73
- function replyFor(match: RegExpExecArray, cursorPosition: boolean): string {
83
+ function replyFor(match: RegExpExecArray): string {
74
84
  const final = match[3];
75
85
  if (final !== undefined) {
76
86
  const intermediate = match[1];
@@ -82,7 +92,7 @@ function replyFor(match: RegExpExecArray, cursorPosition: boolean): string {
82
92
  }
83
93
  if (intermediate !== "") return ""; // private DSR forms (DECXCPR, appearance) stay unanswered
84
94
  const selector = params.split(";", 1)[0];
85
- if (selector === "6") return cursorPosition ? "\x1b[1;1R" : ""; // cursor position: home, there is no screen
95
+ if (selector === "6") return CURSOR_POSITION_REPORT;
86
96
  if (selector === "5") return "\x1b[0n"; // device status: OK
87
97
  return "";
88
98
  }