@oh-my-pi/pi-tui 16.0.10 → 16.1.0
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 +23 -0
- package/dist/types/components/box.d.ts +14 -1
- package/dist/types/utils.d.ts +3 -7
- package/package.json +3 -3
- package/src/components/box.ts +54 -8
- package/src/components/markdown.ts +61 -9
- package/src/tui.ts +19 -12
- package/src/utils.ts +20 -30
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.0] - 2026-06-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Box` now accepts an optional `border` (box-drawing glyphs + colorizer) and exposes `setBorder()`, drawing a colored outline around its padded/background content. The border is automatically dropped at widths too narrow to frame so a bordered box never overflows its given width.
|
|
10
|
+
|
|
11
|
+
## [16.0.11] - 2026-06-19
|
|
12
|
+
|
|
13
|
+
### Breaking Changes
|
|
14
|
+
|
|
15
|
+
- Removed `getIndentation` and `getIndentationNoescape` exported utilities
|
|
16
|
+
- Tab-related operations no longer respect per-file or globally configured indentation settings
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Standardized tab expansion to use a fixed display width instead of configurable settings
|
|
21
|
+
- Removed support for custom tab width configuration in text rendering and input handling
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Corrected logic in string truncation to prevent improper truncation of short strings
|
|
26
|
+
- Fixed a one-frame transcript flash during a non-multiplexer resize drag: while the drag borrowed the alternate screen and painted only the viewport, any ordinary (non-forced) render from a still-animating block — a tool spinner tick, a streamed token, a cursor blink — fell through to the deferred geometry-rebuild full paint, which left the alternate screen to repaint the whole transcript on the normal screen for a single frame before the next SIGWINCH re-entered the viewport fast path, so a live tool block flashed in and vanished. Ordinary renders mid-drag now stay on the viewport fast path; only forced renders (tool finalization, reset, image reconciliation) still preempt it.
|
|
27
|
+
|
|
5
28
|
## [16.0.10] - 2026-06-18
|
|
6
29
|
|
|
7
30
|
### Fixed
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { Component } from "../tui";
|
|
2
|
+
/** Box-drawing glyphs plus an optional colorizer for an outline drawn around a {@link Box}. */
|
|
3
|
+
export interface BoxBorder {
|
|
4
|
+
chars: {
|
|
5
|
+
topLeft: string;
|
|
6
|
+
topRight: string;
|
|
7
|
+
bottomLeft: string;
|
|
8
|
+
bottomRight: string;
|
|
9
|
+
horizontal: string;
|
|
10
|
+
vertical: string;
|
|
11
|
+
};
|
|
12
|
+
color?: (text: string) => string;
|
|
13
|
+
}
|
|
2
14
|
/**
|
|
3
15
|
* Box component - a container that applies padding and background to all children
|
|
4
16
|
*/
|
|
@@ -6,13 +18,14 @@ export declare class Box implements Component {
|
|
|
6
18
|
#private;
|
|
7
19
|
children: Component[];
|
|
8
20
|
setIgnoreTight(ignore: boolean): this;
|
|
9
|
-
constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
|
|
21
|
+
constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string, border?: BoxBorder);
|
|
10
22
|
addChild(component: Component): void;
|
|
11
23
|
removeChild(component: Component): void;
|
|
12
24
|
clear(): void;
|
|
13
25
|
setPaddingX(paddingX: number): void;
|
|
14
26
|
setPaddingY(paddingY: number): void;
|
|
15
27
|
setBgFn(bgFn?: (text: string) => string): void;
|
|
28
|
+
setBorder(border?: BoxBorder): void;
|
|
16
29
|
invalidate(): void;
|
|
17
30
|
render(width: number): readonly string[];
|
|
18
31
|
}
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ellipsis, type ExtractSegmentsResult, type SliceResult } from "@oh-my-pi/pi-natives";
|
|
2
2
|
export { Ellipsis } from "@oh-my-pi/pi-natives";
|
|
3
|
-
export {
|
|
3
|
+
export { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
|
|
4
4
|
export type TextSizingScale = 1 | 2 | 3;
|
|
5
5
|
export type TextSizingVerticalAlign = "top" | "bottom" | "center";
|
|
6
6
|
export type TextSizingHorizontalAlign = "left" | "right" | "center";
|
|
@@ -17,14 +17,10 @@ export interface TextSizingOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function encodeTextSized(text: string, options?: TextSizingOptions): string;
|
|
19
19
|
export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult;
|
|
20
|
-
export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null, pad?: boolean | null): string;
|
|
20
|
+
export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null | "", pad?: boolean | null): string;
|
|
21
21
|
export declare function wrapTextWithAnsi(text: string, width: number): string[];
|
|
22
22
|
export declare function extractSegments(line: string, beforeEnd: number, afterStart: number, afterLen: number, strictAfter: boolean): ExtractSegmentsResult;
|
|
23
|
-
|
|
24
|
-
* Tab width in columns for `file`, using `process.cwd()` as the project root for relative paths.
|
|
25
|
-
*/
|
|
26
|
-
export declare function getIndentationNoescape(file?: string): number;
|
|
27
|
-
export declare function replaceTabs(text: string, file?: string): string;
|
|
23
|
+
export declare function replaceTabs(text: string): string;
|
|
28
24
|
/**
|
|
29
25
|
* Returns a string of n spaces. Uses a pre-allocated buffer for efficiency.
|
|
30
26
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "16.0
|
|
4
|
+
"version": "16.1.0",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "16.0
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.0
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.1.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.1.0",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.5"
|
|
44
44
|
},
|
package/src/components/box.ts
CHANGED
|
@@ -4,10 +4,24 @@ import { applyBackgroundToLine, getPaddingX, padding, visibleWidth } from "../ut
|
|
|
4
4
|
type Cache = {
|
|
5
5
|
width: number;
|
|
6
6
|
bgSample: string | undefined;
|
|
7
|
+
borderSample: string | undefined;
|
|
7
8
|
childLines: (readonly string[])[];
|
|
8
9
|
result: string[];
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
/** Box-drawing glyphs plus an optional colorizer for an outline drawn around a {@link Box}. */
|
|
13
|
+
export interface BoxBorder {
|
|
14
|
+
chars: {
|
|
15
|
+
topLeft: string;
|
|
16
|
+
topRight: string;
|
|
17
|
+
bottomLeft: string;
|
|
18
|
+
bottomRight: string;
|
|
19
|
+
horizontal: string;
|
|
20
|
+
vertical: string;
|
|
21
|
+
};
|
|
22
|
+
color?: (text: string) => string;
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
/**
|
|
12
26
|
* Box component - a container that applies padding and background to all children
|
|
13
27
|
*/
|
|
@@ -16,6 +30,7 @@ export class Box implements Component {
|
|
|
16
30
|
#paddingX: number;
|
|
17
31
|
#paddingY: number;
|
|
18
32
|
#bgFn?: (text: string) => string;
|
|
33
|
+
#border?: BoxBorder;
|
|
19
34
|
|
|
20
35
|
#ignoreTight = false;
|
|
21
36
|
|
|
@@ -28,10 +43,11 @@ export class Box implements Component {
|
|
|
28
43
|
// Cache for rendered output
|
|
29
44
|
#cached?: Cache;
|
|
30
45
|
|
|
31
|
-
constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {
|
|
46
|
+
constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string, border?: BoxBorder) {
|
|
32
47
|
this.#paddingX = paddingX;
|
|
33
48
|
this.#paddingY = paddingY;
|
|
34
49
|
this.#bgFn = bgFn;
|
|
50
|
+
this.#border = border;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
addChild(component: Component): void {
|
|
@@ -72,6 +88,11 @@ export class Box implements Component {
|
|
|
72
88
|
// Don't invalidate here - we'll detect bgFn changes by sampling output
|
|
73
89
|
}
|
|
74
90
|
|
|
91
|
+
setBorder(border?: BoxBorder): void {
|
|
92
|
+
this.#border = border;
|
|
93
|
+
this.#invalidateCache();
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
#invalidateCache(): void {
|
|
76
97
|
this.#cached = undefined;
|
|
77
98
|
}
|
|
@@ -87,10 +108,18 @@ export class Box implements Component {
|
|
|
87
108
|
const children = this.children;
|
|
88
109
|
const count = children.length;
|
|
89
110
|
const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
//
|
|
111
|
+
// A border eats one column on each side; skip it unless the interior can still
|
|
112
|
+
// hold the horizontal padding plus at least one content column, so a bordered
|
|
113
|
+
// Box never overflows the width it was given.
|
|
114
|
+
const border = this.#border && width - 2 >= paddingX * 2 + 1 ? this.#border : undefined;
|
|
115
|
+
const innerWidth = border ? width - 2 : width;
|
|
116
|
+
const contentWidth = Math.max(1, innerWidth - paddingX * 2);
|
|
117
|
+
// bgFn / border output can change without the function reference changing
|
|
118
|
+
// (theme mutation); sample both so a silent palette swap still misses the cache.
|
|
93
119
|
const bgSample = this.#bgFn ? this.#bgFn("test") : undefined;
|
|
120
|
+
const borderSample = border
|
|
121
|
+
? `${border.color ? border.color("|") : "|"}${border.chars.topLeft}${border.chars.vertical}`
|
|
122
|
+
: undefined;
|
|
94
123
|
|
|
95
124
|
// Render every child every frame (renders may carry side effects); the
|
|
96
125
|
// memo only skips re-deriving the padded/background rows. Per the
|
|
@@ -101,6 +130,7 @@ export class Box implements Component {
|
|
|
101
130
|
cached !== undefined &&
|
|
102
131
|
cached.width === width &&
|
|
103
132
|
cached.bgSample === bgSample &&
|
|
133
|
+
cached.borderSample === borderSample &&
|
|
104
134
|
cached.childLines.length === count;
|
|
105
135
|
const childLines: (readonly string[])[] = new Array(count);
|
|
106
136
|
let contentRows = 0;
|
|
@@ -115,23 +145,39 @@ export class Box implements Component {
|
|
|
115
145
|
const result: string[] = [];
|
|
116
146
|
if (contentRows > 0) {
|
|
117
147
|
const leftPad = padding(paddingX);
|
|
148
|
+
const interior: string[] = [];
|
|
118
149
|
// Top padding
|
|
119
150
|
for (let i = 0; i < this.#paddingY; i++) {
|
|
120
|
-
|
|
151
|
+
interior.push(this.#applyBg("", innerWidth));
|
|
121
152
|
}
|
|
122
153
|
// Content
|
|
123
154
|
for (const lines of childLines) {
|
|
124
155
|
for (const line of lines) {
|
|
125
|
-
|
|
156
|
+
interior.push(this.#applyBg(leftPad + line, innerWidth));
|
|
126
157
|
}
|
|
127
158
|
}
|
|
128
159
|
// Bottom padding
|
|
129
160
|
for (let i = 0; i < this.#paddingY; i++) {
|
|
130
|
-
|
|
161
|
+
interior.push(this.#applyBg("", innerWidth));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (border) {
|
|
165
|
+
const paint = border.color ?? (s => s);
|
|
166
|
+
const rule = border.chars.horizontal.repeat(Math.max(0, innerWidth));
|
|
167
|
+
const side = paint(border.chars.vertical);
|
|
168
|
+
result.push(paint(border.chars.topLeft + rule + border.chars.topRight));
|
|
169
|
+
for (const row of interior) {
|
|
170
|
+
result.push(side + row + side);
|
|
171
|
+
}
|
|
172
|
+
result.push(paint(border.chars.bottomLeft + rule + border.chars.bottomRight));
|
|
173
|
+
} else {
|
|
174
|
+
for (const row of interior) {
|
|
175
|
+
result.push(row);
|
|
176
|
+
}
|
|
131
177
|
}
|
|
132
178
|
}
|
|
133
179
|
|
|
134
|
-
this.#cached = { width, bgSample, childLines, result };
|
|
180
|
+
this.#cached = { width, bgSample, borderSample, childLines, result };
|
|
135
181
|
return result;
|
|
136
182
|
}
|
|
137
183
|
|
|
@@ -222,6 +222,56 @@ markdownParser.setOptions({
|
|
|
222
222
|
// never math. Inline extensions run before marked's escape tokenizer, so
|
|
223
223
|
// `\(…\)` becomes math while a genuinely escaped `\$` is left to `escape` and
|
|
224
224
|
// renders as a literal dollar.
|
|
225
|
+
const CUSTOM_HR_START_REGEX = /(?:^|\n) {0,3}([-*_─━═=–—])[ \t]*(?:\1[ \t]*){2,}(?:\n+|$)/;
|
|
226
|
+
const CUSTOM_HR_TOKENIZER_REGEX = /^ {0,3}([-*_─━═=–—])[ \t]*(?:\1[ \t]*){2,}(?:\n+|$)/;
|
|
227
|
+
|
|
228
|
+
function getHrChar(char: string, hrChar: string): string {
|
|
229
|
+
const isAscii = hrChar === "-";
|
|
230
|
+
switch (char) {
|
|
231
|
+
case "=":
|
|
232
|
+
return "=";
|
|
233
|
+
case "═":
|
|
234
|
+
return isAscii ? "=" : "═";
|
|
235
|
+
case "━":
|
|
236
|
+
return isAscii ? "-" : "━";
|
|
237
|
+
case "─":
|
|
238
|
+
return isAscii ? "-" : "─";
|
|
239
|
+
case "–":
|
|
240
|
+
return isAscii ? "-" : "–";
|
|
241
|
+
case "—":
|
|
242
|
+
return isAscii ? "-" : "—";
|
|
243
|
+
default:
|
|
244
|
+
return hrChar;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const customHrExtension: TokenizerAndRendererExtension = {
|
|
249
|
+
name: "customHr",
|
|
250
|
+
level: "block",
|
|
251
|
+
start(src) {
|
|
252
|
+
const match = CUSTOM_HR_START_REGEX.exec(src);
|
|
253
|
+
if (!match) return undefined;
|
|
254
|
+
let idx = match.index;
|
|
255
|
+
if (src[idx] === "\n") {
|
|
256
|
+
idx += 1;
|
|
257
|
+
}
|
|
258
|
+
return idx;
|
|
259
|
+
},
|
|
260
|
+
tokenizer(src) {
|
|
261
|
+
const match = CUSTOM_HR_TOKENIZER_REGEX.exec(src);
|
|
262
|
+
if (match) {
|
|
263
|
+
return {
|
|
264
|
+
type: "hr",
|
|
265
|
+
raw: match[0],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
},
|
|
270
|
+
renderer() {
|
|
271
|
+
return "";
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
225
275
|
const mathExtension: TokenizerAndRendererExtension = {
|
|
226
276
|
name: "math",
|
|
227
277
|
level: "inline",
|
|
@@ -332,7 +382,7 @@ const mathEnvBlockExtension: TokenizerAndRendererExtension = {
|
|
|
332
382
|
return (token as { text?: string }).text ?? "";
|
|
333
383
|
},
|
|
334
384
|
};
|
|
335
|
-
markdownParser.use({ extensions: [mathBlockExtension, mathEnvBlockExtension, mathExtension] });
|
|
385
|
+
markdownParser.use({ extensions: [customHrExtension, mathBlockExtension, mathEnvBlockExtension, mathExtension] });
|
|
336
386
|
|
|
337
387
|
// ---------------------------------------------------------------------------
|
|
338
388
|
// Module-level LRU render cache
|
|
@@ -360,16 +410,14 @@ export function clearRenderCache(): void {
|
|
|
360
410
|
}
|
|
361
411
|
|
|
362
412
|
// Stable numeric IDs for structural theme/style objects (no ID field on type).
|
|
363
|
-
//
|
|
364
|
-
const
|
|
365
|
-
type WithObjectId = object & { [kObjectId]?: number };
|
|
413
|
+
// WeakMap-keyed so the ID matches strict object identity and doesn't get copied by spread/cloning.
|
|
414
|
+
const themeObjectIds = new WeakMap<object, number>();
|
|
366
415
|
let nextObjectId = 0;
|
|
367
416
|
function objectId(o: object): number {
|
|
368
|
-
|
|
369
|
-
let id = tagged[kObjectId];
|
|
417
|
+
let id = themeObjectIds.get(o);
|
|
370
418
|
if (id === undefined) {
|
|
371
419
|
id = nextObjectId++;
|
|
372
|
-
|
|
420
|
+
themeObjectIds.set(o, id);
|
|
373
421
|
}
|
|
374
422
|
return id;
|
|
375
423
|
}
|
|
@@ -1150,12 +1198,16 @@ export class Markdown implements Component {
|
|
|
1150
1198
|
break;
|
|
1151
1199
|
}
|
|
1152
1200
|
|
|
1153
|
-
case "hr":
|
|
1154
|
-
|
|
1201
|
+
case "hr": {
|
|
1202
|
+
const raw = "raw" in token && typeof token.raw === "string" ? token.raw.trim() : "";
|
|
1203
|
+
const char = raw[0] || "";
|
|
1204
|
+
const fillChar = getHrChar(char, this.#theme.symbols.hrChar);
|
|
1205
|
+
lines.push(this.#theme.hr(fillChar.repeat(Math.min(width, 80))));
|
|
1155
1206
|
if (nextTokenType && nextTokenType !== "space") {
|
|
1156
1207
|
lines.push(""); // Add spacing after horizontal rules (unless space token follows)
|
|
1157
1208
|
}
|
|
1158
1209
|
break;
|
|
1210
|
+
}
|
|
1159
1211
|
|
|
1160
1212
|
case "html":
|
|
1161
1213
|
if ("raw" in token && typeof token.raw === "string") {
|
package/src/tui.ts
CHANGED
|
@@ -1005,11 +1005,6 @@ export class TUI extends Container {
|
|
|
1005
1005
|
// fast path (`#renderResizeViewport`) instead of an authoritative full
|
|
1006
1006
|
// paint, and no commit/window/diff state is advanced.
|
|
1007
1007
|
#resizeViewportActive = false;
|
|
1008
|
-
// Set only by the resize callback's cheap-paint request. A concurrent
|
|
1009
|
-
// caller-forced render (tool finalization, reset, image reconciliation) must
|
|
1010
|
-
// not be downgraded to the throwaway viewport path just because a resize
|
|
1011
|
-
// settle window is active.
|
|
1012
|
-
#resizeViewportPaintPending = false;
|
|
1013
1008
|
// Quiet-window timer that ends the drag: its callback clears the flag and
|
|
1014
1009
|
// drives the one authoritative full paint. Reset on every resize event so it
|
|
1015
1010
|
// only fires once the drag stops. Cancelled on stop().
|
|
@@ -1792,7 +1787,6 @@ export class TUI extends Container {
|
|
|
1792
1787
|
// Any non-component-scoped request makes the pending frame a full one.
|
|
1793
1788
|
this.#pendingRenderComponentsOnly = false;
|
|
1794
1789
|
if (force) {
|
|
1795
|
-
this.#resizeViewportPaintPending = false;
|
|
1796
1790
|
// Forced repaints landing inside the multiplexer resize debounce
|
|
1797
1791
|
// (e.g. `#finishSixelProbe`, image-budget eviction, a programmatic
|
|
1798
1792
|
// `requestRender(true)`) would paint into a still-reflowing pane
|
|
@@ -2471,16 +2465,30 @@ export class TUI extends Container {
|
|
|
2471
2465
|
// Strictly state-isolated: it never consumes #resizeEventPending nor
|
|
2472
2466
|
// advances any commit/window/diff field, so the authoritative full paint
|
|
2473
2467
|
// the settle timer queues reconciles as if these throwaway frames never
|
|
2474
|
-
// ran.
|
|
2475
|
-
//
|
|
2476
|
-
//
|
|
2468
|
+
// ran. Two render sources reach here mid-drag and BOTH must stay on this
|
|
2469
|
+
// path:
|
|
2470
|
+
// - the resize callback's own cheap paint after each SIGWINCH;
|
|
2471
|
+
// - an ordinary (non-forced) render from a live block that keeps
|
|
2472
|
+
// animating through the drag — a spinner tick, a streamed token, a
|
|
2473
|
+
// cursor blink — firing requestRender(false)/requestComponentRender.
|
|
2474
|
+
// #resizeEventPending is still set (the fast path never consumed it),
|
|
2475
|
+
// so without this branch the ordinary render falls through to the
|
|
2476
|
+
// geometry-rebuild full paint below, which LEAVES the borrowed
|
|
2477
|
+
// alternate screen to repaint the whole transcript on the normal
|
|
2478
|
+
// screen — then the next SIGWINCH re-enters the alt screen and paints
|
|
2479
|
+
// only the tail, so the block flashes in for one frame and vanishes.
|
|
2480
|
+
// A forced render (tool finalization, reset, image reconciliation) must
|
|
2481
|
+
// still preempt: it set #forceViewportRepaintOnNextRender via
|
|
2482
|
+
// #prepareForcedRender and owns the next authoritative paint, so it falls
|
|
2483
|
+
// through. A visible overlay composites over the transcript and needs the
|
|
2484
|
+
// whole window, so it also falls through (overlay resizes are not on the
|
|
2485
|
+
// drag-cost hot path).
|
|
2477
2486
|
if (
|
|
2478
|
-
this.#resizeViewportPaintPending &&
|
|
2479
2487
|
this.#resizeViewportActive &&
|
|
2488
|
+
!this.#forceViewportRepaintOnNextRender &&
|
|
2480
2489
|
this.#hasEverRendered &&
|
|
2481
2490
|
this.#getTopmostVisibleOverlay() === undefined
|
|
2482
2491
|
) {
|
|
2483
|
-
this.#resizeViewportPaintPending = false;
|
|
2484
2492
|
this.#componentRenderTargets.clear();
|
|
2485
2493
|
this.#renderResizeViewport(width, height);
|
|
2486
2494
|
return;
|
|
@@ -3145,7 +3153,6 @@ export class TUI extends Container {
|
|
|
3145
3153
|
|
|
3146
3154
|
#requestResizeViewportPaint(): void {
|
|
3147
3155
|
if (this.#stopped) return;
|
|
3148
|
-
this.#resizeViewportPaintPending = true;
|
|
3149
3156
|
this.#renderRequested = false;
|
|
3150
3157
|
this.#lastRenderAt = this.#renderScheduler.now();
|
|
3151
3158
|
this.#doRender();
|
package/src/utils.ts
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
wrapTextWithAnsi as nativeWrapTextWithAnsi,
|
|
8
8
|
type SliceResult,
|
|
9
9
|
} from "@oh-my-pi/pi-natives";
|
|
10
|
-
import {
|
|
10
|
+
import { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
|
|
11
11
|
|
|
12
12
|
export { Ellipsis } from "@oh-my-pi/pi-natives";
|
|
13
13
|
|
|
14
|
-
export {
|
|
14
|
+
export { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
|
|
15
15
|
|
|
16
16
|
export type TextSizingScale = 1 | 2 | 3;
|
|
17
17
|
export type TextSizingVerticalAlign = "top" | "bottom" | "center";
|
|
@@ -74,35 +74,32 @@ export function encodeTextSized(text: string, options: TextSizingOptions = {}):
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult {
|
|
77
|
-
return nativeSliceWithWidth(line, startCol, length, strict ?? null,
|
|
77
|
+
return nativeSliceWithWidth(line, startCol, length, strict ?? null, DEFAULT_TAB_WIDTH);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function truncateToWidth(
|
|
81
81
|
text: string,
|
|
82
82
|
maxWidth: number,
|
|
83
|
-
ellipsisKind?: Ellipsis | null,
|
|
83
|
+
ellipsisKind?: Ellipsis | null | "",
|
|
84
84
|
pad?: boolean | null,
|
|
85
85
|
): string {
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let resolvedEllipsis: Ellipsis | null | undefined | string = ellipsisKind;
|
|
92
|
-
if (typeof resolvedEllipsis === "string") {
|
|
93
|
-
resolvedEllipsis = resolvedEllipsis === "" ? Ellipsis.Omit : Ellipsis.Unicode;
|
|
86
|
+
maxWidth = Math.max(0, maxWidth | 0);
|
|
87
|
+
// Fast path: every UTF-16 unit is at most 3 cells wide, so a string whose
|
|
88
|
+
// `length * 3` already fits within `safeWidth` cannot need truncation.
|
|
89
|
+
if (!pad && text.length * 3 <= maxWidth) {
|
|
90
|
+
return text;
|
|
94
91
|
}
|
|
95
92
|
return nativeTruncateToWidth(
|
|
96
93
|
text,
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
maxWidth,
|
|
95
|
+
(ellipsisKind === "" ? Ellipsis.Omit : ellipsisKind) ?? Ellipsis.Unicode,
|
|
99
96
|
pad ?? false,
|
|
100
|
-
|
|
97
|
+
DEFAULT_TAB_WIDTH,
|
|
101
98
|
);
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
export function wrapTextWithAnsi(text: string, width: number): string[] {
|
|
105
|
-
return nativeWrapTextWithAnsi(text, width,
|
|
102
|
+
return nativeWrapTextWithAnsi(text, width, DEFAULT_TAB_WIDTH);
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
export function extractSegments(
|
|
@@ -112,24 +109,17 @@ export function extractSegments(
|
|
|
112
109
|
afterLen: number,
|
|
113
110
|
strictAfter: boolean,
|
|
114
111
|
): ExtractSegmentsResult {
|
|
115
|
-
return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter,
|
|
112
|
+
return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, DEFAULT_TAB_WIDTH);
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
// Pre-allocated space buffer for padding
|
|
119
116
|
const SPACE_BUFFER = " ".repeat(512);
|
|
120
117
|
|
|
121
|
-
/**
|
|
122
|
-
* Tab width in columns for `file`, using `process.cwd()` as the project root for relative paths.
|
|
123
|
-
*/
|
|
124
|
-
export function getIndentationNoescape(file?: string): number {
|
|
125
|
-
return getIndentation(file, process.cwd());
|
|
126
|
-
}
|
|
127
|
-
|
|
128
118
|
/*
|
|
129
|
-
* Replace tabs with
|
|
119
|
+
* Replace tabs with the fixed display tab width for consistent rendering.
|
|
130
120
|
*/
|
|
131
|
-
export function replaceTabs(text: string
|
|
132
|
-
return text.replaceAll("\t", " ".repeat(
|
|
121
|
+
export function replaceTabs(text: string): string {
|
|
122
|
+
return text.replaceAll("\t", " ".repeat(DEFAULT_TAB_WIDTH));
|
|
133
123
|
}
|
|
134
124
|
|
|
135
125
|
/**
|
|
@@ -186,7 +176,7 @@ export function visibleWidth(str: string): number {
|
|
|
186
176
|
for (let tabIndex = str.indexOf(TAB); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
|
|
187
177
|
tabCount++;
|
|
188
178
|
}
|
|
189
|
-
if (tabCount > 0) width += tabCount *
|
|
179
|
+
if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
|
|
190
180
|
return width;
|
|
191
181
|
}
|
|
192
182
|
|
|
@@ -203,7 +193,7 @@ export function visibleWidth(str: string): number {
|
|
|
203
193
|
}
|
|
204
194
|
}
|
|
205
195
|
if (i === str.length) {
|
|
206
|
-
return tabCount === 0 ? str.length : str.length + tabCount * (
|
|
196
|
+
return tabCount === 0 ? str.length : str.length + tabCount * (DEFAULT_TAB_WIDTH - 1);
|
|
207
197
|
}
|
|
208
198
|
|
|
209
199
|
if (tabCount === 0) {
|
|
@@ -224,7 +214,7 @@ export function visibleWidth(str: string): number {
|
|
|
224
214
|
// the native scanner that traps under Bun 1.3.x GC/N-API load). It strips
|
|
225
215
|
// CSI/OSC to zero cells and shares the native engine's UAX#11 width tables.
|
|
226
216
|
let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
|
|
227
|
-
if (tabCount > 0) width += tabCount *
|
|
217
|
+
if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
|
|
228
218
|
|
|
229
219
|
// OSC 66: add back each stripped span as `scale * (explicit w ?? payload
|
|
230
220
|
// width)`. Matched rather than replaced to avoid reallocating the string.
|