@linxiraos/pi-tui 1.1.7 → 1.1.8
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 +3 -7
- package/dist/types/components/editor.d.ts +9 -3
- package/dist/types/components/image.d.ts +5 -6
- package/dist/types/keys.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +5 -5
- package/dist/types/stdin-buffer.d.ts +6 -1
- package/dist/types/terminal-capabilities.d.ts +12 -7
- package/dist/types/terminal-multiplexer.d.ts +4 -0
- package/dist/types/terminal.d.ts +46 -21
- package/dist/types/tui.d.ts +18 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +1 -1
- package/src/components/editor.ts +13 -6
- package/src/components/image.ts +31 -24
- package/src/components/markdown.ts +21 -10
- package/src/components/select-list.ts +1 -0
- package/src/components/spacer.ts +1 -1
- package/src/components/text.ts +1 -1
- package/src/deccara.ts +2 -0
- package/src/keys.ts +1 -1
- package/src/kitty-graphics.ts +9 -6
- package/src/latex-block.ts +1 -0
- package/src/stdin-buffer.ts +28 -5
- package/src/terminal-capabilities.ts +22 -21
- package/src/terminal-multiplexer.ts +24 -0
- package/src/terminal.ts +173 -85
- package/src/tui.ts +158 -23
package/src/tui.ts
CHANGED
|
@@ -14,18 +14,19 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import { performance } from "node:perf_hooks";
|
|
17
|
-
import { $flag, getDebugLogPath, logger } from "@linxiraos/pi-utils";
|
|
17
|
+
import { $flag, getDebugLogPath, logger, postmortem } from "@linxiraos/pi-utils";
|
|
18
18
|
import { DEFAULT_MAX_INLINE_IMAGES, ImageBudget } from "./components/image";
|
|
19
19
|
import { TuiDebugServer } from "./debug-server";
|
|
20
20
|
import { isKeyRelease, matchesKey } from "./keys";
|
|
21
21
|
import { LoopWatchdog } from "./loop-watchdog";
|
|
22
|
-
import { setAltScreenActive, type Terminal } from "./terminal";
|
|
22
|
+
import { STDOUT_BACKLOG_CLEAR_BYTES, setAltScreenActive, type Terminal } from "./terminal";
|
|
23
23
|
import {
|
|
24
24
|
encodeKittyDeleteAllImages,
|
|
25
25
|
encodeKittyDeleteImage,
|
|
26
26
|
encodeKittyPlacementLine,
|
|
27
27
|
ImageProtocol,
|
|
28
28
|
isImageProtocolForced,
|
|
29
|
+
isInsideHerdr,
|
|
29
30
|
isInsideTerminalMultiplexer,
|
|
30
31
|
parseKittyDirectPlacementLine,
|
|
31
32
|
setCellDimensions,
|
|
@@ -56,6 +57,7 @@ const SEGMENT_RESET = "\x1b[0m";
|
|
|
56
57
|
const LINE_TERMINATOR = "\x1b[0m\x1b]8;;\x07";
|
|
57
58
|
const ERASE_LINE = "\x1b[2K";
|
|
58
59
|
const ERASE_TO_END_OF_LINE = "\x1b[K";
|
|
60
|
+
const MIN_MAIN_AREA_COLUMNS = 64;
|
|
59
61
|
// Keep the common short-row path out of native width/truncation. Longer rows
|
|
60
62
|
// are fit by visible cells, not source code units, so zero-width-heavy prefixes
|
|
61
63
|
// cannot hide visible suffix text that still belongs in the viewport.
|
|
@@ -474,6 +476,7 @@ export class Container implements Component {
|
|
|
474
476
|
let refs = this.#memoChildLines;
|
|
475
477
|
let unchanged = this.#memoLines !== undefined && this.#memoWidth === width && refs.length === count;
|
|
476
478
|
if (refs.length !== count) {
|
|
479
|
+
// [suppressed] render-frame length preallocation
|
|
477
480
|
refs = new Array(count);
|
|
478
481
|
this.#memoChildLines = refs;
|
|
479
482
|
}
|
|
@@ -753,8 +756,12 @@ export class TUI extends Container {
|
|
|
753
756
|
* render (keeping its forced/clear-scrollback intent) and retry shortly;
|
|
754
757
|
* the eventual frame composes the latest component state, so a slow
|
|
755
758
|
* terminal receives only fresh frames instead of every intermediate one.
|
|
759
|
+
*
|
|
760
|
+
* This is the terminal's healthy-backlog level ({@link STDOUT_BACKLOG_CLEAR_BYTES}):
|
|
761
|
+
* gating here and ending a StdoutStallWatchdog episode there keeps the stall
|
|
762
|
+
* watchdog armed across exactly the range where frames are deferred (#10434).
|
|
756
763
|
*/
|
|
757
|
-
static readonly #MAX_PENDING_OUTPUT_BYTES =
|
|
764
|
+
static readonly #MAX_PENDING_OUTPUT_BYTES = STDOUT_BACKLOG_CLEAR_BYTES;
|
|
758
765
|
/** Retry cadence while the output backlog gate is holding renders back. */
|
|
759
766
|
static readonly #OUTPUT_BACKLOG_RETRY_MS = 10;
|
|
760
767
|
/** Quiet window before restoring the normal buffer after resize. */
|
|
@@ -792,9 +799,20 @@ export class TUI extends Container {
|
|
|
792
799
|
// Consumed by the next frame: a user-driven redraw gesture (resetDisplay,
|
|
793
800
|
// requestRender(true)) that must rewrite the viewport even when the diff
|
|
794
801
|
// believes nothing changed.
|
|
802
|
+
// Sidebar support (restored from db37b05765, adapted to the v18.1.5 engine):
|
|
803
|
+
// when #mainWidthOverride is set and the terminal is wide enough to keep
|
|
804
|
+
// MIN_MAIN_AREA_COLUMNS for the main area, #renderChildrenFrame composes and
|
|
805
|
+
// paints at the overridden width; the freed right margin is painted per frame
|
|
806
|
+
// from #gutterComponent via absolute-positioned writes that never enter the
|
|
807
|
+
// composed frame, committed prefix, or native scrollback.
|
|
808
|
+
#mainWidthOverride: number | null = null;
|
|
809
|
+
#gutterComponent: Component | null = null;
|
|
810
|
+
// Gutter rows painted by the most recent emitting frame.
|
|
811
|
+
#paintedGutterRows: readonly string[] | null = null;
|
|
795
812
|
#forceViewportRepaintOnNextRender = false;
|
|
796
813
|
#hasEverRendered = false;
|
|
797
814
|
#stopped = false;
|
|
815
|
+
#cancelPostmortemRestore?: () => void;
|
|
798
816
|
/** True between a `deferInput` start() and enableInput(). */
|
|
799
817
|
#inputDeferred = false;
|
|
800
818
|
// Always-on event-loop lag probe. The high default threshold keeps it quiet;
|
|
@@ -872,6 +890,73 @@ export class TUI extends Container {
|
|
|
872
890
|
setMaxInlineImages(cap: number): void {
|
|
873
891
|
this.#imageBudget.setCap(cap);
|
|
874
892
|
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Yield `width` columns from the right edge of the terminal as a sidebar
|
|
896
|
+
* margin, repainted per frame from the gutter component (see
|
|
897
|
+
* {@link setGutterComponent}). The main area composes and paints at
|
|
898
|
+
* `terminal.columns - width`, which keeps every committed row — and
|
|
899
|
+
* therefore native scrollback — free of gutter text. `null` restores
|
|
900
|
+
* full-width rendering. Frames where that would drop the main area below
|
|
901
|
+
* {@link MIN_MAIN_AREA_COLUMNS}, or where a fullscreen-capable overlay is
|
|
902
|
+
* visible, ignore the reservation and paint at the physical width.
|
|
903
|
+
*/
|
|
904
|
+
setMainWidth(width: number | null): void {
|
|
905
|
+
const next = width !== null && Number.isInteger(width) && width > 0 ? width : null;
|
|
906
|
+
if (this.#mainWidthOverride === next) return;
|
|
907
|
+
this.#mainWidthOverride = next;
|
|
908
|
+
this.requestRender();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Component rendered into the right-hand margin created by
|
|
913
|
+
* {@link setMainWidth}. Its rows are painted viewport-only via absolute
|
|
914
|
+
* cursor addressing inside each frame's synchronized block; they never enter
|
|
915
|
+
* the composed frame or scrollback. `null` clears the margin.
|
|
916
|
+
*/
|
|
917
|
+
setGutterComponent(component: Component | null): void {
|
|
918
|
+
if (this.#gutterComponent === component) return;
|
|
919
|
+
this.#gutterComponent = component;
|
|
920
|
+
this.#paintedGutterRows = null;
|
|
921
|
+
this.requestRender();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Effective main-area width for this frame, honoring the sidebar guards. */
|
|
925
|
+
#effectiveMainWidth(rawWidth: number): number {
|
|
926
|
+
if (
|
|
927
|
+
this.#mainWidthOverride === null ||
|
|
928
|
+
rawWidth - this.#mainWidthOverride < MIN_MAIN_AREA_COLUMNS ||
|
|
929
|
+
this.overlayStack.length > 0
|
|
930
|
+
) {
|
|
931
|
+
return rawWidth;
|
|
932
|
+
}
|
|
933
|
+
return rawWidth - this.#mainWidthOverride;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Absolute-positioned paint for one frame's gutter column. Writes every
|
|
938
|
+
* viewport row's gutter cells at column `mainWidth + 1`, then restores the
|
|
939
|
+
* hardware cursor. Returns "" when there is no gutter this frame.
|
|
940
|
+
*/
|
|
941
|
+
#gutterPaintSequence(
|
|
942
|
+
rows: readonly string[] | null,
|
|
943
|
+
gutterWidth: number,
|
|
944
|
+
mainWidth: number,
|
|
945
|
+
height: number,
|
|
946
|
+
restoreRow: number,
|
|
947
|
+
): string {
|
|
948
|
+
const col = mainWidth + 1;
|
|
949
|
+
let seq = "";
|
|
950
|
+
for (let row = 0; row < height; row++) {
|
|
951
|
+
if (rows === null || gutterWidth <= 0) return "";
|
|
952
|
+
let text = truncateToWidth(rows[row] ?? "", gutterWidth, Ellipsis.Omit);
|
|
953
|
+
const pad = gutterWidth - visibleWidth(text);
|
|
954
|
+
if (pad > 0) text += " ".repeat(pad);
|
|
955
|
+
seq += `\x1b[${row + 1};${col}H${text}`;
|
|
956
|
+
}
|
|
957
|
+
seq += `\x1b[${restoreRow + 1};1H`;
|
|
958
|
+
return seq;
|
|
959
|
+
}
|
|
875
960
|
/** Return how settled resizes refresh native scrollback. */
|
|
876
961
|
getResizeScrollback(): ResizeScrollbackMode {
|
|
877
962
|
return this.#resizeScrollbackMode;
|
|
@@ -1102,9 +1187,15 @@ export class TUI extends Container {
|
|
|
1102
1187
|
// implementing DECRQM, so retain the statically detected default instead of
|
|
1103
1188
|
// exposing destructive full paints. An explicit user opt-out/force still
|
|
1104
1189
|
// wins, so skip every probe result in that case.
|
|
1105
|
-
this.terminal.onPrivateModeReport?.((mode, supported, confirmed = true) => {
|
|
1190
|
+
this.terminal.onPrivateModeReport?.((mode, supported, confirmed = true, status) => {
|
|
1106
1191
|
if (mode !== 2026 || !confirmed) return;
|
|
1107
1192
|
if (synchronizedOutputUserOverride() !== null) return;
|
|
1193
|
+
// Herdr's Ghostty VTE honors DEC 2026 even when DECRQM is unanswered or
|
|
1194
|
+
// reports unrecognized (status 0). Other confirmed unsupported reports
|
|
1195
|
+
// still disable: status 4 is permanently reset, and a three-argument
|
|
1196
|
+
// callback (`status` omitted) is a definitive unsupported from a
|
|
1197
|
+
// custom Terminal that does not distinguish DECRPM codes.
|
|
1198
|
+
if (!supported && isInsideHerdr() && status === 0) return;
|
|
1108
1199
|
this.#setSynchronizedOutput(supported);
|
|
1109
1200
|
});
|
|
1110
1201
|
this.terminal.start(
|
|
@@ -1126,6 +1217,8 @@ export class TUI extends Container {
|
|
|
1126
1217
|
{ deferInput: this.#inputDeferred },
|
|
1127
1218
|
);
|
|
1128
1219
|
if (this.#stopped) return;
|
|
1220
|
+
this.#cancelPostmortemRestore?.();
|
|
1221
|
+
this.#cancelPostmortemRestore = postmortem.register("tui-restore", () => this.stop());
|
|
1129
1222
|
for (const listener of this.#startListeners) {
|
|
1130
1223
|
try {
|
|
1131
1224
|
listener();
|
|
@@ -1422,11 +1515,13 @@ export class TUI extends Container {
|
|
|
1422
1515
|
/** Paint the full semantic tail on the borrowed resize buffer. */
|
|
1423
1516
|
#renderResizeAltFrame(width: number, height: number): void {
|
|
1424
1517
|
const provider = this.#frameProvider;
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1518
|
+
let rendered: readonly string[];
|
|
1519
|
+
do {
|
|
1520
|
+
this.#imageBudget.beginPass();
|
|
1521
|
+
rendered =
|
|
1522
|
+
provider?.renderResizeFrame?.({ columns: width, rows: height }) ??
|
|
1523
|
+
(provider ? provider.renderFrame({ columns: width, rows: height }).viewport : this.render(width));
|
|
1524
|
+
} while (this.#imageBudget.endPass());
|
|
1430
1525
|
const viewport = rendered.length > height ? rendered.slice(rendered.length - height) : Array.from(rendered);
|
|
1431
1526
|
this.#extractCursorMarkers(viewport);
|
|
1432
1527
|
this.#emitAltFrame(this.#prepareLinesArray(viewport, width), width, height);
|
|
@@ -1600,9 +1695,11 @@ export class TUI extends Container {
|
|
|
1600
1695
|
if (width <= 0 || height <= 0) return;
|
|
1601
1696
|
provider.beginHistoryFlush();
|
|
1602
1697
|
while (true) {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1698
|
+
let plan: TerminalFramePlan;
|
|
1699
|
+
do {
|
|
1700
|
+
this.#imageBudget.beginPass();
|
|
1701
|
+
plan = provider.renderFrame({ columns: width, rows: height });
|
|
1702
|
+
} while (this.#imageBudget.endPass());
|
|
1606
1703
|
if (plan.history === undefined) return;
|
|
1607
1704
|
let viewport = Array.from(plan.viewport);
|
|
1608
1705
|
if (viewport.length > height) viewport = viewport.slice(0, height);
|
|
@@ -1615,6 +1712,8 @@ export class TUI extends Container {
|
|
|
1615
1712
|
}
|
|
1616
1713
|
|
|
1617
1714
|
stop(): void {
|
|
1715
|
+
this.#cancelPostmortemRestore?.();
|
|
1716
|
+
this.#cancelPostmortemRestore = undefined;
|
|
1618
1717
|
this.#debugServer?.stop();
|
|
1619
1718
|
this.#debugServer = undefined;
|
|
1620
1719
|
this.#resizeSettleTimer?.cancel();
|
|
@@ -2262,9 +2361,11 @@ export class TUI extends Container {
|
|
|
2262
2361
|
const provider = this.#frameProvider;
|
|
2263
2362
|
if (!provider || width <= 0 || height <= 0) return;
|
|
2264
2363
|
this.#debugNextWindowTop = 0;
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2364
|
+
let plan: TerminalFramePlan;
|
|
2365
|
+
do {
|
|
2366
|
+
this.#imageBudget.beginPass();
|
|
2367
|
+
plan = provider.renderFrame({ columns: width, rows: height });
|
|
2368
|
+
} while (this.#imageBudget.endPass());
|
|
2268
2369
|
let viewport = Array.from(plan.viewport);
|
|
2269
2370
|
if (viewport.length > height) {
|
|
2270
2371
|
const message = `Frame provider returned ${viewport.length} rows for a ${height}-row viewport`;
|
|
@@ -2413,7 +2514,8 @@ export class TUI extends Container {
|
|
|
2413
2514
|
const geometryStable = this.#hasEverRendered && this.#previousWidth === width && this.#previousHeight === height;
|
|
2414
2515
|
const startTop = destructiveReset ? 0 : Math.min(this.#providerViewportTop, Math.max(0, height - 1));
|
|
2415
2516
|
const newTop = Math.max(0, Math.min(startTop + historyRows.length, height - rows));
|
|
2416
|
-
|
|
2517
|
+
const pendingAltExit = this.#pendingAltExit;
|
|
2518
|
+
let buffer = this.#paintBeginSequence + pendingAltExit;
|
|
2417
2519
|
if (destructiveReset && TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
2418
2520
|
// ED2/ED3 erase text cells but leave Kitty graphics visible. A reset is
|
|
2419
2521
|
// explicitly destructive, so remove every placement—not only the ones
|
|
@@ -2520,6 +2622,10 @@ export class TUI extends Container {
|
|
|
2520
2622
|
altScreen: false,
|
|
2521
2623
|
...(target === null ? {} : { cursor: { x: target.col, y: target.row, visible: target.visible } }),
|
|
2522
2624
|
};
|
|
2625
|
+
if (pendingAltExit) {
|
|
2626
|
+
this.#pendingAltExit = "";
|
|
2627
|
+
setAltScreenActive(false);
|
|
2628
|
+
}
|
|
2523
2629
|
if (target) this.#recordHardwareCursorState(target);
|
|
2524
2630
|
else this.#recordHardwareCursorHidden();
|
|
2525
2631
|
this.#providerWindow = mutablePrepared;
|
|
@@ -2584,8 +2690,15 @@ export class TUI extends Container {
|
|
|
2584
2690
|
const mouseExit = this.#altMouseTrackingActive ? MOUSE_TRACKING_OFF : "";
|
|
2585
2691
|
const enhancementExit = this.#keyboardEnhancementExit();
|
|
2586
2692
|
const exitSequence = `${mouseExit}${enhancementExit}\x1b[?1049l`;
|
|
2587
|
-
|
|
2588
|
-
|
|
2693
|
+
// Session replacement finishes while its fullscreen selector still
|
|
2694
|
+
// covers the old normal buffer. Fuse the restore into the destructive
|
|
2695
|
+
// repaint so no stale frame can become visible between writes.
|
|
2696
|
+
if (this.#clearScrollbackOnNextRender) {
|
|
2697
|
+
this.#pendingAltExit = exitSequence;
|
|
2698
|
+
} else {
|
|
2699
|
+
this.terminal.write(exitSequence);
|
|
2700
|
+
setAltScreenActive(false);
|
|
2701
|
+
}
|
|
2589
2702
|
this.#forgetHardwareCursorState();
|
|
2590
2703
|
this.#altActive = false;
|
|
2591
2704
|
this.#altMouseTrackingActive = false;
|
|
@@ -2615,7 +2728,7 @@ export class TUI extends Container {
|
|
|
2615
2728
|
this.#renderProviderFrame(width, height);
|
|
2616
2729
|
return;
|
|
2617
2730
|
}
|
|
2618
|
-
this.#renderChildrenFrame(width, height);
|
|
2731
|
+
this.#renderChildrenFrame(width, this.#effectiveMainWidth(width), height);
|
|
2619
2732
|
}
|
|
2620
2733
|
|
|
2621
2734
|
/**
|
|
@@ -2623,18 +2736,38 @@ export class TUI extends Container {
|
|
|
2623
2736
|
* compose the root children and paint the bottom `height` rows as the
|
|
2624
2737
|
* mutable viewport. Nothing is ever appended to terminal history.
|
|
2625
2738
|
*/
|
|
2626
|
-
#renderChildrenFrame(width: number, height: number): void {
|
|
2627
|
-
this.#
|
|
2628
|
-
|
|
2629
|
-
|
|
2739
|
+
#renderChildrenFrame(rawWidth: number, width: number, height: number): void {
|
|
2740
|
+
const gutter = this.#gutterComponent && width < rawWidth ? this.#gutterComponent : null;
|
|
2741
|
+
let composed: readonly string[];
|
|
2742
|
+
do {
|
|
2743
|
+
this.#imageBudget.beginPass();
|
|
2744
|
+
composed = this.render(width);
|
|
2745
|
+
} while (this.#imageBudget.endPass());
|
|
2630
2746
|
if (this.#maybeDeferGhosttyInitialImagePaint()) return;
|
|
2631
2747
|
this.#debugNextWindowTop = Math.max(0, composed.length - height);
|
|
2632
2748
|
const viewport = composed.length > height ? composed.slice(composed.length - height) : Array.from(composed);
|
|
2633
2749
|
this.#emitPlanFrame(width, height, viewport, undefined, undefined);
|
|
2750
|
+
if (gutter) {
|
|
2751
|
+
const rows = gutter.render(width);
|
|
2752
|
+
// Paint the freed right margin after the frame block: absolute-positioned
|
|
2753
|
+
// writes at column width+1 never touch the composed frame or scrollback.
|
|
2754
|
+
const seq = this.#gutterPaintSequence(rows, rawWidth - width, width, viewport.length, 0);
|
|
2755
|
+
if (seq) this.terminal.write(seq);
|
|
2756
|
+
this.#paintedGutterRows = rows;
|
|
2757
|
+
} else if (this.#paintedGutterRows !== null) {
|
|
2758
|
+
// Gutter removed: clear the margin column once via absolute EL per row.
|
|
2759
|
+
const col = width + 1;
|
|
2760
|
+
let seq = "";
|
|
2761
|
+
for (let row = 0; row < height; row++) seq += `\x1b[${row + 1};${col}H\x1b[K`;
|
|
2762
|
+
seq += "\x1b[H";
|
|
2763
|
+
this.terminal.write(seq);
|
|
2764
|
+
this.#paintedGutterRows = null;
|
|
2765
|
+
}
|
|
2634
2766
|
}
|
|
2635
2767
|
|
|
2636
2768
|
/** Stateless variant for overlay-composited windows and alt-screen frames. */
|
|
2637
2769
|
#prepareLinesArray(lines: readonly string[], width: number): string[] {
|
|
2770
|
+
// [suppressed] render-frame length preallocation
|
|
2638
2771
|
const prepared: string[] = new Array(lines.length);
|
|
2639
2772
|
for (let i = 0; i < lines.length; i++) {
|
|
2640
2773
|
prepared[i] = this.#prepareLine(lines[i]!, width).line;
|
|
@@ -2923,6 +3056,7 @@ export class TUI extends Container {
|
|
|
2923
3056
|
* blank base — the transcript is never touched while the alt buffer is up.
|
|
2924
3057
|
*/
|
|
2925
3058
|
#renderAltFrame(width: number, height: number): void {
|
|
3059
|
+
// [suppressed] alt-frame length preallocation
|
|
2926
3060
|
const base: string[] = new Array(Math.max(0, height)).fill("");
|
|
2927
3061
|
let lines = this.#compositeOverlaysIntoWindow(base, width, height);
|
|
2928
3062
|
this.#extractCursorMarkers(lines);
|
|
@@ -2936,6 +3070,7 @@ export class TUI extends Container {
|
|
|
2936
3070
|
* native-scrollback byte. The hardware cursor stays hidden here.
|
|
2937
3071
|
*/
|
|
2938
3072
|
#emitAltFrame(lines: string[], width: number, height: number): void {
|
|
3073
|
+
// [suppressed] alt-frame length preallocation
|
|
2939
3074
|
const fitted: string[] = new Array(height);
|
|
2940
3075
|
for (let r = 0; r < height; r++) fitted[r] = lines[r] ?? "";
|
|
2941
3076
|
// Flush queued image-data transmits (`a=t`, no visible output) before the
|