@oh-my-pi/pi-tui 16.0.2 → 16.0.4
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 +28 -0
- package/dist/types/components/markdown.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/latex-block.d.ts +7 -0
- package/dist/types/latex-to-unicode.d.ts +33 -0
- package/dist/types/tui.d.ts +5 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +225 -16
- package/src/index.ts +3 -0
- package/src/latex-block.ts +461 -0
- package/src/latex-to-unicode.ts +1994 -0
- package/src/terminal-capabilities.ts +2 -2
- package/src/terminal.ts +17 -2
- package/src/tui.ts +25 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encodeSixel } from "@oh-my-pi/pi-natives";
|
|
2
|
-
import { $env, isBunTestRuntime } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $env, isBunTestRuntime, isTerminalHeadless } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import {
|
|
4
4
|
detectKittyUnicodePlaceholdersSupport,
|
|
5
5
|
getKittyGraphics,
|
|
@@ -97,7 +97,7 @@ export class TerminalInfo {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
sendNotification(message: string | TerminalNotification): void {
|
|
100
|
-
if (isNotificationSuppressed()) return;
|
|
100
|
+
if (isNotificationSuppressed() || isTerminalHeadless()) return;
|
|
101
101
|
process.stdout.write(this.formatNotification(message));
|
|
102
102
|
}
|
|
103
103
|
}
|
package/src/terminal.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import { $env, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { $env, isBunTestRuntime, isTerminalHeadless, logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { setKittyProtocolActive } from "./keys";
|
|
5
5
|
import { StdinBuffer } from "./stdin-buffer";
|
|
6
6
|
import { NotifyProtocol, setCellDimensions, setOsc99Supported, TERMINAL } from "./terminal-capabilities";
|
|
@@ -249,7 +249,7 @@ export function emergencyTerminalRestore(): void {
|
|
|
249
249
|
altScreenActive = false;
|
|
250
250
|
}
|
|
251
251
|
terminal.showCursor();
|
|
252
|
-
} else if (terminalEverStarted) {
|
|
252
|
+
} else if (terminalEverStarted && !isTerminalHeadless()) {
|
|
253
253
|
// Blind restore only if we know a terminal was started but lost track of it
|
|
254
254
|
// This avoids writing escape sequences for non-TUI commands (grep, commit, etc.)
|
|
255
255
|
process.stdout.write(
|
|
@@ -404,6 +404,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
404
404
|
#stdinBuffer?: StdinBuffer;
|
|
405
405
|
#stdinDataHandler?: (data: string) => void;
|
|
406
406
|
#dead = false;
|
|
407
|
+
// Captured at construction and re-read at start(): when true, every real
|
|
408
|
+
// terminal side effect (writes, probes, raw mode, SIGWINCH, timers) is
|
|
409
|
+
// suppressed. Defaults on under `bun test` — see isTerminalHeadless().
|
|
410
|
+
#headless = isTerminalHeadless();
|
|
407
411
|
#writeLogPath = $env.PI_TUI_WRITE_LOG || "";
|
|
408
412
|
#stdoutErrorCleanup?: () => void;
|
|
409
413
|
#stdoutErrorHandler = (err: Error) => {
|
|
@@ -459,6 +463,13 @@ export class ProcessTerminal implements Terminal {
|
|
|
459
463
|
this.#inputHandler = onInput;
|
|
460
464
|
this.#resizeHandler = onResize;
|
|
461
465
|
|
|
466
|
+
// Headless (tests): suppress every real-terminal side effect. Skip raw
|
|
467
|
+
// mode, stdin listeners, capability probes, SIGWINCH, and emergency-restore
|
|
468
|
+
// ownership; #safeWrite is also a no-op, so frame paints and teardown
|
|
469
|
+
// escapes never reach the developer's terminal during `bun test`.
|
|
470
|
+
this.#headless = isTerminalHeadless();
|
|
471
|
+
if (this.#headless) return;
|
|
472
|
+
|
|
462
473
|
// Register for emergency cleanup
|
|
463
474
|
activeTerminal = this;
|
|
464
475
|
terminalEverStarted = true;
|
|
@@ -1134,6 +1145,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1134
1145
|
}
|
|
1135
1146
|
|
|
1136
1147
|
async drainInput(maxMs = 1000, idleMs = 50): Promise<void> {
|
|
1148
|
+
if (this.#headless) return;
|
|
1137
1149
|
if (this.#kittyProtocolActive) {
|
|
1138
1150
|
// Disable Kitty keyboard protocol first so any late key releases
|
|
1139
1151
|
// do not generate new Kitty escape sequences.
|
|
@@ -1176,6 +1188,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1176
1188
|
}
|
|
1177
1189
|
|
|
1178
1190
|
stop(): void {
|
|
1191
|
+
if (this.#headless) return;
|
|
1179
1192
|
// Unregister from emergency cleanup
|
|
1180
1193
|
if (activeTerminal === this) {
|
|
1181
1194
|
activeTerminal = null;
|
|
@@ -1303,6 +1316,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1303
1316
|
}
|
|
1304
1317
|
|
|
1305
1318
|
#safeWrite(data: string): void {
|
|
1319
|
+
if (this.#headless) return;
|
|
1306
1320
|
if (this.#dead) return;
|
|
1307
1321
|
// Skip control sequences when stdout isn't a TTY (piped output, tests, log
|
|
1308
1322
|
// files). They serve no purpose there and would surface as visible noise.
|
|
@@ -1385,6 +1399,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1385
1399
|
}
|
|
1386
1400
|
|
|
1387
1401
|
setProgress(active: boolean): void {
|
|
1402
|
+
if (this.#headless) return;
|
|
1388
1403
|
if (active) {
|
|
1389
1404
|
this.#safeWrite(TERMINAL_PROGRESS_ACTIVE_SEQUENCE);
|
|
1390
1405
|
if (!this.#progressTimer) {
|
package/src/tui.ts
CHANGED
|
@@ -171,6 +171,12 @@ export interface Component {
|
|
|
171
171
|
dispose?(): void;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/** Lets an overlay root delegate keyboard focus to components it owns. */
|
|
175
|
+
export interface OverlayFocusOwner {
|
|
176
|
+
/** Returns true when `component` is a focus target inside this overlay. */
|
|
177
|
+
ownsOverlayFocusTarget(component: Component): boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
174
180
|
/**
|
|
175
181
|
* Component seam for append-only native-scrollback commits. A component that
|
|
176
182
|
* renders a finalized prefix followed by a live/mutating suffix reports the
|
|
@@ -221,6 +227,13 @@ function setNativeScrollbackCommittedRows(component: Component, rows: number): v
|
|
|
221
227
|
(component as Component & Partial<NativeScrollbackCommittedRows>).setNativeScrollbackCommittedRows?.(rows);
|
|
222
228
|
}
|
|
223
229
|
|
|
230
|
+
function isOverlayFocusTarget(owner: Component, component: Component | null): boolean {
|
|
231
|
+
if (component === owner) return true;
|
|
232
|
+
if (!component) return false;
|
|
233
|
+
const candidate = owner as Component & Partial<OverlayFocusOwner>;
|
|
234
|
+
return candidate.ownsOverlayFocusTarget?.(component) === true;
|
|
235
|
+
}
|
|
236
|
+
|
|
224
237
|
function getNativeScrollbackLiveRegionStart(component: Component): number | undefined {
|
|
225
238
|
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackLiveRegionStart?.();
|
|
226
239
|
}
|
|
@@ -1171,6 +1184,14 @@ export class TUI extends Container {
|
|
|
1171
1184
|
}
|
|
1172
1185
|
|
|
1173
1186
|
setFocus(component: Component | null): void {
|
|
1187
|
+
const topVisibleOverlay = this.#getTopmostVisibleOverlay();
|
|
1188
|
+
if (topVisibleOverlay && !isOverlayFocusTarget(topVisibleOverlay.component, component)) {
|
|
1189
|
+
const currentFocus = this.#focusedComponent;
|
|
1190
|
+
component = isOverlayFocusTarget(topVisibleOverlay.component, currentFocus)
|
|
1191
|
+
? currentFocus
|
|
1192
|
+
: topVisibleOverlay.component;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1174
1195
|
const previousFocusedComponent = this.#focusedComponent;
|
|
1175
1196
|
// Clear focused flag on old component
|
|
1176
1197
|
if (isFocusable(previousFocusedComponent)) {
|
|
@@ -1213,8 +1234,8 @@ export class TUI extends Container {
|
|
|
1213
1234
|
const index = this.overlayStack.indexOf(entry);
|
|
1214
1235
|
if (index !== -1) {
|
|
1215
1236
|
this.overlayStack.splice(index, 1);
|
|
1216
|
-
// Restore focus if this overlay had focus
|
|
1217
|
-
if (this.#focusedComponent
|
|
1237
|
+
// Restore focus if this overlay or one of its owned targets had focus
|
|
1238
|
+
if (isOverlayFocusTarget(component, this.#focusedComponent)) {
|
|
1218
1239
|
const topVisible = this.#getTopmostVisibleOverlay();
|
|
1219
1240
|
this.setFocus(topVisible?.component ?? entry.preFocus);
|
|
1220
1241
|
}
|
|
@@ -1230,8 +1251,8 @@ export class TUI extends Container {
|
|
|
1230
1251
|
entry.hidden = hidden;
|
|
1231
1252
|
// Update focus when hiding/showing
|
|
1232
1253
|
if (hidden) {
|
|
1233
|
-
// If this overlay had focus, move focus to next visible or preFocus
|
|
1234
|
-
if (this.#focusedComponent
|
|
1254
|
+
// If this overlay or one of its owned targets had focus, move focus to next visible or preFocus
|
|
1255
|
+
if (isOverlayFocusTarget(component, this.#focusedComponent)) {
|
|
1235
1256
|
const topVisible = this.#getTopmostVisibleOverlay();
|
|
1236
1257
|
this.setFocus(topVisible?.component ?? entry.preFocus);
|
|
1237
1258
|
}
|