pi-tui-status-beautifier 1.1.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 +40 -0
- package/extensions/tui-status-beautifier.spec.ts +77 -0
- package/extensions/tui-status-beautifier.ts +326 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# π¨ Pi TUI Status Beautifier
|
|
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.
|
|
4
|
+
|
|
5
|
+
## π Installation & Usage (Recommended)
|
|
6
|
+
|
|
7
|
+
Since this package is published on GitHub, you can install it instantly in the Pi TUI command environment using the following single line:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install git:github.com/3ZEROS12/pi-tui-status-beautifier
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Enable/Disable Styles
|
|
14
|
+
Once installed, open the Pi TUI terminal and run the dynamic interactive switch command:
|
|
15
|
+
```bash
|
|
16
|
+
/beautify
|
|
17
|
+
```
|
|
18
|
+
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`).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## π¨ Layout Themes
|
|
23
|
+
|
|
24
|
+
* **Minimal (Default)**: `wechat β― β` | `plan β― β (2)`
|
|
25
|
+
* **Glass (ζ―η»η)**: `β wechat β β`
|
|
26
|
+
* **Glow Badge**: `wechat [ β ]`
|
|
27
|
+
* **Matrix (ζΊζΆεηΊΏζ‘)**: `wechat β¦ β β¦`
|
|
28
|
+
|
|
29
|
+
## π Tech & Brand Themes
|
|
30
|
+
|
|
31
|
+
* **Mac Cupertino (`apple`)**: Minimal vertical divider with native traffic-light state dots.
|
|
32
|
+
* **ChatGPT (`openai`)**: Star/spirograph symbol `β` wrapping plugin indicators in OpenAI Cyan.
|
|
33
|
+
* **Claude Humanist (`anthropic`)**: Artful 4-corner Claude star `β¦` styling.
|
|
34
|
+
* **Fluent Microsoft (`microsoft`)**: Flat terminal tile layout using Microsoft Fluent block `β`.
|
|
35
|
+
* **Material Google (`google`)**: Google multi-color letters rendering index name values.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## π οΈ Contribution & Development
|
|
40
|
+
The core logic resides in `extensions/tui-status-beautifier.ts`. All status mapping is completely memory-cached to eliminate Paint Loop I/O bottlenecks.
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
// Mapping of internal package names to clean human-readable TUI status names
|
|
6
|
+
const COMMON_NAMES: Record<string, string> = {
|
|
7
|
+
"plannotator": "plan",
|
|
8
|
+
"plannotator-review": "plan",
|
|
9
|
+
"pi-mcp-adapter": "mcp",
|
|
10
|
+
"subagent-slash": "subagent",
|
|
11
|
+
"subagent-slash-text": "subagent",
|
|
12
|
+
"pi-wechat-assistant": "wechat",
|
|
13
|
+
"wechat-assistant": "wechat"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Store original statuses to allow dynamic redrawing on configuration changes.
|
|
17
|
+
// Cleaned up on session restart to prevent memory leaks from replaced sessions.
|
|
18
|
+
const originalStatuses = new Map<string, string | undefined>();
|
|
19
|
+
|
|
20
|
+
// Memoization cache for Google colorized text to reduce GC overhead and theme calls.
|
|
21
|
+
const googleColorizeCache = new Map<string, string>();
|
|
22
|
+
|
|
23
|
+
// Safe, non-backtracking ReDoS-mitigated ANSI escape sequence matcher (supporting ; and :)
|
|
24
|
+
const ANSI_STRIP_REGEX = /[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g;
|
|
25
|
+
|
|
26
|
+
// Hoisted regex patterns to prevent instantiation and GC overhead in the render hot-path
|
|
27
|
+
const STATUS_PATTERN = /[π’π΄π‘βͺβΈβ
βββ?βββββπ]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected/i;
|
|
28
|
+
const NAME_SUFFIX_PATTERN = /-(extension|plugin|assistant|adapter|slash-text|slash|text|widget)$/gi;
|
|
29
|
+
const FRACTION_PATTERN = /(\d+\/\d+|\d+%\s*)/;
|
|
30
|
+
const PARENTHESES_PATTERN = /\((\d+)\)/;
|
|
31
|
+
const BRACKET_PATTERN = /\[(\d+)\]/;
|
|
32
|
+
const NUMBER_PATTERN = /\b(\d+)\b/;
|
|
33
|
+
|
|
34
|
+
// Load settings path
|
|
35
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
36
|
+
const settingsPath = path.join(home, ".pi/agent/settings.json");
|
|
37
|
+
|
|
38
|
+
// Command-Driven I/O State:
|
|
39
|
+
// Loaded ONCE synchronously during extension loading, thereafter handled entirely in memory.
|
|
40
|
+
// Disk I/O only occurs when the user triggers the "/beautify" selection command.
|
|
41
|
+
let currentStyle = "minimal";
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(settingsPath)) {
|
|
45
|
+
const data = fs.readFileSync(settingsPath, "utf-8");
|
|
46
|
+
const config = JSON.parse(data);
|
|
47
|
+
if (config.beautifier && typeof config.beautifier.style === "string") {
|
|
48
|
+
currentStyle = config.beautifier.style;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Graceful fallback to default in case of JSON parse or read errors
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Google playful multi-color letter colorizer with type safeguards and memoization
|
|
56
|
+
export function googleColorize(text: string, theme: any): string {
|
|
57
|
+
if (!theme || typeof theme.fg !== "function") return text;
|
|
58
|
+
|
|
59
|
+
const cached = googleColorizeCache.get(text);
|
|
60
|
+
if (cached !== undefined) return cached;
|
|
61
|
+
|
|
62
|
+
// Google's brand colors cycle: Blue, Red, Yellow, Green
|
|
63
|
+
const colors = ["accent", "error", "warning", "success"];
|
|
64
|
+
const colorized = text.split("")
|
|
65
|
+
.map((char, index) => {
|
|
66
|
+
const color = colors[index % colors.length];
|
|
67
|
+
return theme.fg(color, char);
|
|
68
|
+
})
|
|
69
|
+
.join("");
|
|
70
|
+
|
|
71
|
+
googleColorizeCache.set(text, colorized);
|
|
72
|
+
return colorized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Clean status text and map state and details
|
|
76
|
+
export function beautifyStatus(key: string, originalVal: string | undefined, theme: any, style: string): string | undefined {
|
|
77
|
+
if (originalVal === undefined) return undefined;
|
|
78
|
+
if (style === "off") return originalVal; // Pass through untouched if disabled
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Fast-path: bypass Regex search if no ESC or CSI bytes exist to reduce hot redraw latency
|
|
82
|
+
const hasAnsi = originalVal.includes("\u001B") || originalVal.includes("\u009B");
|
|
83
|
+
const cleanVal = hasAnsi
|
|
84
|
+
? originalVal.replace(ANSI_STRIP_REGEX, "").trim()
|
|
85
|
+
: originalVal.trim();
|
|
86
|
+
|
|
87
|
+
if (cleanVal === "") return "";
|
|
88
|
+
|
|
89
|
+
// Safely bypass values that have no status signifiers (such as logs or text widgets)
|
|
90
|
+
if (!STATUS_PATTERN.test(cleanVal)) {
|
|
91
|
+
return originalVal;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 1. Get display name
|
|
95
|
+
let name = key.toLowerCase();
|
|
96
|
+
if (COMMON_NAMES[name]) {
|
|
97
|
+
name = COMMON_NAMES[name];
|
|
98
|
+
} else {
|
|
99
|
+
const lastSlash = name.lastIndexOf("/");
|
|
100
|
+
if (lastSlash !== -1) {
|
|
101
|
+
name = name.slice(lastSlash + 1);
|
|
102
|
+
}
|
|
103
|
+
name = name
|
|
104
|
+
.replace(NAME_SUFFIX_PATTERN, "")
|
|
105
|
+
.toLowerCase();
|
|
106
|
+
name = name.slice(0, 10);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. Extract metrics/progress details safely (e.g. (2) or fraction 2/5 or count)
|
|
110
|
+
let details: string | undefined;
|
|
111
|
+
const fractionMatch = cleanVal.match(FRACTION_PATTERN);
|
|
112
|
+
if (fractionMatch) {
|
|
113
|
+
details = fractionMatch[1];
|
|
114
|
+
} else {
|
|
115
|
+
const countMatch = cleanVal.match(PARENTHESES_PATTERN) || cleanVal.match(BRACKET_PATTERN);
|
|
116
|
+
if (countMatch) {
|
|
117
|
+
details = countMatch[1];
|
|
118
|
+
} else {
|
|
119
|
+
const numberMatch = cleanVal.match(NUMBER_PATTERN);
|
|
120
|
+
if (numberMatch) {
|
|
121
|
+
details = numberMatch[1];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. Determine state based on colors, icons, emojis or text keywords in original val
|
|
127
|
+
let state: "success" | "warning" | "error" | "accent" = "success";
|
|
128
|
+
const valLower = cleanVal.toLowerCase();
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
valLower.includes("β") ||
|
|
132
|
+
valLower.includes("π΄") ||
|
|
133
|
+
valLower.includes("β") ||
|
|
134
|
+
valLower.includes("offline") ||
|
|
135
|
+
valLower.includes("error") ||
|
|
136
|
+
valLower.includes("ζͺη»ε½") ||
|
|
137
|
+
valLower.includes("failed")
|
|
138
|
+
) {
|
|
139
|
+
state = "error";
|
|
140
|
+
} else if (
|
|
141
|
+
valLower.includes("βΈ") ||
|
|
142
|
+
valLower.includes("π‘") ||
|
|
143
|
+
valLower.includes("?") ||
|
|
144
|
+
valLower.includes("warning") ||
|
|
145
|
+
valLower.includes("pause") ||
|
|
146
|
+
valLower.includes("planning") ||
|
|
147
|
+
valLower.includes("plan")
|
|
148
|
+
) {
|
|
149
|
+
state = "warning";
|
|
150
|
+
} else if (
|
|
151
|
+
valLower.includes("π’") ||
|
|
152
|
+
valLower.includes("β
") ||
|
|
153
|
+
valLower.includes("β") ||
|
|
154
|
+
valLower.includes("online") ||
|
|
155
|
+
valLower.includes("success") ||
|
|
156
|
+
valLower.includes("ε·²θΏζ₯")
|
|
157
|
+
) {
|
|
158
|
+
state = "success";
|
|
159
|
+
} else {
|
|
160
|
+
state = name === "plan" ? "accent" : "success";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 4. Map state to target glyph shapes
|
|
164
|
+
let char = "β";
|
|
165
|
+
if (state === "warning") char = "β";
|
|
166
|
+
else if (state === "error") char = "β²";
|
|
167
|
+
|
|
168
|
+
const hasThemeFg = theme && typeof theme.fg === "function";
|
|
169
|
+
const coloredIndicator = hasThemeFg ? theme.fg(state, char) : char;
|
|
170
|
+
|
|
171
|
+
// 5. Render layout presets
|
|
172
|
+
switch (style) {
|
|
173
|
+
case "apple": {
|
|
174
|
+
const separator = hasThemeFg ? theme.fg("dim", " β ") : " β ";
|
|
175
|
+
const dimmedName = hasThemeFg ? theme.fg("dim", name) : name;
|
|
176
|
+
const detailStr = details ? ` (${details})` : "";
|
|
177
|
+
return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
|
|
178
|
+
}
|
|
179
|
+
case "openai": {
|
|
180
|
+
const spirograph = hasThemeFg ? theme.fg("success", "β") : "β";
|
|
181
|
+
const detailStr = details ? ` [${details}]` : "";
|
|
182
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
|
|
183
|
+
return `${spirograph} ${dimmedName}${detailStr}`;
|
|
184
|
+
}
|
|
185
|
+
case "anthropic": {
|
|
186
|
+
const organicStar = "β¦";
|
|
187
|
+
const coloredStar = hasThemeFg ? theme.fg(state, organicStar) : organicStar;
|
|
188
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
|
|
189
|
+
const detailStr = details ? ` β ${details}` : "";
|
|
190
|
+
return `${dimmedName} ${coloredStar}${detailStr}`;
|
|
191
|
+
}
|
|
192
|
+
case "microsoft": {
|
|
193
|
+
const windowsLogo = hasThemeFg ? theme.fg("accent", "β") : "β";
|
|
194
|
+
const detailStr = details ? ` β ${details}` : "";
|
|
195
|
+
const prefix = hasThemeFg ? theme.fg("dim", "[") : "[";
|
|
196
|
+
const suffix = hasThemeFg ? theme.fg("dim", "]") : "]";
|
|
197
|
+
return `${windowsLogo} ${prefix}${name}${suffix} ${coloredIndicator}${detailStr}`;
|
|
198
|
+
}
|
|
199
|
+
case "google": {
|
|
200
|
+
const coloredName = googleColorize(name, theme);
|
|
201
|
+
const detailStr = details ? `:${details}` : "";
|
|
202
|
+
return `${coloredName} β ${coloredIndicator}${detailStr}`;
|
|
203
|
+
}
|
|
204
|
+
case "glass": {
|
|
205
|
+
const prefix = hasThemeFg ? theme.fg("dim", "β ") : "β ";
|
|
206
|
+
const suffix = hasThemeFg ? theme.fg("dim", " β") : " β";
|
|
207
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
|
|
208
|
+
const detailStr = details ? ` (${details})` : "";
|
|
209
|
+
return `${prefix}${dimmedName} ${coloredIndicator}${detailStr}${suffix}`;
|
|
210
|
+
}
|
|
211
|
+
case "glow": {
|
|
212
|
+
const label = ` ${char}${details ? ` ${details}` : ""} `;
|
|
213
|
+
const hasInverse = theme && typeof theme.inverse === "function";
|
|
214
|
+
const coloredBadge = (hasInverse && hasThemeFg)
|
|
215
|
+
? theme.inverse(theme.fg(state, label))
|
|
216
|
+
: `[${label}]`;
|
|
217
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
|
|
218
|
+
return `${dimmedName} ${coloredBadge}`;
|
|
219
|
+
}
|
|
220
|
+
case "matrix": {
|
|
221
|
+
const prefix = hasThemeFg ? theme.fg("dim", " β¦ ") : " β¦ ";
|
|
222
|
+
const suffix = hasThemeFg ? theme.fg("dim", " β¦") : " β¦";
|
|
223
|
+
const separator = hasThemeFg ? theme.fg("dim", " | ") : " | ";
|
|
224
|
+
const detailStr = details ? `${separator}${details}` : "";
|
|
225
|
+
return `${name}${prefix}${coloredIndicator}${detailStr}${suffix}`;
|
|
226
|
+
}
|
|
227
|
+
case "minimal": {
|
|
228
|
+
const separator = hasThemeFg ? theme.fg("dim", " β― ") : " β― ";
|
|
229
|
+
const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
|
|
230
|
+
const detailStr = details ? ` (${details})` : "";
|
|
231
|
+
return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
|
|
232
|
+
}
|
|
233
|
+
default:
|
|
234
|
+
return originalVal;
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// Resilience fallback: prevent rendering exceptions from ever crashing UI updates
|
|
238
|
+
return originalVal;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default function (pi: ExtensionAPI) {
|
|
243
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
244
|
+
if (!ctx.hasUI || !ctx.ui) return;
|
|
245
|
+
|
|
246
|
+
// Flush stale statuses from previous sessions to prevent memory leaks and clear color cache
|
|
247
|
+
originalStatuses.clear();
|
|
248
|
+
googleColorizeCache.clear();
|
|
249
|
+
|
|
250
|
+
const originalSetStatus = ctx.ui.setStatus;
|
|
251
|
+
if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
|
|
252
|
+
const wrapped = function (key: string, value: string | undefined) {
|
|
253
|
+
// Track original statuses for dynamic updates
|
|
254
|
+
originalStatuses.set(key, value);
|
|
255
|
+
|
|
256
|
+
// Uses in-memory synchronization (zero file I/O overhead on render pipeline ticks)
|
|
257
|
+
const beautified = beautifyStatus(key, value, ctx.ui.theme, currentStyle);
|
|
258
|
+
|
|
259
|
+
return originalSetStatus.call(ctx.ui, key, beautified);
|
|
260
|
+
};
|
|
261
|
+
(wrapped as any).__beautifierHooked = true;
|
|
262
|
+
ctx.ui.setStatus = wrapped;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Dynamic status bar style controller registry
|
|
267
|
+
pi.registerCommand("beautify", {
|
|
268
|
+
description: "Choose TUI status bar beautification style",
|
|
269
|
+
handler: async (args, ctx) => {
|
|
270
|
+
if (!ctx.hasUI || !ctx.ui) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const styleOptions = [
|
|
275
|
+
"minimal (Millennial Minimalist - e.g. name β― β)",
|
|
276
|
+
"apple (Apple Cupertino - e.g. name β β)",
|
|
277
|
+
"openai (OpenAI Spirograph - e.g. β name)",
|
|
278
|
+
"anthropic (Anthropic Claude - e.g. name β¦)",
|
|
279
|
+
"google (Google Playful - e.g. n-a-m-e β β)",
|
|
280
|
+
"microsoft (Microsoft Fluent - e.g. β [name] β)",
|
|
281
|
+
"glass (Charm Glass - e.g. β name β β)",
|
|
282
|
+
"glow (Glow Badge - e.g. name [ β ])",
|
|
283
|
+
"matrix (Retro Matrix - e.g. name β¦ β | 2 β¦)",
|
|
284
|
+
"off (use raw output)"
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const choice = await ctx.ui.select(
|
|
288
|
+
`Choose status style (Current: ${currentStyle}):`,
|
|
289
|
+
styleOptions
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (choice) {
|
|
293
|
+
const styleKey = choice.split(" ")[0].trim();
|
|
294
|
+
const validStyles = [
|
|
295
|
+
"apple", "openai", "anthropic", "google", "microsoft",
|
|
296
|
+
"minimal", "glass", "glow", "matrix", "off"
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
if (validStyles.includes(styleKey)) {
|
|
300
|
+
// 1. Instantly update in-memory caching
|
|
301
|
+
currentStyle = styleKey;
|
|
302
|
+
googleColorizeCache.clear(); // Clear cache when changing style/redrawing to ensure new colors are loaded
|
|
303
|
+
ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
|
|
304
|
+
|
|
305
|
+
// 2. Redraw TUI indicator widgets instantly on configuration event using cached data
|
|
306
|
+
for (const [key, val] of originalStatuses.entries()) {
|
|
307
|
+
ctx.ui.setStatus(key, val);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 3. Command-Driven Disk I/O: Persist settings to disk asynchronously outside core paint loop
|
|
311
|
+
try {
|
|
312
|
+
if (fs.existsSync(settingsPath)) {
|
|
313
|
+
const data = fs.readFileSync(settingsPath, "utf-8");
|
|
314
|
+
const config = JSON.parse(data);
|
|
315
|
+
if (!config.beautifier) config.beautifier = {};
|
|
316
|
+
config.beautifier.style = styleKey;
|
|
317
|
+
fs.writeFileSync(settingsPath, JSON.stringify(config, null, 2), "utf-8");
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// Silently fail disk write to avoid crashing active user sessions
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-tui-status-beautifier",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Premium brand & layout status bar beautifier for Pi terminal TUI.",
|
|
5
|
+
"main": "extensions/tui-status-beautifier.ts",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"pi": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./extensions/tui-status-beautifier.ts"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
16
|
+
},
|
|
17
|
+
"author": "Jason",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"lint": "eslint . --ext .ts",
|
|
22
|
+
"format": "prettier --write \"**/*.ts\""
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.11.0",
|
|
26
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
27
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
28
|
+
"eslint": "^8.56.0",
|
|
29
|
+
"prettier": "^3.2.5",
|
|
30
|
+
"typescript": "^5.3.3",
|
|
31
|
+
"vitest": "^1.2.2"
|
|
32
|
+
}
|
|
33
|
+
}
|