@oh-my-pi/pi-tui 18.0.8 → 18.0.9
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 +18 -0
- package/dist/types/components/box.d.ts +2 -0
- package/dist/types/components/cancellable-loader.d.ts +2 -0
- package/dist/types/components/editor.d.ts +4 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/input.d.ts +2 -0
- package/dist/types/components/loader.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +2 -0
- package/dist/types/components/scroll-view.d.ts +2 -0
- package/dist/types/components/select-list.d.ts +2 -0
- package/dist/types/components/settings-list.d.ts +4 -0
- package/dist/types/components/spacer.d.ts +2 -0
- package/dist/types/components/tab-bar.d.ts +2 -0
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/components/truncated-text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/latex-to-unicode.d.ts +4 -4
- package/dist/types/tui.d.ts +23 -0
- package/package.json +3 -3
- package/src/components/box.ts +10 -0
- package/src/components/cancellable-loader.ts +7 -0
- package/src/components/editor.ts +28 -0
- package/src/components/image.ts +11 -0
- package/src/components/input.ts +13 -0
- package/src/components/loader.ts +10 -0
- package/src/components/markdown.ts +39 -10
- package/src/components/scroll-view.ts +12 -0
- package/src/components/select-list.ts +13 -0
- package/src/components/settings-list.ts +19 -0
- package/src/components/spacer.ts +4 -0
- package/src/components/tab-bar.ts +11 -0
- package/src/components/text.ts +11 -0
- package/src/components/truncated-text.ts +15 -1
- package/src/debug-server.ts +422 -0
- package/src/index.ts +2 -0
- package/src/latex-to-unicode.ts +401 -172
- package/src/tui.ts +105 -12
- package/src/utils.ts +10 -1
package/src/tui.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as fs from "node:fs";
|
|
|
16
16
|
import { performance } from "node:perf_hooks";
|
|
17
17
|
import { $flag, getDebugLogPath, logger } from "@oh-my-pi/pi-utils";
|
|
18
18
|
import { DEFAULT_MAX_INLINE_IMAGES, ImageBudget } from "./components/image";
|
|
19
|
+
import { TuiDebugServer } from "./debug-server";
|
|
19
20
|
import { isKeyRelease, matchesKey } from "./keys";
|
|
20
21
|
import { LoopWatchdog } from "./loop-watchdog";
|
|
21
22
|
import { setAltScreenActive, type Terminal } from "./terminal";
|
|
@@ -181,6 +182,15 @@ const DEFAULT_RENDER_SCHEDULER: RenderScheduler = {
|
|
|
181
182
|
* which leading rows survived.
|
|
182
183
|
*/
|
|
183
184
|
export interface Component {
|
|
185
|
+
/** Stable identifier surfaced in the debug tree as kind#id. */
|
|
186
|
+
debugId?: string;
|
|
187
|
+
/** Override for the tree node kind (default: constructor.name). */
|
|
188
|
+
debugKind?: string;
|
|
189
|
+
/** Widget state for the debug `values`/`tree` ops. JSON-serializable. */
|
|
190
|
+
debugState?(): Record<string, unknown>;
|
|
191
|
+
/** Children for the debug tree when not already exposed as a public `children` array. */
|
|
192
|
+
debugChildren?: readonly Component[];
|
|
193
|
+
|
|
184
194
|
/**
|
|
185
195
|
* Render the component to an array of physical rows at the given width.
|
|
186
196
|
* The result is component-owned and `readonly` to the caller; an unchanged
|
|
@@ -699,6 +709,16 @@ export class TUI extends Container {
|
|
|
699
709
|
#previousWidth = 0;
|
|
700
710
|
#previousHeight = 0;
|
|
701
711
|
#focusedComponent: Component | null = null;
|
|
712
|
+
#debugServer: TuiDebugServer | undefined;
|
|
713
|
+
#debugPaint:
|
|
714
|
+
| {
|
|
715
|
+
lines: readonly string[];
|
|
716
|
+
windowTop: number;
|
|
717
|
+
altScreen: boolean;
|
|
718
|
+
cursor?: { x: number; y: number; visible?: boolean };
|
|
719
|
+
}
|
|
720
|
+
| undefined;
|
|
721
|
+
#debugNextWindowTop = 0;
|
|
702
722
|
#inputListeners = new Set<InputListener>();
|
|
703
723
|
#startListeners = new Set<StartListener>();
|
|
704
724
|
|
|
@@ -940,6 +960,27 @@ export class TUI extends Container {
|
|
|
940
960
|
getFocused(): Component | null {
|
|
941
961
|
return this.#focusedComponent;
|
|
942
962
|
}
|
|
963
|
+
/** Last viewport successfully written by the renderer, for debug inspection. */
|
|
964
|
+
getDebugPaint():
|
|
965
|
+
| {
|
|
966
|
+
lines: readonly string[];
|
|
967
|
+
windowTop: number;
|
|
968
|
+
altScreen: boolean;
|
|
969
|
+
cursor?: { x: number; y: number; visible?: boolean };
|
|
970
|
+
}
|
|
971
|
+
| undefined {
|
|
972
|
+
return this.#debugPaint;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Render the current root document at the live terminal width for debug inspection. */
|
|
976
|
+
getDebugDocument(): readonly string[] {
|
|
977
|
+
return this.render(Math.max(1, this.terminal.columns));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** Feed debug and test input through the same pipeline as terminal stdin. */
|
|
981
|
+
injectDebugInput(data: string): void {
|
|
982
|
+
this.#handleInput(data);
|
|
983
|
+
}
|
|
943
984
|
|
|
944
985
|
/**
|
|
945
986
|
* Show an overlay component with configurable positioning and sizing.
|
|
@@ -1042,6 +1083,14 @@ export class TUI extends Container {
|
|
|
1042
1083
|
|
|
1043
1084
|
start(options?: TUIStartOptions): void {
|
|
1044
1085
|
this.#stopped = false;
|
|
1086
|
+
this.#debugPaint = undefined;
|
|
1087
|
+
this.#debugServer?.stop();
|
|
1088
|
+
this.#debugServer = undefined;
|
|
1089
|
+
const debugPath = process.env.OMP_TUI_DEBUG;
|
|
1090
|
+
if (debugPath !== undefined && debugPath.length > 0) {
|
|
1091
|
+
this.#debugServer = new TuiDebugServer(this, debugPath);
|
|
1092
|
+
this.#debugServer.start();
|
|
1093
|
+
}
|
|
1045
1094
|
this.#inputDeferred = options?.deferInput === true;
|
|
1046
1095
|
this.#watchdog.start();
|
|
1047
1096
|
this.#ghosttyInitialImageDelayDone = false;
|
|
@@ -1148,7 +1197,7 @@ export class TUI extends Container {
|
|
|
1148
1197
|
this.terminal.columns,
|
|
1149
1198
|
);
|
|
1150
1199
|
const top = Math.max(0, Math.min(this.#providerViewportTop, this.terminal.rows - staleRows));
|
|
1151
|
-
erase = `\x1b[?25l
|
|
1200
|
+
erase = `\x1b[?25l${this.#eraseBelowRow(top, this.terminal.rows)}`;
|
|
1152
1201
|
} else {
|
|
1153
1202
|
const up = this.#reflowedRowCount(
|
|
1154
1203
|
this.#providerWindow,
|
|
@@ -1156,7 +1205,8 @@ export class TUI extends Container {
|
|
|
1156
1205
|
this.#parkedViewportOffset,
|
|
1157
1206
|
this.terminal.columns,
|
|
1158
1207
|
);
|
|
1159
|
-
|
|
1208
|
+
const eraseBelow = this.#eraseBelowCursorRow(this.terminal.columns, this.terminal.rows);
|
|
1209
|
+
erase = `\x1b[?25l${up > 0 ? `\x1b[${up}A` : ""}${eraseBelow}`;
|
|
1160
1210
|
}
|
|
1161
1211
|
// Both erase paths leave the cursor on the viewport's top row, so the
|
|
1162
1212
|
// parked offset no longer applies; carrying a stale nonzero offset
|
|
@@ -1565,6 +1615,8 @@ export class TUI extends Container {
|
|
|
1565
1615
|
}
|
|
1566
1616
|
|
|
1567
1617
|
stop(): void {
|
|
1618
|
+
this.#debugServer?.stop();
|
|
1619
|
+
this.#debugServer = undefined;
|
|
1568
1620
|
this.#resizeSettleTimer?.cancel();
|
|
1569
1621
|
this.#resizeSettleTimer = undefined;
|
|
1570
1622
|
this.#cancelResizeProbe();
|
|
@@ -2209,6 +2261,7 @@ export class TUI extends Container {
|
|
|
2209
2261
|
#renderProviderFrame(width: number, height: number): void {
|
|
2210
2262
|
const provider = this.#frameProvider;
|
|
2211
2263
|
if (!provider || width <= 0 || height <= 0) return;
|
|
2264
|
+
this.#debugNextWindowTop = 0;
|
|
2212
2265
|
this.#imageBudget.beginPass();
|
|
2213
2266
|
const plan = provider.renderFrame({ columns: width, rows: height });
|
|
2214
2267
|
this.#imageBudget.endPass();
|
|
@@ -2263,6 +2316,47 @@ export class TUI extends Container {
|
|
|
2263
2316
|
this.#forceViewportRepaintOnNextRender = true;
|
|
2264
2317
|
}
|
|
2265
2318
|
|
|
2319
|
+
/**
|
|
2320
|
+
* Erase `row` and everything below it, absolute-addressed, without ever
|
|
2321
|
+
* issuing a full-screen clear. tmux and Windows conhost (#9597) archive a
|
|
2322
|
+
* screen-wide erase into pane history instead of discarding it, so an erase
|
|
2323
|
+
* anchored on the first row would preserve the unfinished frame it exists to
|
|
2324
|
+
* remove (#9780). Below existing history a plain ED0 cannot span the screen;
|
|
2325
|
+
* on the first row, EL2 that row and ED0 from the second down touch the same
|
|
2326
|
+
* cells with no full-screen clear. `row` is clamped because a caller's
|
|
2327
|
+
* viewport top can predate a height shrink.
|
|
2328
|
+
* Every form leaves the cursor on the clamped row at column zero.
|
|
2329
|
+
*/
|
|
2330
|
+
#eraseBelowRow(row: number, height: number): string {
|
|
2331
|
+
const top = Math.max(0, Math.min(row, Math.max(0, height - 1)));
|
|
2332
|
+
if (top > 0) return `\x1b[${top + 1};1H\x1b[J`;
|
|
2333
|
+
if (height <= 1) return `\x1b[1;1H${ERASE_LINE}`;
|
|
2334
|
+
return `\x1b[1;1H${ERASE_LINE}\x1b[2;1H\x1b[J\x1b[1;1H`;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
/**
|
|
2338
|
+
* Erase the cursor's row and everything below it, cursor-relative, without
|
|
2339
|
+
* ever issuing a full-screen clear. Used where the resize path must stay
|
|
2340
|
+
* cursor-relative: the terminal reflowed the normal buffer, so absolute rows
|
|
2341
|
+
* are stale and only the parked cursor still tracks the viewport's logical
|
|
2342
|
+
* line. Every form leaves the cursor on column zero of that row, exactly
|
|
2343
|
+
* where a plain `\r\x1b[J` left it.
|
|
2344
|
+
*
|
|
2345
|
+
* The erase must never run from the first cell, which is what makes tmux and
|
|
2346
|
+
* Windows conhost (#9597) archive the screen instead of discarding it
|
|
2347
|
+
* (#9780), and a clamped CUU can put the cursor on the first row without
|
|
2348
|
+
* naming it. Stepping one column right is enough and needs no row movement,
|
|
2349
|
+
* but at one column CUF cannot leave the first cell, so step one row down
|
|
2350
|
+
* instead: from the first row that can only reach row 1, and DECSC/DECRC
|
|
2351
|
+
* restores the anchor even when CUD clamps at the last row. A one-row screen
|
|
2352
|
+
* has nothing below the cursor, so EL2 alone clears everything.
|
|
2353
|
+
*/
|
|
2354
|
+
#eraseBelowCursorRow(width: number, height: number): string {
|
|
2355
|
+
if (height <= 1) return `\r${ERASE_LINE}`;
|
|
2356
|
+
if (width > 1) return `\r${ERASE_LINE}\x1b[C\x1b[J\r`;
|
|
2357
|
+
return `\r${ERASE_LINE}\x1b7\x1b[B\x1b[J\x1b8`;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2266
2360
|
/**
|
|
2267
2361
|
* Physical write transaction: append an ordinary batch, or bottom-split one
|
|
2268
2362
|
* complete replay into a history remainder and final viewport, then serialize
|
|
@@ -2371,16 +2465,7 @@ export class TUI extends Container {
|
|
|
2371
2465
|
// committed rows and blanks, never an unfinished frame.
|
|
2372
2466
|
const pushed = Math.max(0, startTop + preparedHistory.length + rows - height);
|
|
2373
2467
|
if (pushed > this.#providerViewportTop && this.#providerWindow.length > 0) {
|
|
2374
|
-
|
|
2375
|
-
if (eraseTop > 0) {
|
|
2376
|
-
// Below existing history: ED0 here never spans the whole screen.
|
|
2377
|
-
buffer += `\x1b[${eraseTop + 1};1H\x1b[J`;
|
|
2378
|
-
} else {
|
|
2379
|
-
// Full-screen erase makes tmux preserve the live rows (#9780);
|
|
2380
|
-
// EL2 the first row + ED0 the rest clears the same cells, no full clear.
|
|
2381
|
-
buffer += "\x1b[1;1H\x1b[2K";
|
|
2382
|
-
if (height > 1) buffer += "\x1b[2;1H\x1b[J";
|
|
2383
|
-
}
|
|
2468
|
+
buffer += this.#eraseBelowRow(this.#providerViewportTop, height);
|
|
2384
2469
|
}
|
|
2385
2470
|
buffer += `\x1b[${startTop + 1};1H`;
|
|
2386
2471
|
let screenRow = startTop;
|
|
@@ -2429,6 +2514,12 @@ export class TUI extends Container {
|
|
|
2429
2514
|
}
|
|
2430
2515
|
buffer += this.#paintEndSequence;
|
|
2431
2516
|
this.terminal.write(buffer);
|
|
2517
|
+
this.#debugPaint = {
|
|
2518
|
+
lines: prepared,
|
|
2519
|
+
windowTop: this.#debugNextWindowTop,
|
|
2520
|
+
altScreen: false,
|
|
2521
|
+
...(target === null ? {} : { cursor: { x: target.col, y: target.row, visible: target.visible } }),
|
|
2522
|
+
};
|
|
2432
2523
|
if (target) this.#recordHardwareCursorState(target);
|
|
2433
2524
|
else this.#recordHardwareCursorHidden();
|
|
2434
2525
|
this.#providerWindow = mutablePrepared;
|
|
@@ -2537,6 +2628,7 @@ export class TUI extends Container {
|
|
|
2537
2628
|
const composed = this.render(width);
|
|
2538
2629
|
this.#imageBudget.endPass();
|
|
2539
2630
|
if (this.#maybeDeferGhosttyInitialImagePaint()) return;
|
|
2631
|
+
this.#debugNextWindowTop = Math.max(0, composed.length - height);
|
|
2540
2632
|
const viewport = composed.length > height ? composed.slice(composed.length - height) : Array.from(composed);
|
|
2541
2633
|
this.#emitPlanFrame(width, height, viewport, undefined, undefined);
|
|
2542
2634
|
}
|
|
@@ -2882,6 +2974,7 @@ export class TUI extends Container {
|
|
|
2882
2974
|
buffer += this.#paintEndSequence;
|
|
2883
2975
|
this.terminal.write(buffer);
|
|
2884
2976
|
this.#altPreviousLines = fitted;
|
|
2977
|
+
this.#debugPaint = { lines: fitted, windowTop: 0, altScreen: true };
|
|
2885
2978
|
this.#fullRedrawCount += 1;
|
|
2886
2979
|
}
|
|
2887
2980
|
}
|
package/src/utils.ts
CHANGED
|
@@ -623,7 +623,16 @@ export function applyBackgroundToLine(line: string, width: number, bgFn: (text:
|
|
|
623
623
|
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
624
624
|
|
|
625
625
|
// Apply background to content + padding
|
|
626
|
-
|
|
626
|
+
let withPadding = line + padding(paddingNeeded);
|
|
627
|
+
// Nested background resets (e.g. inline color chips closing with \x1b[49m)
|
|
628
|
+
// would terminate a plain open…close background wrapper early; re-open the
|
|
629
|
+
// line background after each one (same trick as Theme.bgFill).
|
|
630
|
+
if (line.includes("\x1b[49m")) {
|
|
631
|
+
const probe = bgFn("\x01");
|
|
632
|
+
const probeIdx = probe.indexOf("\x01");
|
|
633
|
+
const open = probeIdx > 0 ? probe.slice(0, probeIdx) : "";
|
|
634
|
+
if (open) withPadding = withPadding.replaceAll("\x1b[49m", `\x1b[49m${open}`);
|
|
635
|
+
}
|
|
627
636
|
return bgFn(withPadding);
|
|
628
637
|
}
|
|
629
638
|
|