pi-tui-status-beautifier 1.1.2 β 1.2.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/README.md +17 -31
- package/extensions/tui-status-beautifier.ts +115 -174
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,45 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-tui-status-beautifier
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Minimalist status bar formatter & AI-customizable layout engine for the [Pi](https://github.com/earendil-works/pi-coding-agent) terminal TUI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Automatically normalizes verbose extension status strings (`plannotator`, `pi-mcp-adapter`, `pi-subagents`, `toolflow`, etc.) into clean, visually aligned 10-column status badges with zero runtime disk I/O overhead.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pi install npm:pi-tui-status-beautifier
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
pi install git:github.com/3ZEROS12/pi-tui-status-beautifier
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### Enable/Disable Styles
|
|
19
|
-
Once installed, open the Pi TUI terminal and run the dynamic interactive switch command:
|
|
20
|
-
```bash
|
|
21
|
-
/beautify
|
|
22
|
-
```
|
|
23
|
-
This renders a live selectable menu to instantly toggle between brand themes (`Cupertino`, `OpenAI`, `Claude`, `Microsoft Fluent`, `Material`) and layout styles (`Minimal`, `Glass`, `Glow badge`, `Matrix frame`, or `Default/Off`).
|
|
24
|
-
|
|
25
|
-
---
|
|
13
|
+
## Usage
|
|
26
14
|
|
|
27
|
-
|
|
15
|
+
Run `/beautify` inside Pi to switch status bar layouts or generate a custom style with your AI agent:
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
- **`custom` β [AI Prompt Recipe]**: Pre-fills a structured prompt into your terminal editor so your coding agent can inspect `tui-status-beautifier.ts` and craft a bespoke status bar layout tailored to your exact aesthetic.
|
|
18
|
+
- **`minimal`**: Clean chevron indicator (`name β― β (2)`)
|
|
19
|
+
- **`glass`**: Framed capsule badge (`β name β (2) β`)
|
|
20
|
+
- **`glow`**: High-contrast inverse pill (`name [ β 2 ]`)
|
|
21
|
+
- **`off`**: Pass through raw status strings untouched
|
|
33
22
|
|
|
34
|
-
##
|
|
23
|
+
## Engineering Highlights
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* **Fluent Microsoft (`microsoft`)**: Flat terminal tile layout using Microsoft Fluent block `β`.
|
|
40
|
-
* **Material Google (`google`)**: Google multi-color letters rendering index name values.
|
|
25
|
+
- **Unicode & Surrogate-Pair Safe**: Full `codePointAt(0)` visual cell width measurement across CJK ideographs, Hiragana/Katakana, Hangul, emojis, and zero-width joiners (`0x200d`, `0xfe0f`).
|
|
26
|
+
- **Zero Render-Loop Disk I/O**: Reads `~/.pi/agent/settings.json` once at startup and performs dirty-checked in-memory rendering; atomic temp-file rename (`writeFileSync` + `renameSync`) only when switching styles via `/beautify`.
|
|
27
|
+
- **ReDoS-Safe ANSI Stripping**: Fast-path bypass when no ESC/CSI bytes are present.
|
|
41
28
|
|
|
42
|
-
|
|
29
|
+
## License
|
|
43
30
|
|
|
44
|
-
|
|
45
|
-
The core logic resides in `extensions/tui-status-beautifier.ts`. All status mapping is completely memory-cached to eliminate Paint Loop I/O bottlenecks.
|
|
31
|
+
MIT
|
|
@@ -13,13 +13,11 @@ const COMMON_NAMES: Record<string, string> = {
|
|
|
13
13
|
"wechat-assistant": "wechat",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
const VALID_STYLES = ["minimal", "glass", "glow", "off"] as const;
|
|
17
|
+
|
|
16
18
|
// Store original statuses to allow dynamic redrawing on configuration changes.
|
|
17
|
-
// Cleaned up on session restart to prevent memory leaks from replaced sessions.
|
|
18
19
|
const originalStatuses = new Map<string, string | undefined>();
|
|
19
20
|
|
|
20
|
-
// Memoization cache for Google colorized text to reduce GC overhead and theme calls.
|
|
21
|
-
const googleColorizeCache = new Map<string, string>();
|
|
22
|
-
|
|
23
21
|
// State Cache for Dirty-Checking
|
|
24
22
|
const lastRenderedStatus = new Map<string, string | undefined>();
|
|
25
23
|
|
|
@@ -29,7 +27,7 @@ const ANSI_STRIP_REGEX =
|
|
|
29
27
|
|
|
30
28
|
// Hoisted regex patterns to prevent instantiation and GC overhead in the render hot-path
|
|
31
29
|
const STATUS_PATTERN =
|
|
32
|
-
/[
|
|
30
|
+
/[π’π΄π‘βͺβΈβ
βββ?βββββπβ¬]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected/i;
|
|
33
31
|
const NAME_SUFFIX_PATTERN = /-(extension|plugin|assistant|adapter|slash-text|slash|text|widget)$/gi;
|
|
34
32
|
const FRACTION_PATTERN = /(\d+\/\d+|\d+%\s*)/;
|
|
35
33
|
const PARENTHESES_PATTERN = /\((\d+)\)/;
|
|
@@ -40,9 +38,6 @@ const NUMBER_PATTERN = /\b(\d+)\b/;
|
|
|
40
38
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
41
39
|
const settingsPath = path.join(home, ".pi/agent/settings.json");
|
|
42
40
|
|
|
43
|
-
// Command-Driven I/O State:
|
|
44
|
-
// Loaded ONCE synchronously during extension loading, thereafter handled entirely in memory.
|
|
45
|
-
// Disk I/O only occurs when the user triggers the "/beautify" selection command.
|
|
46
41
|
let currentStyle = "minimal";
|
|
47
42
|
|
|
48
43
|
try {
|
|
@@ -50,34 +45,14 @@ try {
|
|
|
50
45
|
const data = fs.readFileSync(settingsPath, "utf-8");
|
|
51
46
|
const config = JSON.parse(data);
|
|
52
47
|
if (config.beautifier && typeof config.beautifier.style === "string") {
|
|
53
|
-
|
|
48
|
+
const saved = config.beautifier.style;
|
|
49
|
+
currentStyle = (VALID_STYLES as readonly string[]).includes(saved) ? saved : "minimal";
|
|
54
50
|
}
|
|
55
51
|
}
|
|
56
|
-
} catch (
|
|
52
|
+
} catch (_e) {
|
|
57
53
|
// Graceful fallback to default in case of JSON parse or read errors
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
// Google playful multi-color letter colorizer with type safeguards and memoization
|
|
61
|
-
export function googleColorize(text: string, theme: any): string {
|
|
62
|
-
if (!theme || typeof theme.fg !== "function") return text;
|
|
63
|
-
|
|
64
|
-
const cached = googleColorizeCache.get(text);
|
|
65
|
-
if (cached !== undefined) return cached;
|
|
66
|
-
|
|
67
|
-
// Google's brand colors cycle: Blue, Red, Yellow, Green
|
|
68
|
-
const colors = ["accent", "error", "warning", "success"];
|
|
69
|
-
const colorized = text
|
|
70
|
-
.split("")
|
|
71
|
-
.map((char, index) => {
|
|
72
|
-
const color = colors[index % colors.length];
|
|
73
|
-
return theme.fg(color, char);
|
|
74
|
-
})
|
|
75
|
-
.join("");
|
|
76
|
-
|
|
77
|
-
googleColorizeCache.set(text, colorized);
|
|
78
|
-
return colorized;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
56
|
// Clean status text and map state and details
|
|
82
57
|
export function beautifyStatus(
|
|
83
58
|
key: string,
|
|
@@ -86,16 +61,14 @@ export function beautifyStatus(
|
|
|
86
61
|
style: string
|
|
87
62
|
): string | undefined {
|
|
88
63
|
if (originalVal === undefined) return undefined;
|
|
89
|
-
if (style === "off") return originalVal;
|
|
64
|
+
if (style === "off") return originalVal;
|
|
90
65
|
|
|
91
66
|
try {
|
|
92
|
-
// Fast-path: bypass Regex search if no ESC or CSI bytes exist to reduce hot redraw latency
|
|
93
67
|
const hasAnsi = originalVal.includes("\u001B") || originalVal.includes("\u009B");
|
|
94
68
|
const cleanVal = hasAnsi ? originalVal.replace(ANSI_STRIP_REGEX, "").trim() : originalVal.trim();
|
|
95
69
|
|
|
96
70
|
if (cleanVal === "") return "";
|
|
97
71
|
|
|
98
|
-
// Safely bypass values that have no status signifiers (such as logs or text widgets)
|
|
99
72
|
if (!STATUS_PATTERN.test(cleanVal)) {
|
|
100
73
|
return originalVal;
|
|
101
74
|
}
|
|
@@ -111,7 +84,6 @@ export function beautifyStatus(
|
|
|
111
84
|
}
|
|
112
85
|
name = name.replace(NAME_SUFFIX_PATTERN, "").toLowerCase();
|
|
113
86
|
}
|
|
114
|
-
// Optimization (Finding 5): Use visual cell width calculations rather than standard .length for CJK/emojis
|
|
115
87
|
const visualName = padToVisualWidth(sliceToVisualWidth(name, 10), 10);
|
|
116
88
|
|
|
117
89
|
// 2. Extract metrics/progress details safely (e.g. (2) or fraction 2/5 or count)
|
|
@@ -141,7 +113,6 @@ export function beautifyStatus(
|
|
|
141
113
|
valLower.includes("β") ||
|
|
142
114
|
valLower.includes("offline") ||
|
|
143
115
|
valLower.includes("error") ||
|
|
144
|
-
valLower.includes("ζͺη»ε½") ||
|
|
145
116
|
valLower.includes("failed")
|
|
146
117
|
) {
|
|
147
118
|
state = "error";
|
|
@@ -160,8 +131,7 @@ export function beautifyStatus(
|
|
|
160
131
|
valLower.includes("β
") ||
|
|
161
132
|
valLower.includes("β") ||
|
|
162
133
|
valLower.includes("online") ||
|
|
163
|
-
valLower.includes("success")
|
|
164
|
-
valLower.includes("ε·²θΏζ₯")
|
|
134
|
+
valLower.includes("success")
|
|
165
135
|
) {
|
|
166
136
|
state = "success";
|
|
167
137
|
} else {
|
|
@@ -176,39 +146,8 @@ export function beautifyStatus(
|
|
|
176
146
|
const hasThemeFg = theme && typeof theme.fg === "function";
|
|
177
147
|
const coloredIndicator = hasThemeFg ? theme.fg(state, char) : char;
|
|
178
148
|
|
|
179
|
-
// 5. Render layout presets
|
|
149
|
+
// 5. Render layout presets (3 curated baseline layouts: minimal, glass, glow)
|
|
180
150
|
switch (style) {
|
|
181
|
-
case "apple": {
|
|
182
|
-
const separator = hasThemeFg ? theme.fg("dim", " β ") : " β ";
|
|
183
|
-
const dimmedName = hasThemeFg ? theme.fg("dim", visualName) : visualName;
|
|
184
|
-
const detailStr = details ? ` (${details})` : "";
|
|
185
|
-
return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
|
|
186
|
-
}
|
|
187
|
-
case "openai": {
|
|
188
|
-
const spirograph = hasThemeFg ? theme.fg("success", "β") : "β";
|
|
189
|
-
const detailStr = details ? ` [${details}]` : "";
|
|
190
|
-
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
191
|
-
return `${spirograph} ${dimmedName}${detailStr}`;
|
|
192
|
-
}
|
|
193
|
-
case "anthropic": {
|
|
194
|
-
const organicStar = "β¦";
|
|
195
|
-
const coloredStar = hasThemeFg ? theme.fg(state, organicStar) : organicStar;
|
|
196
|
-
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
197
|
-
const detailStr = details ? ` β ${details}` : "";
|
|
198
|
-
return `${dimmedName} ${coloredStar}${detailStr}`;
|
|
199
|
-
}
|
|
200
|
-
case "microsoft": {
|
|
201
|
-
const windowsLogo = hasThemeFg ? theme.fg("accent", "β") : "β";
|
|
202
|
-
const detailStr = details ? ` β ${details}` : "";
|
|
203
|
-
const prefix = hasThemeFg ? theme.fg("dim", "[") : "[";
|
|
204
|
-
const suffix = hasThemeFg ? theme.fg("dim", "]") : "]";
|
|
205
|
-
return `${windowsLogo} ${prefix}${visualName}${suffix} ${coloredIndicator}${detailStr}`;
|
|
206
|
-
}
|
|
207
|
-
case "google": {
|
|
208
|
-
const coloredName = googleColorize(visualName, theme);
|
|
209
|
-
const detailStr = details ? `:${details}` : "";
|
|
210
|
-
return `${coloredName} β ${coloredIndicator}${detailStr}`;
|
|
211
|
-
}
|
|
212
151
|
case "glass": {
|
|
213
152
|
const prefix = hasThemeFg ? theme.fg("dim", "β ") : "β ";
|
|
214
153
|
const suffix = hasThemeFg ? theme.fg("dim", " β") : " β";
|
|
@@ -223,99 +162,108 @@ export function beautifyStatus(
|
|
|
223
162
|
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
224
163
|
return `${dimmedName} ${coloredBadge}`;
|
|
225
164
|
}
|
|
226
|
-
case "
|
|
227
|
-
|
|
228
|
-
const suffix = hasThemeFg ? theme.fg("dim", " \u2398") : " \u2398";
|
|
229
|
-
const separator = hasThemeFg ? theme.fg("dim", " | ") : " | ";
|
|
230
|
-
const detailStr = details ? `${separator}${details}` : "";
|
|
231
|
-
return `${visualName}${prefix}${coloredIndicator}${detailStr}${suffix}`;
|
|
232
|
-
}
|
|
233
|
-
case "minimal": {
|
|
165
|
+
case "minimal":
|
|
166
|
+
default: {
|
|
234
167
|
const separator = hasThemeFg ? theme.fg("dim", " β― ") : " β― ";
|
|
235
168
|
const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
|
|
236
169
|
const detailStr = details ? ` (${details})` : "";
|
|
237
170
|
return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
|
|
238
171
|
}
|
|
239
|
-
default:
|
|
240
|
-
return originalVal;
|
|
241
172
|
}
|
|
242
|
-
} catch (
|
|
243
|
-
// Resilience fallback: prevent rendering exceptions from ever crashing UI updates
|
|
173
|
+
} catch (_error) {
|
|
244
174
|
return originalVal;
|
|
245
175
|
}
|
|
246
176
|
}
|
|
247
177
|
|
|
248
178
|
/**
|
|
249
|
-
* Calculates the visual column cell width of a string
|
|
250
|
-
* full-width Unicode characters, East Asian
|
|
179
|
+
* Calculates the visual column cell width of a string, taking into account
|
|
180
|
+
* full-width Unicode characters, East Asian ideographs, Kana, Hangul, emojis, and zero-width joiners.
|
|
251
181
|
*/
|
|
252
182
|
export function getStringWidth(str: string): number {
|
|
253
183
|
if (!str) return 0;
|
|
254
|
-
// Strip ANSI escape sequences & control codes before counting visual width
|
|
255
184
|
const clean = str.replace(ANSI_STRIP_REGEX, "");
|
|
256
185
|
let width = 0;
|
|
257
|
-
|
|
258
|
-
|
|
186
|
+
|
|
187
|
+
for (const char of clean) {
|
|
188
|
+
const code = char.codePointAt(0) || 0;
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
code === 0x200d ||
|
|
192
|
+
code === 0xfe0f ||
|
|
193
|
+
code === 0xfe0e ||
|
|
194
|
+
(code >= 0x0300 && code <= 0x036f) ||
|
|
195
|
+
(code >= 0x200b && code <= 0x200f)
|
|
196
|
+
) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
259
200
|
if (isFullWidth(code)) {
|
|
260
201
|
width += 2;
|
|
261
202
|
} else {
|
|
262
203
|
width += 1;
|
|
263
204
|
}
|
|
264
205
|
}
|
|
206
|
+
|
|
265
207
|
return width;
|
|
266
208
|
}
|
|
267
209
|
|
|
268
210
|
/**
|
|
269
211
|
* Checks if a character unicode code point is full-width (East Asian Width).
|
|
270
212
|
*/
|
|
271
|
-
function isFullWidth(code: number): boolean {
|
|
213
|
+
export function isFullWidth(code: number): boolean {
|
|
272
214
|
if (isNaN(code)) return false;
|
|
273
215
|
return (
|
|
274
|
-
code >= 0x1100 &&
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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))
|
|
216
|
+
(code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
|
|
217
|
+
code === 0x2329 ||
|
|
218
|
+
code === 0x232a ||
|
|
219
|
+
(code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || // CJK Radicals
|
|
220
|
+
(code >= 0x3040 && code <= 0x309f) || // Hiragana
|
|
221
|
+
(code >= 0x30a0 && code <= 0x30ff) || // Katakana
|
|
222
|
+
(code >= 0x3250 && code <= 0x4dbf) || // CJK Extension A
|
|
223
|
+
(code >= 0x4e00 && code <= 0xa4c6) || // CJK Unified Ideographs .. Yi
|
|
224
|
+
(code >= 0xa960 && code <= 0xa97c) || // Hangul Jamo Extended-A
|
|
225
|
+
(code >= 0xac00 && code <= 0xd7af) || // Hangul Syllables
|
|
226
|
+
(code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
|
|
227
|
+
(code >= 0xfe10 && code <= 0xfe19) || // Vertical Forms
|
|
228
|
+
(code >= 0xfe30 && code <= 0xfe6b) || // CJK Compatibility Forms
|
|
229
|
+
(code >= 0xff01 && code <= 0xff60) || // Fullwidth Forms
|
|
230
|
+
(code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Symbols
|
|
231
|
+
(code >= 0x1f300 && code <= 0x1f9ff) || // Emojis & Pictographs
|
|
232
|
+
(code >= 0x1f600 && code <= 0x1f64f) || // Emoticons
|
|
233
|
+
(code >= 0x1f680 && code <= 0x1f6ff) || // Transport & Map
|
|
234
|
+
(code >= 0x2600 && code <= 0x27bf) || // Dingbats & Misc
|
|
235
|
+
(code >= 0x1fa70 && code <= 0x1faff) || // Symbols Extended-A
|
|
236
|
+
(code >= 0x20000 && code <= 0x3fffd) // CJK Extensions B/C/D
|
|
301
237
|
);
|
|
302
238
|
}
|
|
303
239
|
|
|
304
240
|
/**
|
|
305
|
-
* Slices a string to a maximum visual cell width.
|
|
241
|
+
* Slices a string to a maximum visual cell width without cutting surrogate pairs in half.
|
|
306
242
|
*/
|
|
307
243
|
export function sliceToVisualWidth(str: string, maxWidth: number): string {
|
|
308
244
|
let width = 0;
|
|
309
245
|
let sliced = "";
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
246
|
+
|
|
247
|
+
for (const char of str) {
|
|
248
|
+
const code = char.codePointAt(0) || 0;
|
|
249
|
+
const charWidth =
|
|
250
|
+
code === 0x200d ||
|
|
251
|
+
code === 0xfe0f ||
|
|
252
|
+
code === 0xfe0e ||
|
|
253
|
+
(code >= 0x0300 && code <= 0x036f) ||
|
|
254
|
+
(code >= 0x200b && code <= 0x200f)
|
|
255
|
+
? 0
|
|
256
|
+
: isFullWidth(code)
|
|
257
|
+
? 2
|
|
258
|
+
: 1;
|
|
259
|
+
|
|
313
260
|
if (width + charWidth > maxWidth) {
|
|
314
261
|
break;
|
|
315
262
|
}
|
|
316
263
|
width += charWidth;
|
|
317
|
-
sliced +=
|
|
264
|
+
sliced += char;
|
|
318
265
|
}
|
|
266
|
+
|
|
319
267
|
return sliced;
|
|
320
268
|
}
|
|
321
269
|
|
|
@@ -334,21 +282,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
334
282
|
pi.on("session_start", async (_event, ctx) => {
|
|
335
283
|
if (!ctx.hasUI || !ctx.ui) return;
|
|
336
284
|
|
|
337
|
-
// Flush stale statuses from previous sessions to prevent memory leaks and clear color cache
|
|
338
285
|
originalStatuses.clear();
|
|
339
|
-
googleColorizeCache.clear();
|
|
340
286
|
lastRenderedStatus.clear();
|
|
341
287
|
|
|
342
288
|
const originalSetStatus = ctx.ui.setStatus;
|
|
343
289
|
if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
|
|
344
290
|
const wrapped = function (key: string, value: string | undefined) {
|
|
345
|
-
// Track original statuses for dynamic updates
|
|
346
291
|
originalStatuses.set(key, value);
|
|
347
292
|
|
|
348
|
-
// Uses in-memory synchronization (zero file I/O overhead on render pipeline ticks)
|
|
349
293
|
const beautified = beautifyStatus(key, value, ctx.ui.theme, currentStyle);
|
|
350
294
|
|
|
351
|
-
// Dirty-checking (Finding 6): Skip redundant stdout paint updates
|
|
352
295
|
const cacheKey = `${key}:${currentStyle}`;
|
|
353
296
|
if (lastRenderedStatus.get(cacheKey) === beautified) {
|
|
354
297
|
return;
|
|
@@ -364,66 +307,64 @@ export default function (pi: ExtensionAPI) {
|
|
|
364
307
|
|
|
365
308
|
// Dynamic status bar style controller registry
|
|
366
309
|
pi.registerCommand("beautify", {
|
|
367
|
-
description: "Choose TUI status bar beautification style",
|
|
368
|
-
handler: async (
|
|
310
|
+
description: "Choose TUI status bar beautification style or generate a custom style with AI",
|
|
311
|
+
handler: async (_args, ctx) => {
|
|
369
312
|
if (!ctx.hasUI || !ctx.ui) {
|
|
370
313
|
return;
|
|
371
314
|
}
|
|
372
315
|
|
|
373
316
|
const styleOptions = [
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"microsoft (Microsoft Fluent - e.g. β [name] β)",
|
|
380
|
-
"glass (Charm Glass - e.g. β name β β)",
|
|
381
|
-
"glow (Glow Badge - e.g. name [ β ])",
|
|
382
|
-
"matrix (Retro Matrix - e.g. name β¦ β | 2 β¦)",
|
|
383
|
-
"off (use raw output)",
|
|
317
|
+
"custom - [AI Prompt Recipe] Let your agent generate or tailor your own style",
|
|
318
|
+
"minimal - Clean chevron indicator (e.g. name β― β)",
|
|
319
|
+
"glass - Framed capsule badge (e.g. β name β β)",
|
|
320
|
+
"glow - High-contrast inverse pill (e.g. name [ β ])",
|
|
321
|
+
"off - Pass through raw status output",
|
|
384
322
|
];
|
|
385
323
|
|
|
386
324
|
const choice = await ctx.ui.select(`Choose status style (Current: ${currentStyle}):`, styleOptions);
|
|
387
325
|
|
|
388
|
-
if (choice)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
326
|
+
if (!choice) return;
|
|
327
|
+
|
|
328
|
+
const styleKey = choice.split(" ")[0].trim();
|
|
329
|
+
|
|
330
|
+
if (styleKey === "custom") {
|
|
331
|
+
const promptRecipe = [
|
|
332
|
+
"Please customize my Pi TUI status bar style in `tui-status-beautifier.ts`.",
|
|
333
|
+
"1. Inspect the installed extension (`~/.pi/agent/npm/node_modules/pi-tui-status-beautifier/extensions/tui-status-beautifier.ts` or local repo).",
|
|
334
|
+
"2. In `beautifyStatus()`, design a bespoke status bar layout matching my taste (custom separators, status glyphs, brackets, and theme colors: accent/success/warning/error/muted/dim).",
|
|
335
|
+
"3. Apply the new style as default and show me a preview of the updated format."
|
|
336
|
+
].join("\n");
|
|
337
|
+
|
|
338
|
+
if (typeof (ctx.ui as any).setEditorText === "function") {
|
|
339
|
+
(ctx.ui as any).setEditorText(promptRecipe);
|
|
340
|
+
ctx.ui.notify("Custom style prompt loaded into editor. Edit your preferences and press Enter!", "info");
|
|
341
|
+
} else {
|
|
342
|
+
ctx.ui.notify(promptRecipe, "info");
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if ((VALID_STYLES as readonly string[]).includes(styleKey)) {
|
|
348
|
+
currentStyle = styleKey;
|
|
349
|
+
lastRenderedStatus.clear();
|
|
350
|
+
ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
|
|
351
|
+
|
|
352
|
+
for (const [key, val] of originalStatuses.entries()) {
|
|
353
|
+
ctx.ui.setStatus(key, val);
|
|
354
|
+
}
|
|
414
355
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} catch (e) {
|
|
425
|
-
// Silently fail disk write to avoid crashing active user sessions
|
|
356
|
+
try {
|
|
357
|
+
if (fs.existsSync(settingsPath)) {
|
|
358
|
+
const data = fs.readFileSync(settingsPath, "utf-8");
|
|
359
|
+
const config = JSON.parse(data);
|
|
360
|
+
if (!config.beautifier) config.beautifier = {};
|
|
361
|
+
config.beautifier.style = styleKey;
|
|
362
|
+
const tempSettings = `${settingsPath}.tmp.${process.pid}.${Date.now()}`;
|
|
363
|
+
fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
|
|
364
|
+
fs.renameSync(tempSettings, settingsPath);
|
|
426
365
|
}
|
|
366
|
+
} catch (_e) {
|
|
367
|
+
// Silently ignore disk write errors
|
|
427
368
|
}
|
|
428
369
|
}
|
|
429
370
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tui-status-beautifier",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Minimalist status bar formatter & AI-customizable layout engine for Pi TUI.",
|
|
5
5
|
"main": "extensions/tui-status-beautifier.ts",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package"
|