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 CHANGED
@@ -1,45 +1,31 @@
1
- # 🎨 Pi TUI Status Beautifier
1
+ # pi-tui-status-beautifier
2
2
 
3
- A premium status bar beautifier extension for the Pi Terminal TUI. Instantly transforms boring status lines into brand-inspired themes and clean layouts.
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
- ## πŸš€ Installation & Usage
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
- Install directly via the Pi package manager:
7
+ ## Installation
8
8
 
9
9
  ```bash
10
10
  pi install npm:pi-tui-status-beautifier
11
11
  ```
12
12
 
13
- Or install from git:
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
- ## 🎨 Layout Themes
15
+ Run `/beautify` inside Pi to switch status bar layouts or generate a custom style with your AI agent:
28
16
 
29
- * **Minimal (Default)**: `wechat ❯ ●` | `plan ❯ ● (2)`
30
- * **Glass (ζ―›ηŽ»η’ƒ)**: `β–• wechat ● ▏`
31
- * **Glow Badge**: `wechat [ ● ]`
32
- * **Matrix (ζœΊζžΆεŒηΊΏζ‘†)**: `wechat β¦— ● ⦘`
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
- ## 🍎 Tech & Brand Themes
23
+ ## Engineering Highlights
35
24
 
36
- * **Mac Cupertino (`apple`)**: Minimal vertical divider with native traffic-light state dots.
37
- * **ChatGPT (`openai`)**: Star/spirograph symbol `❂` wrapping plugin indicators in OpenAI Cyan.
38
- * **Claude Humanist (`anthropic`)**: Artful 4-corner Claude star `✦` styling.
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
- ## πŸ› οΈ Contribution & Development
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
- /[πŸŸ’πŸ”΄πŸŸ‘βšͺβΈβœ…βŒβœ“βœ—?βœ”βœ–β˜‘β˜β—†πŸ“‹]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected/i;
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
- currentStyle = config.beautifier.style;
48
+ const saved = config.beautifier.style;
49
+ currentStyle = (VALID_STYLES as readonly string[]).includes(saved) ? saved : "minimal";
54
50
  }
55
51
  }
56
- } catch (e) {
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; // Pass through untouched if disabled
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 "matrix": {
227
- const prefix = hasThemeFg ? theme.fg("dim", " \u2397 ") : " \u2397 ";
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 (error) {
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 (Finding 5), taking into account
250
- * full-width Unicode characters, East Asian characters, and emojis.
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
- for (let i = 0; i < clean.length; i++) {
258
- const code = clean.charCodeAt(i);
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
- (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))
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
- for (let i = 0; i < str.length; i++) {
311
- const code = str.charCodeAt(i);
312
- const charWidth = isFullWidth(code) ? 2 : 1;
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 += str.charAt(i);
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 (args, ctx) => {
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
- "minimal (Millennial Minimalist - e.g. name ❯ ●)",
375
- "apple (Apple Cupertino - e.g. name β”‚ ●)",
376
- "openai (OpenAI Spirograph - e.g. ❂ name)",
377
- "anthropic (Anthropic Claude - e.g. name ✦)",
378
- "google (Google Playful - e.g. n-a-m-e βž” ●)",
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
- const styleKey = choice.split(" ")[0].trim();
390
- const validStyles = [
391
- "apple",
392
- "openai",
393
- "anthropic",
394
- "google",
395
- "microsoft",
396
- "minimal",
397
- "glass",
398
- "glow",
399
- "matrix",
400
- "off",
401
- ];
402
-
403
- if (validStyles.includes(styleKey)) {
404
- // 1. Instantly update in-memory caching
405
- currentStyle = styleKey;
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
408
- ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
409
-
410
- // 2. Redraw TUI indicator widgets instantly on configuration event using cached data
411
- for (const [key, val] of originalStatuses.entries()) {
412
- ctx.ui.setStatus(key, val);
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
- // 3. Command-Driven Disk I/O: Persist settings to disk asynchronously outside core paint loop
416
- try {
417
- if (fs.existsSync(settingsPath)) {
418
- const data = fs.readFileSync(settingsPath, "utf-8");
419
- const config = JSON.parse(data);
420
- if (!config.beautifier) config.beautifier = {};
421
- config.beautifier.style = styleKey;
422
- fs.writeFileSync(settingsPath, JSON.stringify(config, null, 2), "utf-8");
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.1.2",
4
- "description": "Premium brand & layout status bar beautifier for Pi terminal TUI.",
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"