pi-tui-status-beautifier 1.1.3 β†’ 1.2.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/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,30 +162,21 @@ 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
179
+ * Calculates the visual column cell width of a string, taking into account
250
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 {
@@ -257,7 +187,6 @@ export function getStringWidth(str: string): number {
257
187
  for (const char of clean) {
258
188
  const code = char.codePointAt(0) || 0;
259
189
 
260
- // Zero-width characters (ZWJ, variation selectors, combining marks)
261
190
  if (
262
191
  code === 0x200d ||
263
192
  code === 0xfe0f ||
@@ -317,10 +246,16 @@ export function sliceToVisualWidth(str: string, maxWidth: number): string {
317
246
 
318
247
  for (const char of str) {
319
248
  const code = char.codePointAt(0) || 0;
320
- const charWidth = (
321
- code === 0x200d || code === 0xfe0f || code === 0xfe0e ||
322
- (code >= 0x0300 && code <= 0x036f) || (code >= 0x200b && code <= 0x200f)
323
- ) ? 0 : (isFullWidth(code) ? 2 : 1);
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;
324
259
 
325
260
  if (width + charWidth > maxWidth) {
326
261
  break;
@@ -347,21 +282,16 @@ export default function (pi: ExtensionAPI) {
347
282
  pi.on("session_start", async (_event, ctx) => {
348
283
  if (!ctx.hasUI || !ctx.ui) return;
349
284
 
350
- // Flush stale statuses from previous sessions to prevent memory leaks and clear color cache
351
285
  originalStatuses.clear();
352
- googleColorizeCache.clear();
353
286
  lastRenderedStatus.clear();
354
287
 
355
288
  const originalSetStatus = ctx.ui.setStatus;
356
289
  if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
357
290
  const wrapped = function (key: string, value: string | undefined) {
358
- // Track original statuses for dynamic updates
359
291
  originalStatuses.set(key, value);
360
292
 
361
- // Uses in-memory synchronization (zero file I/O overhead on render pipeline ticks)
362
293
  const beautified = beautifyStatus(key, value, ctx.ui.theme, currentStyle);
363
294
 
364
- // Dirty-checking (Finding 6): Skip redundant stdout paint updates
365
295
  const cacheKey = `${key}:${currentStyle}`;
366
296
  if (lastRenderedStatus.get(cacheKey) === beautified) {
367
297
  return;
@@ -377,68 +307,64 @@ export default function (pi: ExtensionAPI) {
377
307
 
378
308
  // Dynamic status bar style controller registry
379
309
  pi.registerCommand("beautify", {
380
- description: "Choose TUI status bar beautification style",
381
- handler: async (args, ctx) => {
310
+ description: "Choose TUI status bar beautification style or generate a custom style with AI",
311
+ handler: async (_args, ctx) => {
382
312
  if (!ctx.hasUI || !ctx.ui) {
383
313
  return;
384
314
  }
385
315
 
386
316
  const styleOptions = [
387
- "minimal (Millennial Minimalist - e.g. name ❯ ●)",
388
- "apple (Apple Cupertino - e.g. name β”‚ ●)",
389
- "openai (OpenAI Spirograph - e.g. ❂ name)",
390
- "anthropic (Anthropic Claude - e.g. name ✦)",
391
- "google (Google Playful - e.g. n-a-m-e βž” ●)",
392
- "microsoft (Microsoft Fluent - e.g. ⊞ [name] ●)",
393
- "glass (Charm Glass - e.g. β–• name ● ▏)",
394
- "glow (Glow Badge - e.g. name [ ● ])",
395
- "matrix (Retro Matrix - e.g. name β¦— ● | 2 ⦘)",
396
- "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",
397
322
  ];
398
323
 
399
324
  const choice = await ctx.ui.select(`Choose status style (Current: ${currentStyle}):`, styleOptions);
400
325
 
401
- if (choice) {
402
- const styleKey = choice.split(" ")[0].trim();
403
- const validStyles = [
404
- "apple",
405
- "openai",
406
- "anthropic",
407
- "google",
408
- "microsoft",
409
- "minimal",
410
- "glass",
411
- "glow",
412
- "matrix",
413
- "off",
414
- ];
415
-
416
- if (validStyles.includes(styleKey)) {
417
- // 1. Instantly update in-memory caching
418
- currentStyle = styleKey;
419
- googleColorizeCache.clear(); // Clear cache when changing style/redrawing to ensure new colors are loaded
420
- lastRenderedStatus.clear(); // Clear dirty checking cache to force repaint
421
- ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
422
-
423
- // 2. Redraw TUI indicator widgets instantly on configuration event using cached data
424
- for (const [key, val] of originalStatuses.entries()) {
425
- ctx.ui.setStatus(key, val);
426
- }
326
+ if (!choice) return;
327
+
328
+ const styleKey = choice.split(" ")[0].trim();
329
+
330
+ if (styleKey === "custom") {
331
+ const promptRecipe = [
332
+ "You are acting as my Pi TUI Status Bar designer.",
333
+ "1. Detect my primary language from our recent conversation history or environment locale.",
334
+ "2. In that EXACT primary language, proactively ask me what visual aesthetic I want for my status bar, and propose 3 distinct creative ideas (e.g. Cyberpunk Dot-Matrix, Warm Minimalist, High-Contrast Pill).",
335
+ "3. Wait for my preference or feedback, then inspect `tui-status-beautifier.ts` and apply the custom layout for me."
336
+ ].join("\n");
337
+
338
+ if (typeof (ctx.ui as any).setEditorText === "function") {
339
+ (ctx.ui as any).setEditorText(promptRecipe);
340
+ ctx.ui.notify("Press Enter to let your agent consult your style preference in your native language!", "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
+ }
427
355
 
428
- // 3. Command-Driven Disk I/O: Persist settings to disk atomically outside core paint loop
429
- try {
430
- if (fs.existsSync(settingsPath)) {
431
- const data = fs.readFileSync(settingsPath, "utf-8");
432
- const config = JSON.parse(data);
433
- if (!config.beautifier) config.beautifier = {};
434
- config.beautifier.style = styleKey;
435
- const tempSettings = `${settingsPath}.tmp.${process.pid}.${Date.now()}`;
436
- fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
437
- fs.renameSync(tempSettings, settingsPath);
438
- }
439
- } catch (e) {
440
- // 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);
441
365
  }
366
+ } catch (_e) {
367
+ // Silently ignore disk write errors
442
368
  }
443
369
  }
444
370
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-tui-status-beautifier",
3
- "version": "1.1.3",
4
- "description": "Premium brand & layout status bar beautifier for Pi terminal TUI.",
3
+ "version": "1.2.1",
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"