@quandev104/pi-style 0.1.1
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 +35 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/extensions/pi-style.js +7897 -0
- package/dist/extensions/pi-style.js.map +1 -0
- package/extension-src/pi-style/app/command-service.ts +244 -0
- package/extension-src/pi-style/app/commands.ts +12 -0
- package/extension-src/pi-style/app/config-storage.ts +98 -0
- package/extension-src/pi-style/app/doctor.ts +53 -0
- package/extension-src/pi-style/app/index.ts +322 -0
- package/extension-src/pi-style/app/providers.ts +268 -0
- package/extension-src/pi-style/app/render-scheduler.ts +48 -0
- package/extension-src/pi-style/app/runtime.ts +404 -0
- package/extension-src/pi-style/app/snapshot.ts +15 -0
- package/extension-src/pi-style/domain/capabilities.ts +34 -0
- package/extension-src/pi-style/domain/config-authorization.ts +20 -0
- package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
- package/extension-src/pi-style/domain/config-diff.ts +30 -0
- package/extension-src/pi-style/domain/config-migrations.ts +52 -0
- package/extension-src/pi-style/domain/config-normalization.ts +567 -0
- package/extension-src/pi-style/domain/config-presets.ts +51 -0
- package/extension-src/pi-style/domain/config-types.ts +115 -0
- package/extension-src/pi-style/domain/providers.ts +37 -0
- package/extension-src/pi-style/domain/status-presets.ts +60 -0
- package/extension-src/pi-style/domain/status-renderer.ts +142 -0
- package/extension-src/pi-style/domain/status.ts +430 -0
- package/extension-src/pi-style/domain/theme.ts +232 -0
- package/extension-src/pi-style/features/.gitkeep +0 -0
- package/extension-src/pi-style/features/editor/index.ts +304 -0
- package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
- package/extension-src/pi-style/features/messages/index.ts +145 -0
- package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
- package/extension-src/pi-style/features/startup/index.ts +564 -0
- package/extension-src/pi-style/features/startup/logo.ts +151 -0
- package/extension-src/pi-style/features/status-line/index.ts +319 -0
- package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
- package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
- package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
- package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
- package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
- package/extension-src/pi-style/features/tools/index.ts +480 -0
- package/extension-src/pi-style/pi/commands.ts +17 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
- package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
- package/extension-src/pi-style/pi/config-host.ts +52 -0
- package/extension-src/pi-style/pi/config-session.ts +105 -0
- package/extension-src/pi-style/pi/index.ts +77 -0
- package/extension-src/pi-style/pi/operational-state.ts +29 -0
- package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
- package/extension-src/pi-style/pi/session-usage.ts +62 -0
- package/extension-src/pi-style/pi/startup-resources.ts +70 -0
- package/extension-src/pi-style/shared/.gitkeep +0 -0
- package/extension-src/pi-style/shared/ansi.ts +386 -0
- package/extension-src/pi-style/shared/box.ts +805 -0
- package/extension-src/pi-style/shared/disposable-store.ts +28 -0
- package/extension-src/pi-style/shared/elapsed.ts +88 -0
- package/extension-src/pi-style/shared/render-budget.ts +367 -0
- package/extension-src/pi-style/shared/split-diff.ts +692 -0
- package/extension-src/pi-style/shared/theme-extras.ts +292 -0
- package/package.json +68 -0
- package/themes/.gitkeep +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface Disposable {
|
|
2
|
+
dispose(): void | Promise<void>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class DisposableStore implements Disposable {
|
|
6
|
+
private readonly items: Disposable[] = [];
|
|
7
|
+
private disposed = false;
|
|
8
|
+
add<T extends Disposable>(item: T): T {
|
|
9
|
+
if (this.disposed) {
|
|
10
|
+
void item.dispose();
|
|
11
|
+
return item;
|
|
12
|
+
}
|
|
13
|
+
this.items.push(item);
|
|
14
|
+
return item;
|
|
15
|
+
}
|
|
16
|
+
addCallback(callback: () => void | Promise<void>): void {
|
|
17
|
+
this.add({ dispose: callback });
|
|
18
|
+
}
|
|
19
|
+
get size(): number {
|
|
20
|
+
return this.items.length;
|
|
21
|
+
}
|
|
22
|
+
async dispose(): Promise<void> {
|
|
23
|
+
if (this.disposed) return;
|
|
24
|
+
this.disposed = true;
|
|
25
|
+
for (let i = this.items.length - 1; i >= 0; i--) await this.items[i]?.dispose();
|
|
26
|
+
this.items.length = 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Tool metrics: elapsed wall time + output size formatting and annotation.
|
|
2
|
+
|
|
3
|
+
const ELAPSED_KEY = "__elapsedMs";
|
|
4
|
+
const OUTPUT_CHARS_KEY = "__outputChars";
|
|
5
|
+
|
|
6
|
+
export interface MetricResultLike {
|
|
7
|
+
readonly content?: readonly unknown[];
|
|
8
|
+
readonly details?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getTextOutputLength(result: MetricResultLike): number {
|
|
12
|
+
if (!Array.isArray(result.content)) return 0;
|
|
13
|
+
let length = 0;
|
|
14
|
+
let seenText = false;
|
|
15
|
+
for (const contentBlock of result.content) {
|
|
16
|
+
if (!contentBlock || typeof contentBlock !== "object") continue;
|
|
17
|
+
const block = contentBlock as { type?: unknown; text?: unknown };
|
|
18
|
+
if (block.type !== "text") continue;
|
|
19
|
+
if (seenText) length += 1; // matches getTextOutput() joining text blocks with newlines
|
|
20
|
+
length += String(block.text ?? "").replace(/\r/g, "").length;
|
|
21
|
+
seenText = true;
|
|
22
|
+
}
|
|
23
|
+
return length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatCompactCount(value: number): string {
|
|
27
|
+
if (value < 1000) return `${Math.round(value)}`;
|
|
28
|
+
if (value < 10000) return `${(value / 1000).toFixed(1)}k`;
|
|
29
|
+
if (value < 1000000) return `${Math.round(value / 1000)}k`;
|
|
30
|
+
if (value < 10000000) return `${(value / 1000000).toFixed(1)}M`;
|
|
31
|
+
return `${Math.round(value / 1000000)}M`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatElapsedMs(ms: number | undefined): string {
|
|
35
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "";
|
|
36
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
37
|
+
const s = ms / 1000;
|
|
38
|
+
return s < 10 ? `${s.toFixed(1)}s` : `${Math.round(s)}s`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatOutputChars(chars: number | undefined): string {
|
|
42
|
+
if (typeof chars !== "number" || !Number.isFinite(chars) || chars <= 0) return "";
|
|
43
|
+
return `${formatCompactCount(chars)} ${chars === 1 ? "char" : "chars"}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatToolMetricsFromValues(elapsedMs: number | undefined, outputChars: number | undefined): string {
|
|
47
|
+
return [formatElapsedMs(elapsedMs), formatOutputChars(outputChars)].filter(Boolean).join(" · ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getElapsedMs(result: MetricResultLike | undefined): number | undefined {
|
|
51
|
+
if (!result || typeof result.details !== "object" || result.details === null) return undefined;
|
|
52
|
+
const elapsed = (result.details as Record<string, unknown>)[ELAPSED_KEY];
|
|
53
|
+
return typeof elapsed === "number" && Number.isFinite(elapsed) ? elapsed : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatElapsed(result: MetricResultLike | undefined): string {
|
|
57
|
+
return formatElapsedMs(getElapsedMs(result));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatOutputSize(result: MetricResultLike | undefined): string {
|
|
61
|
+
return formatOutputChars(
|
|
62
|
+
(result?.details as Record<string, unknown> | undefined)?.[OUTPUT_CHARS_KEY] as number | undefined,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatToolMetrics(result: MetricResultLike | undefined): string {
|
|
67
|
+
return [formatElapsed(result), formatOutputSize(result)].filter(Boolean).join(" · ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Annotate elapsed/output metrics into a result's details (only when absent). */
|
|
71
|
+
export function annotateToolResultMetrics(result: MetricResultLike | undefined, elapsedMs?: number): void {
|
|
72
|
+
if (!result || typeof result !== "object") return;
|
|
73
|
+
if (!result.details || typeof result.details !== "object") {
|
|
74
|
+
(result as { details?: unknown }).details = {};
|
|
75
|
+
}
|
|
76
|
+
const details = result.details as Record<string, unknown>;
|
|
77
|
+
const existingElapsed = details[ELAPSED_KEY];
|
|
78
|
+
if (
|
|
79
|
+
typeof elapsedMs === "number" &&
|
|
80
|
+
Number.isFinite(elapsedMs) &&
|
|
81
|
+
(typeof existingElapsed !== "number" || !Number.isFinite(existingElapsed))
|
|
82
|
+
) {
|
|
83
|
+
details[ELAPSED_KEY] = elapsedMs;
|
|
84
|
+
}
|
|
85
|
+
if (typeof details[OUTPUT_CHARS_KEY] !== "number" || !Number.isFinite(details[OUTPUT_CHARS_KEY])) {
|
|
86
|
+
details[OUTPUT_CHARS_KEY] = getTextOutputLength(result);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// Render budget + ANSI-safe width/truncation/wrapping helpers for boxed tool
|
|
2
|
+
// presentation.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
truncateToWidth as tuiTruncateToWidth,
|
|
6
|
+
visibleWidth as tuiVisibleWidth,
|
|
7
|
+
wrapTextWithAnsi,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
export const MAX_RENDER_LINE_CHARS = 2000;
|
|
11
|
+
export const DEFAULT_COLLAPSED_RENDER_LINES = 10;
|
|
12
|
+
export const MAX_BOXED_RESULT_RENDERED_HEAD_LINES = 40;
|
|
13
|
+
export const MAX_BOXED_RESULT_RENDERED_TAIL_LINES = 8;
|
|
14
|
+
export const MAX_BOXED_RESULT_RENDERED_LINES = 160;
|
|
15
|
+
|
|
16
|
+
const RENDER_TRUNCATION_SUFFIX = "… (truncated)";
|
|
17
|
+
const TRUNCATE_ELLIPSIS = "…";
|
|
18
|
+
const ANSI_RESET = "\x1b[0m";
|
|
19
|
+
const ESC = "\x1b";
|
|
20
|
+
const SGR_PREFIX_PATTERN = new RegExp(`^(?:${ESC}\\[[0-9;]*m)+`);
|
|
21
|
+
const SGR_SUFFIX_PATTERN = new RegExp(`(?:${ESC}\\[[0-9;]*m)+$`);
|
|
22
|
+
|
|
23
|
+
type SimpleSgrWrappedText = {
|
|
24
|
+
prefix: string;
|
|
25
|
+
body: string;
|
|
26
|
+
suffix: string;
|
|
27
|
+
kind: "sgrAscii" | "sgrSimple";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type FastTextKind = "ascii" | "sgrAscii" | "simple" | "sgrSimple";
|
|
31
|
+
|
|
32
|
+
type FastTextWidth = {
|
|
33
|
+
visibleWidth: number;
|
|
34
|
+
kind: FastTextKind;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type FastTruncateResult = {
|
|
38
|
+
text: string;
|
|
39
|
+
visibleWidth: number;
|
|
40
|
+
kind: FastTextKind;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type FastBoxLineContent = {
|
|
44
|
+
text: string;
|
|
45
|
+
visibleWidth: number;
|
|
46
|
+
kind: FastTextKind;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type BoxedResultRenderBudget = {
|
|
50
|
+
headLines: number;
|
|
51
|
+
tailLines: number;
|
|
52
|
+
maxRenderedLines: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cut a string to a maximum UTF-16 length without splitting a surrogate pair
|
|
57
|
+
* (emoji / supplementary-plane characters). A plain `slice` on a non-ASCII
|
|
58
|
+
* boundary produces a lone surrogate that renders as the U+FFFD replacement
|
|
59
|
+
* character (�).
|
|
60
|
+
*/
|
|
61
|
+
export function truncateAtCodePointBoundary(text: string, maxChars: number): string {
|
|
62
|
+
if (text.length <= maxChars) return text;
|
|
63
|
+
const code = text.charCodeAt(maxChars);
|
|
64
|
+
const end = code >= 0xdc00 && code <= 0xdfff ? maxChars - 1 : maxChars;
|
|
65
|
+
return text.slice(0, end);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function clampRenderLine(line: string, maxChars = MAX_RENDER_LINE_CHARS): string {
|
|
69
|
+
if (line.length <= maxChars) return line;
|
|
70
|
+
return truncateAtCodePointBoundary(line, maxChars) + RENDER_TRUNCATION_SUFFIX;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isPrintableAsciiCode(code: number): boolean {
|
|
74
|
+
return code >= 0x20 && code <= 0x7e;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isSimpleWidthOneGlyphCode(code: number): boolean {
|
|
78
|
+
if (code >= 0x2500 && code <= 0x257f) return true; // box drawing
|
|
79
|
+
if (code >= 0x2580 && code <= 0x259f) return true; // block elements
|
|
80
|
+
if (code >= 0x25a0 && code <= 0x25ff) return true; // geometric shapes used by UI status marks
|
|
81
|
+
if (code >= 0x2800 && code <= 0x28ff) return true; // braille loader frames
|
|
82
|
+
return (
|
|
83
|
+
code === 0x00b2 || // ²
|
|
84
|
+
code === 0x00b7 || // ·
|
|
85
|
+
code === 0x03c0 || // π
|
|
86
|
+
code === 0x2013 || // –
|
|
87
|
+
code === 0x2014 || // —
|
|
88
|
+
code === 0x2022 || // •
|
|
89
|
+
code === 0x2026 || // …
|
|
90
|
+
code === 0x2191 || // ↑
|
|
91
|
+
code === 0x2192 || // →
|
|
92
|
+
code === 0x2193 || // ↓
|
|
93
|
+
code === 0x21b3 || // ↳
|
|
94
|
+
code === 0x2205 || // ∅
|
|
95
|
+
code === 0x2248 || // ≈
|
|
96
|
+
code === 0x22ef || // ⋯
|
|
97
|
+
code === 0x2387 || // ⎇
|
|
98
|
+
code === 0x23f9 || // ⏹
|
|
99
|
+
code === 0x270e || // ✎
|
|
100
|
+
code === 0x2713 || // ✓
|
|
101
|
+
code === 0x2717 || // ✗
|
|
102
|
+
code === 0x276f || // ❯
|
|
103
|
+
code === 0x2794 // ➔
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isSimpleWidthOneCode(code: number): boolean {
|
|
108
|
+
return isPrintableAsciiCode(code) || isSimpleWidthOneGlyphCode(code);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function breakLongAsciiWord(word: string, width: number): string[] {
|
|
112
|
+
const lines: string[] = [];
|
|
113
|
+
for (let i = 0; i < word.length; i += width) {
|
|
114
|
+
lines.push(word.slice(i, i + width));
|
|
115
|
+
}
|
|
116
|
+
return lines.length > 0 ? lines : [""];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function wrapPrintableAsciiLine(line: string, width: number): string[] {
|
|
120
|
+
if (!line) return [""];
|
|
121
|
+
if (line.length <= width) return [line];
|
|
122
|
+
|
|
123
|
+
const wrapped: string[] = [];
|
|
124
|
+
let currentLine = "";
|
|
125
|
+
let currentVisibleLength = 0;
|
|
126
|
+
let tokenStart = 0;
|
|
127
|
+
|
|
128
|
+
while (tokenStart < line.length) {
|
|
129
|
+
const tokenIsSpace = line[tokenStart] === " ";
|
|
130
|
+
let tokenEnd = tokenStart + 1;
|
|
131
|
+
while (tokenEnd < line.length && (line[tokenEnd] === " ") === tokenIsSpace) tokenEnd++;
|
|
132
|
+
|
|
133
|
+
const token = line.slice(tokenStart, tokenEnd);
|
|
134
|
+
const tokenVisibleLength = token.length;
|
|
135
|
+
if (tokenVisibleLength > width && !tokenIsSpace) {
|
|
136
|
+
if (currentLine) {
|
|
137
|
+
wrapped.push(currentLine.trimEnd());
|
|
138
|
+
currentLine = "";
|
|
139
|
+
currentVisibleLength = 0;
|
|
140
|
+
}
|
|
141
|
+
const broken = breakLongAsciiWord(token, width);
|
|
142
|
+
wrapped.push(...broken.slice(0, -1));
|
|
143
|
+
currentLine = broken[broken.length - 1] ?? "";
|
|
144
|
+
currentVisibleLength = currentLine.length;
|
|
145
|
+
tokenStart = tokenEnd;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const totalNeeded = currentVisibleLength + tokenVisibleLength;
|
|
150
|
+
if (totalNeeded > width && currentVisibleLength > 0) {
|
|
151
|
+
wrapped.push(currentLine.trimEnd());
|
|
152
|
+
if (tokenIsSpace) {
|
|
153
|
+
currentLine = "";
|
|
154
|
+
currentVisibleLength = 0;
|
|
155
|
+
} else {
|
|
156
|
+
currentLine = token;
|
|
157
|
+
currentVisibleLength = tokenVisibleLength;
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
currentLine += token;
|
|
161
|
+
currentVisibleLength += tokenVisibleLength;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
tokenStart = tokenEnd;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (currentLine) wrapped.push(currentLine);
|
|
168
|
+
return wrapped.length > 0 ? wrapped.map((wrappedLine) => wrappedLine.trimEnd()) : [""];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function matchSimpleSgrWrappedText(text: string): SimpleSgrWrappedText | null {
|
|
172
|
+
const prefixMatch = SGR_PREFIX_PATTERN.exec(text);
|
|
173
|
+
const suffixMatch = SGR_SUFFIX_PATTERN.exec(text);
|
|
174
|
+
if (!prefixMatch || !suffixMatch) return null;
|
|
175
|
+
const prefix = prefixMatch[0];
|
|
176
|
+
const suffix = suffixMatch[0];
|
|
177
|
+
const bodyStart = prefix.length;
|
|
178
|
+
const bodyEnd = suffixMatch.index;
|
|
179
|
+
if (bodyEnd < bodyStart) return null;
|
|
180
|
+
|
|
181
|
+
const body = text.slice(bodyStart, bodyEnd);
|
|
182
|
+
const bodyWidth = knownVisibleWidth(body);
|
|
183
|
+
if (!bodyWidth || bodyWidth.kind === "sgrAscii" || bodyWidth.kind === "sgrSimple") return null;
|
|
184
|
+
return { prefix, body, suffix, kind: bodyWidth.kind === "ascii" ? "sgrAscii" : "sgrSimple" };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function wrapSimpleSgrWrappedText(
|
|
188
|
+
text: string,
|
|
189
|
+
width: number,
|
|
190
|
+
): { lines: string[]; kind: "sgrAscii" | "sgrSimple" } | null {
|
|
191
|
+
const match = matchSimpleSgrWrappedText(text);
|
|
192
|
+
if (!match) return null;
|
|
193
|
+
return {
|
|
194
|
+
lines: wrapPrintableAsciiLine(match.body, width).map((line) => `${match.prefix}${line}${match.suffix}`),
|
|
195
|
+
kind: match.kind,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readSgrSequenceEnd(text: string, offset: number): number {
|
|
200
|
+
if (text.charCodeAt(offset) !== 0x1b || text[offset + 1] !== "[") return -1;
|
|
201
|
+
let cursor = offset + 2;
|
|
202
|
+
while (cursor < text.length) {
|
|
203
|
+
const code = text.charCodeAt(cursor);
|
|
204
|
+
if (code === 0x6d) return cursor + 1; // m
|
|
205
|
+
if (code !== 0x3b && (code < 0x30 || code > 0x39)) return -1; // ; or 0-9
|
|
206
|
+
cursor++;
|
|
207
|
+
}
|
|
208
|
+
return -1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function knownVisibleWidth(text: string): FastTextWidth | null {
|
|
212
|
+
let width = 0;
|
|
213
|
+
let sawSgr = false;
|
|
214
|
+
let sawSimpleGlyph = false;
|
|
215
|
+
for (let i = 0; i < text.length; ) {
|
|
216
|
+
const sgrEnd = readSgrSequenceEnd(text, i);
|
|
217
|
+
if (sgrEnd > i) {
|
|
218
|
+
sawSgr = true;
|
|
219
|
+
i = sgrEnd;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const code = text.charCodeAt(i);
|
|
223
|
+
if (!isSimpleWidthOneCode(code)) return null;
|
|
224
|
+
if (!isPrintableAsciiCode(code)) sawSimpleGlyph = true;
|
|
225
|
+
width++;
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
if (sawSgr) return { visibleWidth: width, kind: sawSimpleGlyph ? "sgrSimple" : "sgrAscii" };
|
|
229
|
+
return { visibleWidth: width, kind: sawSimpleGlyph ? "simple" : "ascii" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function knownEllipsisWidth(text: string): number | null {
|
|
233
|
+
if (text === TRUNCATE_ELLIPSIS) return 1;
|
|
234
|
+
return knownVisibleWidth(text)?.visibleWidth ?? null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function truncatePrintableAscii(
|
|
238
|
+
text: string,
|
|
239
|
+
width: number,
|
|
240
|
+
ellipsis = TRUNCATE_ELLIPSIS,
|
|
241
|
+
): { text: string; visibleWidth: number } {
|
|
242
|
+
if (width <= 0) return { text: "", visibleWidth: 0 };
|
|
243
|
+
if (text.length <= width) return { text, visibleWidth: text.length };
|
|
244
|
+
const ellipsisWidth = knownEllipsisWidth(ellipsis);
|
|
245
|
+
if (ellipsisWidth === null) return { text: text.slice(0, width), visibleWidth: width };
|
|
246
|
+
if (ellipsisWidth >= width) {
|
|
247
|
+
if (ellipsis === TRUNCATE_ELLIPSIS) return { text: TRUNCATE_ELLIPSIS, visibleWidth: 1 };
|
|
248
|
+
return { text: ellipsis.slice(0, width), visibleWidth: Math.min(width, ellipsis.length) };
|
|
249
|
+
}
|
|
250
|
+
const targetWidth = Math.max(0, width - ellipsisWidth);
|
|
251
|
+
return { text: `${text.slice(0, targetWidth)}${ellipsis}`, visibleWidth: targetWidth + ellipsisWidth };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function truncateSgrAscii(
|
|
255
|
+
text: string,
|
|
256
|
+
width: number,
|
|
257
|
+
ellipsis: string,
|
|
258
|
+
visibleWidth: number,
|
|
259
|
+
): { text: string; visibleWidth: number } | null {
|
|
260
|
+
if (visibleWidth <= width) return { text, visibleWidth };
|
|
261
|
+
const ellipsisWidth = knownEllipsisWidth(ellipsis);
|
|
262
|
+
if (ellipsisWidth === null) return null;
|
|
263
|
+
if (ellipsisWidth >= width) return truncatePrintableAscii(ellipsis, width, "");
|
|
264
|
+
|
|
265
|
+
const targetWidth = Math.max(0, width - ellipsisWidth);
|
|
266
|
+
let keptWidth = 0;
|
|
267
|
+
let output = "";
|
|
268
|
+
for (let i = 0; i < text.length && keptWidth < targetWidth; ) {
|
|
269
|
+
const sgrEnd = readSgrSequenceEnd(text, i);
|
|
270
|
+
if (sgrEnd > i) {
|
|
271
|
+
output += text.slice(i, sgrEnd);
|
|
272
|
+
i = sgrEnd;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const code = text.charCodeAt(i);
|
|
276
|
+
if (!isSimpleWidthOneCode(code)) return null;
|
|
277
|
+
output += text[i] ?? "";
|
|
278
|
+
keptWidth++;
|
|
279
|
+
i++;
|
|
280
|
+
}
|
|
281
|
+
return { text: `${output}${ANSI_RESET}${ellipsis}${ANSI_RESET}`, visibleWidth: keptWidth + ellipsisWidth };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function fastTruncateText(text: string, width: number, ellipsis = TRUNCATE_ELLIPSIS): FastTruncateResult | null {
|
|
285
|
+
const knownWidth = knownVisibleWidth(text);
|
|
286
|
+
if (!knownWidth) return null;
|
|
287
|
+
if (knownWidth.kind === "ascii" || knownWidth.kind === "simple")
|
|
288
|
+
return { ...truncatePrintableAscii(text, width, ellipsis), kind: knownWidth.kind };
|
|
289
|
+
|
|
290
|
+
const simple = matchSimpleSgrWrappedText(text);
|
|
291
|
+
if (simple) {
|
|
292
|
+
const truncated = truncatePrintableAscii(simple.body, width, ellipsis);
|
|
293
|
+
return {
|
|
294
|
+
text: `${simple.prefix}${truncated.text}${simple.suffix}`,
|
|
295
|
+
visibleWidth: truncated.visibleWidth,
|
|
296
|
+
kind: simple.kind,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const truncated = truncateSgrAscii(text, width, ellipsis, knownWidth.visibleWidth);
|
|
301
|
+
return truncated ? { ...truncated, kind: knownWidth.kind } : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function safeVisibleWidth(text: string): number {
|
|
305
|
+
const fastWidth = knownVisibleWidth(text);
|
|
306
|
+
if (fastWidth) return fastWidth.visibleWidth;
|
|
307
|
+
if (text === TRUNCATE_ELLIPSIS) return 1;
|
|
308
|
+
return tuiVisibleWidth(text);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function safeTruncateToWidth(text: string, maxWidth: number, ellipsis = "...", pad = false): string {
|
|
312
|
+
const width = Math.floor(maxWidth);
|
|
313
|
+
if (!Number.isFinite(width) || width <= 0) return "";
|
|
314
|
+
if (text.length === 0) return pad ? " ".repeat(width) : "";
|
|
315
|
+
|
|
316
|
+
const truncated = fastTruncateText(text, width, ellipsis);
|
|
317
|
+
if (truncated)
|
|
318
|
+
return pad ? `${truncated.text}${" ".repeat(Math.max(0, width - truncated.visibleWidth))}` : truncated.text;
|
|
319
|
+
|
|
320
|
+
return tuiTruncateToWidth(text, maxWidth, ellipsis, pad);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function fastBoxLineContent(content: string, width: number): FastBoxLineContent | null {
|
|
324
|
+
const contentWidth = Math.floor(width);
|
|
325
|
+
if (!Number.isFinite(contentWidth) || contentWidth <= 0) return null;
|
|
326
|
+
|
|
327
|
+
return fastTruncateText(content, contentWidth);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function safeWrapTextWithAnsi(text: string, width: number, maxChars = MAX_RENDER_LINE_CHARS): string[] {
|
|
331
|
+
const clamped = clampRenderLine(text, maxChars);
|
|
332
|
+
const wrapWidth = Math.floor(width);
|
|
333
|
+
if (!Number.isFinite(wrapWidth) || wrapWidth <= 0) {
|
|
334
|
+
return wrapTextWithAnsi(clamped, width);
|
|
335
|
+
}
|
|
336
|
+
const clampedWidth = knownVisibleWidth(clamped);
|
|
337
|
+
if (clampedWidth?.kind === "ascii" || clampedWidth?.kind === "simple") {
|
|
338
|
+
return wrapPrintableAsciiLine(clamped, wrapWidth);
|
|
339
|
+
}
|
|
340
|
+
const simpleSgrWrapped = wrapSimpleSgrWrappedText(clamped, wrapWidth);
|
|
341
|
+
if (simpleSgrWrapped) {
|
|
342
|
+
return simpleSgrWrapped.lines;
|
|
343
|
+
}
|
|
344
|
+
return wrapTextWithAnsi(clamped, width);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function boxedResultRenderBudget(
|
|
348
|
+
rawLineBudget: number = DEFAULT_COLLAPSED_RENDER_LINES,
|
|
349
|
+
): BoxedResultRenderBudget {
|
|
350
|
+
const rawLines = Math.max(
|
|
351
|
+
0,
|
|
352
|
+
Math.floor(Number.isFinite(rawLineBudget) ? rawLineBudget : DEFAULT_COLLAPSED_RENDER_LINES),
|
|
353
|
+
);
|
|
354
|
+
const maxRenderedLines = Math.max(1, Math.min(rawLines * 3, MAX_BOXED_RESULT_RENDERED_LINES));
|
|
355
|
+
const tailLines = Math.min(
|
|
356
|
+
Math.ceil(rawLines * 0.15),
|
|
357
|
+
MAX_BOXED_RESULT_RENDERED_TAIL_LINES,
|
|
358
|
+
Math.max(0, maxRenderedLines - 1),
|
|
359
|
+
);
|
|
360
|
+
const headLines = Math.min(
|
|
361
|
+
rawLines,
|
|
362
|
+
MAX_BOXED_RESULT_RENDERED_HEAD_LINES,
|
|
363
|
+
Math.max(1, maxRenderedLines - tailLines - 1),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
return { headLines, tailLines, maxRenderedLines };
|
|
367
|
+
}
|