pi-tui-status-beautifier 1.1.0 β 1.1.2
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 +8 -0
- package/LICENSE +21 -0
- package/README.md +7 -2
- package/extensions/tui-status-beautifier.ts +146 -41
- package/package.json +16 -2
- package/extensions/tui-status-beautifier.spec.ts +0 -77
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
## [1.1.1] - 2026-08-23
|
|
3
|
+
### Added
|
|
4
|
+
- Memory-hoisted visual cell-width checks of character sequences to correctly align TUI status templates (Finding 5)
|
|
5
|
+
- State cache-based dirty-checking for status updates to prevent redundant redrawing and layout flickering (Finding 6)
|
|
6
|
+
### Fixed
|
|
7
|
+
- Fixed TUI alignment issues involving full-width CJK characters and emojis in layout paddings
|
|
8
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jason Song (@3ZEROS12)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A premium status bar beautifier extension for the Pi Terminal TUI. Instantly transforms boring status lines into brand-inspired themes and clean layouts.
|
|
4
4
|
|
|
5
|
-
## π Installation & Usage
|
|
5
|
+
## π Installation & Usage
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install directly via the Pi package manager:
|
|
8
8
|
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:pi-tui-status-beautifier
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install from git:
|
|
9
14
|
```bash
|
|
10
15
|
pi install git:github.com/3ZEROS12/pi-tui-status-beautifier
|
|
11
16
|
```
|
|
@@ -4,13 +4,13 @@ import * as path from "path";
|
|
|
4
4
|
|
|
5
5
|
// Mapping of internal package names to clean human-readable TUI status names
|
|
6
6
|
const COMMON_NAMES: Record<string, string> = {
|
|
7
|
-
|
|
7
|
+
plannotator: "plan",
|
|
8
8
|
"plannotator-review": "plan",
|
|
9
9
|
"pi-mcp-adapter": "mcp",
|
|
10
10
|
"subagent-slash": "subagent",
|
|
11
11
|
"subagent-slash-text": "subagent",
|
|
12
12
|
"pi-wechat-assistant": "wechat",
|
|
13
|
-
"wechat-assistant": "wechat"
|
|
13
|
+
"wechat-assistant": "wechat",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// Store original statuses to allow dynamic redrawing on configuration changes.
|
|
@@ -20,11 +20,16 @@ const originalStatuses = new Map<string, string | undefined>();
|
|
|
20
20
|
// Memoization cache for Google colorized text to reduce GC overhead and theme calls.
|
|
21
21
|
const googleColorizeCache = new Map<string, string>();
|
|
22
22
|
|
|
23
|
+
// State Cache for Dirty-Checking
|
|
24
|
+
const lastRenderedStatus = new Map<string, string | undefined>();
|
|
25
|
+
|
|
23
26
|
// Safe, non-backtracking ReDoS-mitigated ANSI escape sequence matcher (supporting ; and :)
|
|
24
|
-
const ANSI_STRIP_REGEX =
|
|
27
|
+
const ANSI_STRIP_REGEX =
|
|
28
|
+
/[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g;
|
|
25
29
|
|
|
26
30
|
// Hoisted regex patterns to prevent instantiation and GC overhead in the render hot-path
|
|
27
|
-
const STATUS_PATTERN =
|
|
31
|
+
const STATUS_PATTERN =
|
|
32
|
+
/[π’π΄π‘βͺβΈβ
βββ?βββββπ]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected/i;
|
|
28
33
|
const NAME_SUFFIX_PATTERN = /-(extension|plugin|assistant|adapter|slash-text|slash|text|widget)$/gi;
|
|
29
34
|
const FRACTION_PATTERN = /(\d+\/\d+|\d+%\s*)/;
|
|
30
35
|
const PARENTHESES_PATTERN = /\((\d+)\)/;
|
|
@@ -38,7 +43,7 @@ const settingsPath = path.join(home, ".pi/agent/settings.json");
|
|
|
38
43
|
// Command-Driven I/O State:
|
|
39
44
|
// Loaded ONCE synchronously during extension loading, thereafter handled entirely in memory.
|
|
40
45
|
// Disk I/O only occurs when the user triggers the "/beautify" selection command.
|
|
41
|
-
let currentStyle = "minimal";
|
|
46
|
+
let currentStyle = "minimal";
|
|
42
47
|
|
|
43
48
|
try {
|
|
44
49
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -55,13 +60,14 @@ try {
|
|
|
55
60
|
// Google playful multi-color letter colorizer with type safeguards and memoization
|
|
56
61
|
export function googleColorize(text: string, theme: any): string {
|
|
57
62
|
if (!theme || typeof theme.fg !== "function") return text;
|
|
58
|
-
|
|
63
|
+
|
|
59
64
|
const cached = googleColorizeCache.get(text);
|
|
60
65
|
if (cached !== undefined) return cached;
|
|
61
66
|
|
|
62
67
|
// Google's brand colors cycle: Blue, Red, Yellow, Green
|
|
63
68
|
const colors = ["accent", "error", "warning", "success"];
|
|
64
|
-
const colorized = text
|
|
69
|
+
const colorized = text
|
|
70
|
+
.split("")
|
|
65
71
|
.map((char, index) => {
|
|
66
72
|
const color = colors[index % colors.length];
|
|
67
73
|
return theme.fg(color, char);
|
|
@@ -73,16 +79,19 @@ export function googleColorize(text: string, theme: any): string {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
// Clean status text and map state and details
|
|
76
|
-
export function beautifyStatus(
|
|
82
|
+
export function beautifyStatus(
|
|
83
|
+
key: string,
|
|
84
|
+
originalVal: string | undefined,
|
|
85
|
+
theme: any,
|
|
86
|
+
style: string
|
|
87
|
+
): string | undefined {
|
|
77
88
|
if (originalVal === undefined) return undefined;
|
|
78
89
|
if (style === "off") return originalVal; // Pass through untouched if disabled
|
|
79
90
|
|
|
80
91
|
try {
|
|
81
92
|
// Fast-path: bypass Regex search if no ESC or CSI bytes exist to reduce hot redraw latency
|
|
82
93
|
const hasAnsi = originalVal.includes("\u001B") || originalVal.includes("\u009B");
|
|
83
|
-
const cleanVal = hasAnsi
|
|
84
|
-
? originalVal.replace(ANSI_STRIP_REGEX, "").trim()
|
|
85
|
-
: originalVal.trim();
|
|
94
|
+
const cleanVal = hasAnsi ? originalVal.replace(ANSI_STRIP_REGEX, "").trim() : originalVal.trim();
|
|
86
95
|
|
|
87
96
|
if (cleanVal === "") return "";
|
|
88
97
|
|
|
@@ -100,11 +109,10 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
|
|
|
100
109
|
if (lastSlash !== -1) {
|
|
101
110
|
name = name.slice(lastSlash + 1);
|
|
102
111
|
}
|
|
103
|
-
name = name
|
|
104
|
-
.replace(NAME_SUFFIX_PATTERN, "")
|
|
105
|
-
.toLowerCase();
|
|
106
|
-
name = name.slice(0, 10);
|
|
112
|
+
name = name.replace(NAME_SUFFIX_PATTERN, "").toLowerCase();
|
|
107
113
|
}
|
|
114
|
+
// Optimization (Finding 5): Use visual cell width calculations rather than standard .length for CJK/emojis
|
|
115
|
+
const visualName = padToVisualWidth(sliceToVisualWidth(name, 10), 10);
|
|
108
116
|
|
|
109
117
|
// 2. Extract metrics/progress details safely (e.g. (2) or fraction 2/5 or count)
|
|
110
118
|
let details: string | undefined;
|
|
@@ -172,20 +180,20 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
|
|
|
172
180
|
switch (style) {
|
|
173
181
|
case "apple": {
|
|
174
182
|
const separator = hasThemeFg ? theme.fg("dim", " β ") : " β ";
|
|
175
|
-
const dimmedName = hasThemeFg ? theme.fg("dim",
|
|
183
|
+
const dimmedName = hasThemeFg ? theme.fg("dim", visualName) : visualName;
|
|
176
184
|
const detailStr = details ? ` (${details})` : "";
|
|
177
185
|
return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
|
|
178
186
|
}
|
|
179
187
|
case "openai": {
|
|
180
188
|
const spirograph = hasThemeFg ? theme.fg("success", "β") : "β";
|
|
181
189
|
const detailStr = details ? ` [${details}]` : "";
|
|
182
|
-
const dimmedName = hasThemeFg ? theme.fg("muted",
|
|
190
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
183
191
|
return `${spirograph} ${dimmedName}${detailStr}`;
|
|
184
192
|
}
|
|
185
193
|
case "anthropic": {
|
|
186
194
|
const organicStar = "β¦";
|
|
187
195
|
const coloredStar = hasThemeFg ? theme.fg(state, organicStar) : organicStar;
|
|
188
|
-
const dimmedName = hasThemeFg ? theme.fg("muted",
|
|
196
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
189
197
|
const detailStr = details ? ` β ${details}` : "";
|
|
190
198
|
return `${dimmedName} ${coloredStar}${detailStr}`;
|
|
191
199
|
}
|
|
@@ -194,39 +202,37 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
|
|
|
194
202
|
const detailStr = details ? ` β ${details}` : "";
|
|
195
203
|
const prefix = hasThemeFg ? theme.fg("dim", "[") : "[";
|
|
196
204
|
const suffix = hasThemeFg ? theme.fg("dim", "]") : "]";
|
|
197
|
-
return `${windowsLogo} ${prefix}${
|
|
205
|
+
return `${windowsLogo} ${prefix}${visualName}${suffix} ${coloredIndicator}${detailStr}`;
|
|
198
206
|
}
|
|
199
207
|
case "google": {
|
|
200
|
-
const coloredName = googleColorize(
|
|
208
|
+
const coloredName = googleColorize(visualName, theme);
|
|
201
209
|
const detailStr = details ? `:${details}` : "";
|
|
202
210
|
return `${coloredName} β ${coloredIndicator}${detailStr}`;
|
|
203
211
|
}
|
|
204
212
|
case "glass": {
|
|
205
213
|
const prefix = hasThemeFg ? theme.fg("dim", "β ") : "β ";
|
|
206
214
|
const suffix = hasThemeFg ? theme.fg("dim", " β") : " β";
|
|
207
|
-
const dimmedName = hasThemeFg ? theme.fg("muted",
|
|
215
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
208
216
|
const detailStr = details ? ` (${details})` : "";
|
|
209
217
|
return `${prefix}${dimmedName} ${coloredIndicator}${detailStr}${suffix}`;
|
|
210
218
|
}
|
|
211
219
|
case "glow": {
|
|
212
220
|
const label = ` ${char}${details ? ` ${details}` : ""} `;
|
|
213
221
|
const hasInverse = theme && typeof theme.inverse === "function";
|
|
214
|
-
const coloredBadge =
|
|
215
|
-
|
|
216
|
-
: `[${label}]`;
|
|
217
|
-
const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
|
|
222
|
+
const coloredBadge = hasInverse && hasThemeFg ? theme.inverse(theme.fg(state, label)) : `[${label}]`;
|
|
223
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
218
224
|
return `${dimmedName} ${coloredBadge}`;
|
|
219
225
|
}
|
|
220
226
|
case "matrix": {
|
|
221
|
-
const prefix = hasThemeFg ? theme.fg("dim", "
|
|
222
|
-
const suffix = hasThemeFg ? theme.fg("dim", "
|
|
227
|
+
const prefix = hasThemeFg ? theme.fg("dim", " \u2397 ") : " \u2397 ";
|
|
228
|
+
const suffix = hasThemeFg ? theme.fg("dim", " \u2398") : " \u2398";
|
|
223
229
|
const separator = hasThemeFg ? theme.fg("dim", " | ") : " | ";
|
|
224
230
|
const detailStr = details ? `${separator}${details}` : "";
|
|
225
|
-
return `${
|
|
231
|
+
return `${visualName}${prefix}${coloredIndicator}${detailStr}${suffix}`;
|
|
226
232
|
}
|
|
227
233
|
case "minimal": {
|
|
228
234
|
const separator = hasThemeFg ? theme.fg("dim", " β― ") : " β― ";
|
|
229
|
-
const dimmedName = hasThemeFg ? theme.fg("muted",
|
|
235
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
230
236
|
const detailStr = details ? ` (${details})` : "";
|
|
231
237
|
return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
|
|
232
238
|
}
|
|
@@ -239,6 +245,91 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
|
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Calculates the visual column cell width of a string (Finding 5), taking into account
|
|
250
|
+
* full-width Unicode characters, East Asian characters, and emojis.
|
|
251
|
+
*/
|
|
252
|
+
export function getStringWidth(str: string): number {
|
|
253
|
+
if (!str) return 0;
|
|
254
|
+
// Strip ANSI escape sequences & control codes before counting visual width
|
|
255
|
+
const clean = str.replace(ANSI_STRIP_REGEX, "");
|
|
256
|
+
let width = 0;
|
|
257
|
+
for (let i = 0; i < clean.length; i++) {
|
|
258
|
+
const code = clean.charCodeAt(i);
|
|
259
|
+
if (isFullWidth(code)) {
|
|
260
|
+
width += 2;
|
|
261
|
+
} else {
|
|
262
|
+
width += 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return width;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Checks if a character unicode code point is full-width (East Asian Width).
|
|
270
|
+
*/
|
|
271
|
+
function isFullWidth(code: number): boolean {
|
|
272
|
+
if (isNaN(code)) return false;
|
|
273
|
+
return (
|
|
274
|
+
code >= 0x1100 &&
|
|
275
|
+
(code <= 0x115f ||
|
|
276
|
+
code === 0x2329 ||
|
|
277
|
+
code === 0x232a ||
|
|
278
|
+
// CJK Radicals Supplement .. Enclosed CJK Letters and Months
|
|
279
|
+
(code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) ||
|
|
280
|
+
// Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
|
|
281
|
+
(code >= 0x3250 && code <= 0x4dbf) ||
|
|
282
|
+
// CJK Unified Ideographs .. Yi Radicals
|
|
283
|
+
(code >= 0x4e00 && code <= 0xa4c6) ||
|
|
284
|
+
// Hangul Jamo Extended-A
|
|
285
|
+
(code >= 0xa960 && code <= 0xa97c) ||
|
|
286
|
+
// Hangul Syllables
|
|
287
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
288
|
+
// CJK Compatibility Ideographs
|
|
289
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
290
|
+
// Vertical Forms
|
|
291
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
292
|
+
// CJK Compatibility Forms .. Small Form Variants
|
|
293
|
+
(code >= 0xfe30 && code <= 0xfe6b) ||
|
|
294
|
+
// Halfwidth and Fullwidth Forms
|
|
295
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
296
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
297
|
+
// East Asian Wide characters beyond BMP
|
|
298
|
+
(code >= 0x1b000 && code <= 0x1b001) ||
|
|
299
|
+
(code >= 0x1f200 && code <= 0x1f251) ||
|
|
300
|
+
(code >= 0x20000 && code <= 0x3fffd))
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Slices a string to a maximum visual cell width.
|
|
306
|
+
*/
|
|
307
|
+
export function sliceToVisualWidth(str: string, maxWidth: number): string {
|
|
308
|
+
let width = 0;
|
|
309
|
+
let sliced = "";
|
|
310
|
+
for (let i = 0; i < str.length; i++) {
|
|
311
|
+
const code = str.charCodeAt(i);
|
|
312
|
+
const charWidth = isFullWidth(code) ? 2 : 1;
|
|
313
|
+
if (width + charWidth > maxWidth) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
width += charWidth;
|
|
317
|
+
sliced += str.charAt(i);
|
|
318
|
+
}
|
|
319
|
+
return sliced;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Pads a string's visual cell width to target width with trailing spaces.
|
|
324
|
+
*/
|
|
325
|
+
export function padToVisualWidth(str: string, targetWidth: number, padChar = " "): string {
|
|
326
|
+
const currentWidth = getStringWidth(str);
|
|
327
|
+
if (currentWidth >= targetWidth) {
|
|
328
|
+
return str;
|
|
329
|
+
}
|
|
330
|
+
return str + padChar.repeat(targetWidth - currentWidth);
|
|
331
|
+
}
|
|
332
|
+
|
|
242
333
|
export default function (pi: ExtensionAPI) {
|
|
243
334
|
pi.on("session_start", async (_event, ctx) => {
|
|
244
335
|
if (!ctx.hasUI || !ctx.ui) return;
|
|
@@ -246,6 +337,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
337
|
// Flush stale statuses from previous sessions to prevent memory leaks and clear color cache
|
|
247
338
|
originalStatuses.clear();
|
|
248
339
|
googleColorizeCache.clear();
|
|
340
|
+
lastRenderedStatus.clear();
|
|
249
341
|
|
|
250
342
|
const originalSetStatus = ctx.ui.setStatus;
|
|
251
343
|
if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
|
|
@@ -255,7 +347,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
255
347
|
|
|
256
348
|
// Uses in-memory synchronization (zero file I/O overhead on render pipeline ticks)
|
|
257
349
|
const beautified = beautifyStatus(key, value, ctx.ui.theme, currentStyle);
|
|
258
|
-
|
|
350
|
+
|
|
351
|
+
// Dirty-checking (Finding 6): Skip redundant stdout paint updates
|
|
352
|
+
const cacheKey = `${key}:${currentStyle}`;
|
|
353
|
+
if (lastRenderedStatus.get(cacheKey) === beautified) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
lastRenderedStatus.set(cacheKey, beautified);
|
|
357
|
+
|
|
259
358
|
return originalSetStatus.call(ctx.ui, key, beautified);
|
|
260
359
|
};
|
|
261
360
|
(wrapped as any).__beautifierHooked = true;
|
|
@@ -270,7 +369,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
270
369
|
if (!ctx.hasUI || !ctx.ui) {
|
|
271
370
|
return;
|
|
272
371
|
}
|
|
273
|
-
|
|
372
|
+
|
|
274
373
|
const styleOptions = [
|
|
275
374
|
"minimal (Millennial Minimalist - e.g. name β― β)",
|
|
276
375
|
"apple (Apple Cupertino - e.g. name β β)",
|
|
@@ -281,25 +380,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
281
380
|
"glass (Charm Glass - e.g. β name β β)",
|
|
282
381
|
"glow (Glow Badge - e.g. name [ β ])",
|
|
283
382
|
"matrix (Retro Matrix - e.g. name β¦ β | 2 β¦)",
|
|
284
|
-
"off (use raw output)"
|
|
383
|
+
"off (use raw output)",
|
|
285
384
|
];
|
|
286
|
-
|
|
287
|
-
const choice = await ctx.ui.select(
|
|
288
|
-
`Choose status style (Current: ${currentStyle}):`,
|
|
289
|
-
styleOptions
|
|
290
|
-
);
|
|
385
|
+
|
|
386
|
+
const choice = await ctx.ui.select(`Choose status style (Current: ${currentStyle}):`, styleOptions);
|
|
291
387
|
|
|
292
388
|
if (choice) {
|
|
293
389
|
const styleKey = choice.split(" ")[0].trim();
|
|
294
390
|
const validStyles = [
|
|
295
|
-
"apple",
|
|
296
|
-
"
|
|
391
|
+
"apple",
|
|
392
|
+
"openai",
|
|
393
|
+
"anthropic",
|
|
394
|
+
"google",
|
|
395
|
+
"microsoft",
|
|
396
|
+
"minimal",
|
|
397
|
+
"glass",
|
|
398
|
+
"glow",
|
|
399
|
+
"matrix",
|
|
400
|
+
"off",
|
|
297
401
|
];
|
|
298
|
-
|
|
402
|
+
|
|
299
403
|
if (validStyles.includes(styleKey)) {
|
|
300
404
|
// 1. Instantly update in-memory caching
|
|
301
405
|
currentStyle = styleKey;
|
|
302
406
|
googleColorizeCache.clear(); // Clear cache when changing style/redrawing to ensure new colors are loaded
|
|
407
|
+
lastRenderedStatus.clear(); // Clear dirty checking cache to force repaint
|
|
303
408
|
ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
|
|
304
409
|
|
|
305
410
|
// 2. Redraw TUI indicator widgets instantly on configuration event using cached data
|
|
@@ -321,6 +426,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
321
426
|
}
|
|
322
427
|
}
|
|
323
428
|
}
|
|
324
|
-
}
|
|
429
|
+
},
|
|
325
430
|
});
|
|
326
431
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tui-status-beautifier",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Premium brand & layout status bar beautifier for Pi terminal TUI.",
|
|
5
5
|
"main": "extensions/tui-status-beautifier.ts",
|
|
6
6
|
"keywords": [
|
|
@@ -11,6 +11,20 @@
|
|
|
11
11
|
"./extensions/tui-status-beautifier.ts"
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"extensions/tui-status-beautifier.ts",
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/3ZEROS12/pi-tui-status-beautifier.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/3ZEROS12/pi-tui-status-beautifier#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/3ZEROS12/pi-tui-status-beautifier/issues"
|
|
27
|
+
},
|
|
14
28
|
"peerDependencies": {
|
|
15
29
|
"@earendil-works/pi-coding-agent": "*"
|
|
16
30
|
},
|
|
@@ -30,4 +44,4 @@
|
|
|
30
44
|
"typescript": "^5.3.3",
|
|
31
45
|
"vitest": "^1.2.2"
|
|
32
46
|
}
|
|
33
|
-
}
|
|
47
|
+
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { beautifyStatus, googleColorize } from "./tui-status-beautifier";
|
|
3
|
-
|
|
4
|
-
describe("TUI Status Beautifier Code Optimization Tests", () => {
|
|
5
|
-
const mockTheme = {
|
|
6
|
-
fg: (color: string, text: string) => `[${color}]${text}[/color]`,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
describe("ANSI Escape Sequence Stripping & Metric Extraction (Finding 1)", () => {
|
|
10
|
-
it("should correctly clean parameterized 256-color sequences without leaves/parameters leaking", () => {
|
|
11
|
-
const input = "\x1B[38;5;108mactive\x1B[0m (12)";
|
|
12
|
-
const output = beautifyStatus("test-extension", input, mockTheme, "minimal");
|
|
13
|
-
|
|
14
|
-
// Expected metric: 12. If parameters like 38 or 5 leaked, they might be matched as metric instead.
|
|
15
|
-
expect(output).toContain("(12)");
|
|
16
|
-
expect(output).not.toContain("38");
|
|
17
|
-
expect(output).not.toContain("5");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("should correctly clean truecolor RGB sequences", () => {
|
|
21
|
-
const input = "\x1B[38;2;255;0;255mactive\x1B[0m (45)";
|
|
22
|
-
const output = beautifyStatus("test-extension", input, mockTheme, "minimal");
|
|
23
|
-
|
|
24
|
-
expect(output).toContain("(45)");
|
|
25
|
-
expect(output).not.toContain("255");
|
|
26
|
-
expect(output).not.toContain("0");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("Fast-Path Optimization (Finding 2)", () => {
|
|
31
|
-
it("should process simple text correctly and avoid unnecessary matches", () => {
|
|
32
|
-
const output = beautifyStatus("my-plugin", "running 5/5", mockTheme, "minimal");
|
|
33
|
-
expect(output).toContain("my-plugin");
|
|
34
|
-
expect(output).toContain("(5/5)");
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("Google Colorize Memoization/Cache (Finding 5)", () => {
|
|
39
|
-
it("should cache subsequent calls for the same text to avoid redundant mapping and theme calls", () => {
|
|
40
|
-
const text = "plug";
|
|
41
|
-
const fgSpy = vi.fn((color: string, text: string) => `[${color}]${text}[/color]`);
|
|
42
|
-
const customTheme = { fg: fgSpy };
|
|
43
|
-
|
|
44
|
-
const firstCall = googleColorize(text, customTheme);
|
|
45
|
-
const secondCall = googleColorize(text, customTheme);
|
|
46
|
-
|
|
47
|
-
// Verify they return the same colorized string
|
|
48
|
-
expect(firstCall).toBe(secondCall);
|
|
49
|
-
|
|
50
|
-
// Due to cache/memoization, fgSpy should only be called search length times once
|
|
51
|
-
expect(fgSpy).toHaveBeenCalledTimes(text.length);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("Layout Preset Rendering", () => {
|
|
56
|
-
it("should render apple style", () => {
|
|
57
|
-
const output = beautifyStatus("test", "active", mockTheme, "apple");
|
|
58
|
-
expect(output).toContain("β");
|
|
59
|
-
expect(output).toContain("β");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("should render openai style", () => {
|
|
63
|
-
const output = beautifyStatus("test", "active", mockTheme, "openai");
|
|
64
|
-
expect(output).toContain("β");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("should render anthropic style", () => {
|
|
68
|
-
const output = beautifyStatus("test", "active", mockTheme, "anthropic");
|
|
69
|
-
expect(output).toContain("β¦");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("should return raw output when style is off", () => {
|
|
73
|
-
const output = beautifyStatus("test", "active (5)", mockTheme, "off");
|
|
74
|
-
expect(output).toBe("active (5)");
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|