pi-tui-status-beautifier 1.7.2 → 1.7.4
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 +78 -120
- package/extensions/tui-status-beautifier.ts +115 -71
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# pi-tui-status-beautifier
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Minimalist status bar formatter & layout engine for Pi TUI.
|
|
4
|
+
|
|
5
|
+
Clean up fragmented extension status strings. Enjoy adaptive narrow-terminal folding and zero render-loop disk I/O, with zero telemetry hijacking.
|
|
5
6
|
|
|
6
7
|
[](https://www.npmjs.com/package/pi-tui-status-beautifier)
|
|
7
8
|
[](LICENSE)
|
|
@@ -22,24 +23,58 @@
|
|
|
22
23
|
└─────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
Install directly inside Pi:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pi install npm:pi-tui-status-beautifier
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Takes effect immediately. Type `/beautify` anytime to switch styles interactively.
|
|
35
|
+
|
|
25
36
|
---
|
|
26
37
|
|
|
27
|
-
## Why
|
|
38
|
+
## Core Value: Why Do Developers Need This?
|
|
28
39
|
|
|
29
|
-
As the Pi
|
|
40
|
+
As the Pi extension ecosystem grows, developers routinely install multiple tools (`plannotator`, `pi-mcp-adapter`, `toolflow`, `pi-anchor`, `pi-lingual`). The status bar quickly runs into four frictions:
|
|
30
41
|
|
|
31
|
-
1.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
4. **Render-Loop Disk Latency**: Reading configuration files inside high-frequency TUI animation loops introduces I/O thrashing and terminal sluggishness.
|
|
42
|
+
### 1. Inconsistent Syntax & Visual Clutter
|
|
43
|
+
Every extension authors status text independently. Some prefix long package names (`pi-mcp-adapter:`), some wrap counts in brackets (`[2]`), and others dump raw emojis. Inconsistent character metrics create visual distraction at the bottom of your screen.
|
|
44
|
+
`pi-tui-status-beautifier` strips package suffixes and normalizes raw text into uniform, structured micro-badges.
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
### 2. Narrow Terminal Line Wrapping
|
|
47
|
+
In split-pane layouts (tmux, WezTerm, or windows `< 80` columns), long status strings wrap onto multiple lines, shoving your prompt up and down and breaking typing flow.
|
|
48
|
+
The built-in column budget guard automatically collapses idle badges into `(+3 idle)` when width tightens, prioritizing errors and running tasks.
|
|
49
|
+
|
|
50
|
+
### 3. Never Hijacks Platform Telemetry
|
|
51
|
+
Many status bar plugins take a heavy-handed approach: completely replacing Pi's native footer via `ctx.ui.setFooter`. In doing so, they wipe out essential system metrics—git branches, token counts (`↑1.2k ↓3.4k`), cache read rates, and thinking levels (`• high`).
|
|
52
|
+
This extension defaults to a **non-intrusive micro-hook (`status-only`)**: it normalizes extension badges while leaving Pi's native system telemetry 100% intact.
|
|
53
|
+
|
|
54
|
+
### 4. Zero Render-Loop Disk I/O
|
|
55
|
+
Reading configuration files synchronously inside high-frequency TUI animation redraw loops causes disk contention and sluggish typing.
|
|
56
|
+
This extension loads settings into memory at startup and uses dirty-checking caches, performing **zero synchronous file reads** during TUI redraws.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## How It Works: The Action Matrix
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
/beautify (Open interactive theme switcher anytime)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Mode / Scenario | What You Do | What the Beautifier Does | What You See | Best For |
|
|
67
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
68
|
+
| **`stream`** *(Default)* | Install extensions normally | Strips package prefixes and normalizes badges | `plan ❯ ◆ 3 mcp ❯ ● ready anchor ❯ ⌖ 3` | Minimalist everyday pairing |
|
|
69
|
+
| **Narrow Split Panes** | tmux split width `< 80` cols | Preserves active badges, condenses idle items | `toolflow ❯ ⌬ running (2/5) (+3 idle)` | Split panes without line wrap |
|
|
70
|
+
| **`full-footer`** *(Panorama)* | Enable full footer mode | Formats unified two-line layout | Line 1: Git/Token/Model; Line 2: Normalized badges | Geek panoramic overview |
|
|
71
|
+
| **`off`** *(Clean Revert)* | Run `/beautify off` | Physically deletes `beautifier` key from `settings.json` | 100% native unformatted status bar, 0 residue | Zero-regret testing |
|
|
37
72
|
|
|
38
73
|
---
|
|
39
74
|
|
|
40
75
|
## Preset Gallery & Visual Themes
|
|
41
76
|
|
|
42
|
-
|
|
77
|
+
Type `/beautify` to switch styles interactively:
|
|
43
78
|
|
|
44
79
|
```text
|
|
45
80
|
1. stream (Default Minimalist Stream)
|
|
@@ -58,68 +93,34 @@ Switch styles interactively with `/beautify`:
|
|
|
58
93
|
chrome ❯ ● ready toolflow ❯ ⌬ 2/5 anchor ❯ ⌖ 3
|
|
59
94
|
|
|
60
95
|
6. custom (AI-Assisted Co-Design)
|
|
61
|
-
|
|
96
|
+
Describe your aesthetic in plain text; your agent generates 3 ASCII mockups.
|
|
62
97
|
```
|
|
63
98
|
|
|
64
99
|
---
|
|
65
100
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
### 1. Dual-Scope Formatting Architecture
|
|
69
|
-
When configuring `/beautify`, you choose between two execution scopes:
|
|
70
|
-
* **`status-only` (Default & Recommended)**: Only hooks `ctx.ui.setStatus` badges. Leaves Pi's native Git branch, working directory, context window percentages, token telemetry (`↑1.2k ↓3.4k R8.9k`), and thinking levels (`• high`) 100% native and untouched.
|
|
71
|
-
* **`full-footer` (Integrated Geek Footer)**: Replaces the entire bottom bar with a unified two-line layout:
|
|
72
|
-
* **Line 1**: Folder & Git branch on the left; token metrics, context cache hit rates, model identifier, and reasoning level on the right.
|
|
73
|
-
* **Line 2**: Normalized extension status badges with automatic column budgeting.
|
|
74
|
-
|
|
75
|
-
### 2. Adaptive Folding for Narrow Terminals (< 80 Cols)
|
|
76
|
-
In compact split panes, status items risk overflowing terminal boundaries. `pi-tui-status-beautifier` partitions active extensions into two priority tiers:
|
|
77
|
-
* **High-Priority (Active/Warning/Error)**: Badges displaying `running`, `working`, `starting`, `error`, `failed`, or `pause` are always preserved at full fidelity.
|
|
78
|
-
* **Low-Priority (Idle/Ready/Ok)**: When line width exceeds maximum available columns, idle badges are automatically condensed into a compact indicator:
|
|
79
|
-
```text
|
|
80
|
-
toolflow ❯ ⌬ running (2/5) plan ❯ ▲ error (+3 idle)
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### 3. Dedicated Extension Identity & Micro-Spinners
|
|
84
|
-
The engine strips internal package naming suffixes (`-extension`, `-plugin`, `-adapter`, `-slash-text`) and maps established ecosystem tools to clean semantic identities:
|
|
85
|
-
* `plannotator` ➔ `plan` (accent glyph `◆`)
|
|
86
|
-
* `pi-mcp-adapter` ➔ `mcp` (status dot `●`)
|
|
87
|
-
* `pi-control-chrome` ➔ `chrome` (status dot `●`)
|
|
88
|
-
* `pi-anchor` ➔ `anchor` (anchor glyph `⌖`)
|
|
89
|
-
* `toolflow` ➔ `toolflow` (flow glyph `⌬`)
|
|
90
|
-
* `pi-lingual` ➔ `lingual` (exchange glyph `⇄`)
|
|
91
|
-
* In-progress tasks (`running`, `working`, `...`) automatically trigger a 300ms terminal micro-spinner (`◐`, `◓`, `◑`, `◒`).
|
|
92
|
-
|
|
93
|
-
### 4. Zero-Ceremony AI Co-Design (`/beautify custom`)
|
|
94
|
-
Want a unique terminal aesthetic without manually editing code?
|
|
95
|
-
1. Run `/beautify` and select `custom`.
|
|
96
|
-
2. Enter your desired aesthetic in plain text (e.g. *"Cyberpunk neon dots"*, *"Nordic minimal pastels"*, *"Subtle dashed brackets"*).
|
|
97
|
-
3. The beautifier dispatches a structured prompt recipe to your active Coding Agent.
|
|
98
|
-
4. Your agent presents **3 distinct ASCII visual mockups** via interactive selection and writes the chosen configuration directly to `./.pi/settings.json`.
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## Under-The-Hood Engineering
|
|
103
|
-
|
|
104
|
-
### Unicode UAX #11 & Surrogate-Pair Safety
|
|
105
|
-
Terminal character counting based on `string.length` breaks when encountering emojis, Chinese/Japanese characters, or surrogate pairs. `pi-tui-status-beautifier` implements a standalone `codePointAt(0)` scanner (`src/extensions/tui-status-beautifier.ts`):
|
|
106
|
-
* Accurately measures East Asian Wide (2 visual columns), ASCII (1 column), and zero-width markers (0 columns).
|
|
107
|
-
* Explicitly ignores Zero-Width Joiners (`0x200d`), Emoji Variation Selectors (`0xfe0f`, `0xfe0e`), and combining diacritics (`0x0300..0x036f`).
|
|
108
|
-
* Implements `sliceToVisualWidth` to truncate long identifiers without severing multibyte surrogate pairs.
|
|
109
|
-
|
|
110
|
-
### Zero Render-Loop Disk I/O & Dirty-Checking
|
|
111
|
-
* **Cached Memory Reads**: Configuration is loaded into memory at startup. Normal TUI redraw loops perform **zero synchronous file reads (`fs.readFileSync`)**.
|
|
112
|
-
* **State Dirty-Checking**: Renders are cached per badge (`${key}:${currentStyle}`). If an extension sends an identical status string, formatting is skipped, avoiding redundant string allocations.
|
|
113
|
-
* **Atomic Settings Writes**: Style updates via `/beautify` use atomic file replacement (`writeFileSync(temp)` + `renameSync(temp, target)`), preventing corrupt settings files if the process is terminated mid-write.
|
|
101
|
+
## Engineering Highlights
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
- 🛡️ **Non-Destructive Dual Scopes**:
|
|
104
|
+
* `status-only` (Default & Recommended): Only hooks `ctx.ui.setStatus` badges. Preserves Pi's native Git branch, working directory, token counts, and thinking levels 100% untouched.
|
|
105
|
+
* `full-footer`: Formats the entire bottom bar with system telemetry on line 1 and badges on line 2.
|
|
106
|
+
- 📐 **Unicode UAX #11 & Surrogate-Pair Safety**: Built-in character scanner accurately measures East Asian Wide (2 cols), ASCII (1 col), and ANSI escape codes (0 cols), eliminating cursor jitter and column misalignments.
|
|
107
|
+
- ⚡ **Zero Render-Loop Disk I/O**: Loads configuration once into memory. Redraw loops perform zero synchronous disk reads (`fs.readFileSync`), using dirty-checking to skip duplicate renders.
|
|
108
|
+
- ◐ **Terminal Micro-Spinners**: Active background tasks (`running`, `working`) trigger a smooth 300ms spinner (`◐`, `◓`, `◑`, `◒`).
|
|
109
|
+
- 🗑️ **Zero-Trace Physical Cleanup (`off`)**: Turning the extension off restores all original status strings and deletes the `beautifier` block from `settings.json`, leaving zero orphaned configuration behind.
|
|
117
110
|
|
|
118
111
|
---
|
|
119
112
|
|
|
120
|
-
## Configuration
|
|
113
|
+
## Commands & Configuration
|
|
121
114
|
|
|
122
|
-
|
|
115
|
+
### Interactive Command
|
|
116
|
+
```bash
|
|
117
|
+
/beautify # Open interactive style picker
|
|
118
|
+
/beautify stream # Switch directly to stream preset
|
|
119
|
+
/beautify off # Revert to native status bar and delete config
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Settings Schema
|
|
123
|
+
Preferences persist in `./.pi/settings.json` (project-level) or `~/.pi/agent/settings.json` (global):
|
|
123
124
|
|
|
124
125
|
```json
|
|
125
126
|
{
|
|
@@ -130,77 +131,34 @@ Settings reside under `"beautifier"` in `./.pi/settings.json` (project-level) or
|
|
|
130
131
|
}
|
|
131
132
|
```
|
|
132
133
|
|
|
133
|
-
### Custom Style Schema (`style: "custom"`)
|
|
134
|
-
```json
|
|
135
|
-
{
|
|
136
|
-
"beautifier": {
|
|
137
|
-
"style": "custom",
|
|
138
|
-
"fullFooter": false,
|
|
139
|
-
"custom": {
|
|
140
|
-
"prefix": "",
|
|
141
|
-
"suffix": "",
|
|
142
|
-
"separator": " ❯ ",
|
|
143
|
-
"brackets": ["⦗", "⦘"],
|
|
144
|
-
"inverse": false,
|
|
145
|
-
"padName": false,
|
|
146
|
-
"glyphs": {
|
|
147
|
-
"success": "●",
|
|
148
|
-
"warning": "◌",
|
|
149
|
-
"error": "▲",
|
|
150
|
-
"accent": "◆"
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## Installation & Usage
|
|
160
|
-
|
|
161
|
-
### Install as a Pi Extension
|
|
162
|
-
```bash
|
|
163
|
-
# Recommended: Install from npm
|
|
164
|
-
pi install npm:pi-tui-status-beautifier
|
|
165
|
-
|
|
166
|
-
# Or install from GitHub
|
|
167
|
-
pi install git:github.com/3ZEROS12/pi-tui-status-beautifier
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Interactive Command
|
|
171
|
-
Type `/beautify` inside any active Pi session to open the interactive style selector:
|
|
172
|
-
```text
|
|
173
|
-
/beautify
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Development & Testing
|
|
177
|
-
```bash
|
|
178
|
-
git clone https://github.com/3ZEROS12/pi-tui-status-beautifier.git
|
|
179
|
-
cd pi-tui-status-beautifier
|
|
180
|
-
npm install
|
|
181
|
-
npm test # 18/18 Vitest unit tests pass
|
|
182
|
-
npm run lint # Zero ESLint warnings
|
|
183
|
-
```
|
|
184
|
-
|
|
185
134
|
---
|
|
186
135
|
|
|
187
136
|
## Author's Note
|
|
188
137
|
|
|
189
|
-
The
|
|
138
|
+
The inspiration behind `pi-tui-status-beautifier` comes from daily aesthetic frustration: visual noise.
|
|
190
139
|
|
|
191
|
-
As I installed
|
|
140
|
+
As I installed more extensions for Pi, the bottom status bar turned into a chaotic billboard. Every extension author used their own punctuation, raw emojis, and arbitrary string widths. In split-pane tmux or compact windows, lines wrapped awkwardly and the screen constantly flickered.
|
|
192
141
|
|
|
193
142
|
When I looked into existing status bar plugins, many took a sledgehammer approach: completely replacing Pi's native footer via `ctx.ui.setFooter`. In doing so, they wiped out essential platform metrics—token usage, cache read rates, thinking levels, and git branches. Even worse, some plugins read configuration files synchronously inside the animation redraw loop, causing typing sluggishness.
|
|
194
143
|
|
|
195
144
|
I built this beautifier around three strict constraints:
|
|
196
|
-
1.
|
|
197
|
-
2. Zero render-loop disk
|
|
198
|
-
3. Zero-trace revert
|
|
145
|
+
1. **Never hijack native telemetry**: Normalize extension badges while keeping system metrics untouched.
|
|
146
|
+
2. **Zero render-loop disk reads**: Memory-cached configuration with dirty-checking.
|
|
147
|
+
3. **Zero-trace revert**: Turning it off physically deletes the config block (`delete config.beautifier`).
|
|
199
148
|
|
|
200
149
|
A good status bar should be quiet, balanced, and invisible until you need it.
|
|
201
150
|
|
|
202
151
|
---
|
|
203
152
|
|
|
153
|
+
## Verification & Testing
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm test # 18/18 Vitest unit tests pass
|
|
157
|
+
npm run lint # Zero ESLint warnings
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
204
162
|
## License
|
|
205
163
|
|
|
206
164
|
MIT © [Jason](https://github.com/3ZEROS12)
|
|
@@ -93,11 +93,11 @@ const lastRenderedStatus = new Map<string, string | undefined>();
|
|
|
93
93
|
|
|
94
94
|
// Safe, non-backtracking ReDoS-mitigated ANSI escape sequence matcher (supporting ; and :)
|
|
95
95
|
const ANSI_STRIP_REGEX =
|
|
96
|
-
/[\u001B\u009B][[\\
|
|
96
|
+
/[\u001B\u009B][\[\]\\()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:[:;]\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g;
|
|
97
97
|
|
|
98
98
|
// Hoisted regex patterns to prevent instantiation and GC overhead in the render hot-path
|
|
99
99
|
const STATUS_PATTERN =
|
|
100
|
-
/[🟢🔴🟡⚪⏸✅❌✓✗?✔✖☑☐◆📋⌬★✦◇»«‹›⦗⦘]|[0-9]+\/[0-9]
|
|
100
|
+
/[🟢🔴🟡⚪⏸✅❌✓✗?✔✖☑☐◆📋⌬★✦◇»«‹›⦗⦘]|[0-9]+\/[0-9]+|\b(?:active|online|offline|running|paused|error|success|connected|disconnected|ready|idle|ok|done|failed|waiting|stopped|listening|cleanup|starting)\b/i;
|
|
101
101
|
const NAME_SUFFIX_PATTERN = /-(extension|plugin|assistant|adapter|slash-text|slash|text|widget)$/gi;
|
|
102
102
|
const FRACTION_PATTERN = /(\d+\/\d+|\d+%\s*)/;
|
|
103
103
|
const PARENTHESES_PATTERN = /\((\d+)\)/;
|
|
@@ -108,28 +108,42 @@ let currentStyle = "minimal";
|
|
|
108
108
|
let fullFooterEnabled = false;
|
|
109
109
|
let customStyleConfig: CustomStyleConfig | undefined;
|
|
110
110
|
|
|
111
|
+
function isTestEnvironment(): boolean {
|
|
112
|
+
return (
|
|
113
|
+
process.env.NODE_ENV === "test" ||
|
|
114
|
+
process.env.VITEST !== undefined ||
|
|
115
|
+
process.env.NODE_TEST_CONTEXT !== undefined ||
|
|
116
|
+
process.execArgv.some((a) => a.includes("test") || a.includes("vitest")) ||
|
|
117
|
+
process.env.npm_lifecycle_event === "test"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
/**
|
|
112
122
|
* Resolves configuration hierarchically:
|
|
113
123
|
* Project-level `./.pi/settings.json` takes priority over global `~/.pi/agent/settings.json`
|
|
114
124
|
*/
|
|
115
|
-
export function loadConfig(cwd
|
|
125
|
+
export function loadConfig(cwd?: string): {
|
|
116
126
|
style: string;
|
|
117
127
|
fullFooter: boolean;
|
|
118
128
|
custom?: CustomStyleConfig;
|
|
119
129
|
targetSettingsPath: string;
|
|
120
130
|
} {
|
|
131
|
+
const effectiveCwd = cwd || process.cwd();
|
|
121
132
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
122
133
|
const globalPath = path.join(home, ".pi/agent/settings.json");
|
|
123
|
-
const localPath = path.join(
|
|
134
|
+
const localPath = path.join(effectiveCwd, ".pi/settings.json");
|
|
124
135
|
|
|
125
136
|
let resolvedStyle = "minimal";
|
|
126
137
|
let resolvedFullFooter = false;
|
|
127
138
|
let resolvedCustom: CustomStyleConfig | undefined;
|
|
128
139
|
let hasLocalConfig = false;
|
|
129
140
|
|
|
141
|
+
// Multi-Probe Test Isolation: skip reading real global ~/.pi/agent/settings.json in test runner unless cwd explicitly provided
|
|
142
|
+
const skipGlobalInTest = isTestEnvironment() && cwd === undefined;
|
|
143
|
+
|
|
130
144
|
// 1. Read global settings
|
|
131
145
|
try {
|
|
132
|
-
if (fs.existsSync(globalPath)) {
|
|
146
|
+
if (!skipGlobalInTest && fs.existsSync(globalPath)) {
|
|
133
147
|
const data = JSON.parse(fs.readFileSync(globalPath, "utf-8"));
|
|
134
148
|
if (data.beautifier) {
|
|
135
149
|
if (typeof data.beautifier.style === "string") {
|
|
@@ -185,6 +199,37 @@ export function loadConfig(cwd: string = process.cwd()): {
|
|
|
185
199
|
|
|
186
200
|
loadConfig();
|
|
187
201
|
|
|
202
|
+
function sanitizeCustomConfig(raw: any): CustomStyleConfig | undefined {
|
|
203
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
204
|
+
const clampStr = (v: unknown, maxLen = 16): string | undefined =>
|
|
205
|
+
typeof v === "string" ? v.slice(0, maxLen) : undefined;
|
|
206
|
+
const brackets =
|
|
207
|
+
Array.isArray(raw.brackets) &&
|
|
208
|
+
raw.brackets.length === 2 &&
|
|
209
|
+
typeof raw.brackets[0] === "string" &&
|
|
210
|
+
typeof raw.brackets[1] === "string"
|
|
211
|
+
? ([raw.brackets[0].slice(0, 4), raw.brackets[1].slice(0, 4)] as [string, string])
|
|
212
|
+
: undefined;
|
|
213
|
+
const rawGlyphs = raw.glyphs && typeof raw.glyphs === "object" && !Array.isArray(raw.glyphs) ? raw.glyphs : undefined;
|
|
214
|
+
const glyphs = rawGlyphs
|
|
215
|
+
? {
|
|
216
|
+
success: clampStr(rawGlyphs.success, 4),
|
|
217
|
+
warning: clampStr(rawGlyphs.warning, 4),
|
|
218
|
+
error: clampStr(rawGlyphs.error, 4),
|
|
219
|
+
accent: clampStr(rawGlyphs.accent, 4),
|
|
220
|
+
}
|
|
221
|
+
: undefined;
|
|
222
|
+
return {
|
|
223
|
+
prefix: clampStr(raw.prefix, 16),
|
|
224
|
+
suffix: clampStr(raw.suffix, 16),
|
|
225
|
+
separator: clampStr(raw.separator, 12),
|
|
226
|
+
brackets,
|
|
227
|
+
inverse: typeof raw.inverse === "boolean" ? raw.inverse : undefined,
|
|
228
|
+
padName: typeof raw.padName === "boolean" ? raw.padName : undefined,
|
|
229
|
+
glyphs,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
188
233
|
// Clean status text and map state and details
|
|
189
234
|
export function beautifyStatus(
|
|
190
235
|
key: string,
|
|
@@ -197,13 +242,14 @@ export function beautifyStatus(
|
|
|
197
242
|
if (style === "off" || OFF_STYLES.has((style || "").toLowerCase().trim())) return originalVal;
|
|
198
243
|
|
|
199
244
|
try {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
245
|
+
const boundedVal = originalVal.length > 512 ? originalVal.slice(0, 512) : originalVal;
|
|
246
|
+
const hasAnsi = boundedVal.includes("\u001B") || boundedVal.includes("\u009B");
|
|
247
|
+
const cleanVal = hasAnsi ? boundedVal.replace(ANSI_STRIP_REGEX, "").trim() : boundedVal.trim();
|
|
202
248
|
|
|
203
249
|
if (cleanVal === "") return "";
|
|
204
250
|
|
|
205
251
|
if (!STATUS_PATTERN.test(cleanVal)) {
|
|
206
|
-
return originalVal;
|
|
252
|
+
return originalVal.length > 64 ? sliceToVisualWidth(cleanVal, 32) : originalVal;
|
|
207
253
|
}
|
|
208
254
|
|
|
209
255
|
// 1. Get display name (compact natural width by default to save precious terminal columns)
|
|
@@ -275,7 +321,6 @@ export function beautifyStatus(
|
|
|
275
321
|
valLower.includes("warning") ||
|
|
276
322
|
valLower.includes("pause") ||
|
|
277
323
|
valLower.includes("planning") ||
|
|
278
|
-
valLower.includes("plan") ||
|
|
279
324
|
valLower.includes("waiting") ||
|
|
280
325
|
valLower.includes("cleanup")
|
|
281
326
|
) {
|
|
@@ -291,13 +336,14 @@ export function beautifyStatus(
|
|
|
291
336
|
valLower.includes("ok") ||
|
|
292
337
|
valLower.includes("done")
|
|
293
338
|
) {
|
|
294
|
-
state = "success";
|
|
339
|
+
state = name === "plan" ? "accent" : "success";
|
|
295
340
|
} else {
|
|
296
341
|
state = name === "plan" ? "accent" : "success";
|
|
297
342
|
}
|
|
298
343
|
|
|
299
344
|
// 4. Determine state glyph & indicator
|
|
300
345
|
const glyphMap = preset.glyphs || {};
|
|
346
|
+
const isEcosystemGlyph = name === "anchor" || name === "toolflow" || name === "lingual";
|
|
301
347
|
let defaultGlyph =
|
|
302
348
|
name === "anchor"
|
|
303
349
|
? "⌖"
|
|
@@ -314,18 +360,21 @@ export function beautifyStatus(
|
|
|
314
360
|
: "●";
|
|
315
361
|
|
|
316
362
|
// Dynamic micro-spinner for active in-progress / running states (running, working, starting, waiting)
|
|
317
|
-
|
|
363
|
+
const isSpinning =
|
|
318
364
|
valLower.includes("running") ||
|
|
319
365
|
valLower.includes("working") ||
|
|
320
366
|
valLower.includes("starting") ||
|
|
321
367
|
valLower.includes("waiting") ||
|
|
322
|
-
valLower.includes("...")
|
|
323
|
-
) {
|
|
368
|
+
valLower.includes("...");
|
|
369
|
+
if (isSpinning) {
|
|
324
370
|
const spinnerChar = SPINNERS[Math.floor(Date.now() / 300) % SPINNERS.length];
|
|
325
371
|
defaultGlyph = spinnerChar;
|
|
326
372
|
}
|
|
327
373
|
|
|
328
|
-
|
|
374
|
+
// Preserve ecosystem glyphs (⌖, ⌬, ⇄) and active spinners across all preset themes unless overridden in custom style
|
|
375
|
+
const glyph = (isSpinning || (isEcosystemGlyph && style !== "custom"))
|
|
376
|
+
? defaultGlyph
|
|
377
|
+
: (glyphMap[state] || defaultGlyph);
|
|
329
378
|
|
|
330
379
|
const hasThemeFg = theme && typeof theme.fg === "function";
|
|
331
380
|
const coloredGlyph = hasThemeFg ? theme.fg(state, glyph) : glyph;
|
|
@@ -368,6 +417,14 @@ export function beautifyStatus(
|
|
|
368
417
|
}
|
|
369
418
|
}
|
|
370
419
|
|
|
420
|
+
const isZeroWidthCode = (code: number) =>
|
|
421
|
+
code === 0x200d ||
|
|
422
|
+
code === 0xfe0f ||
|
|
423
|
+
code === 0xfe0e ||
|
|
424
|
+
(code >= 0x0300 && code <= 0x036f) ||
|
|
425
|
+
(code >= 0x1f3fb && code <= 0x1f3ff) ||
|
|
426
|
+
(code >= 0x200b && code <= 0x200f);
|
|
427
|
+
|
|
371
428
|
/**
|
|
372
429
|
* Calculates the visual column cell width of a string, taking into account
|
|
373
430
|
* full-width Unicode characters, East Asian ideographs, Kana, Hangul, emojis, and zero-width joiners.
|
|
@@ -376,17 +433,20 @@ export function getStringWidth(str: string): number {
|
|
|
376
433
|
if (!str) return 0;
|
|
377
434
|
const clean = str.replace(ANSI_STRIP_REGEX, "");
|
|
378
435
|
let width = 0;
|
|
436
|
+
let afterZwj = false;
|
|
379
437
|
|
|
380
438
|
for (const char of clean) {
|
|
381
439
|
const code = char.codePointAt(0) || 0;
|
|
382
440
|
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
441
|
+
if (code === 0x200d) {
|
|
442
|
+
afterZwj = true;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (isZeroWidthCode(code)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (afterZwj) {
|
|
449
|
+
afterZwj = false;
|
|
390
450
|
continue;
|
|
391
451
|
}
|
|
392
452
|
|
|
@@ -434,21 +494,27 @@ export function isFullWidth(code: number): boolean {
|
|
|
434
494
|
* Slices a string to a maximum visual cell width without cutting surrogate pairs in half.
|
|
435
495
|
*/
|
|
436
496
|
export function sliceToVisualWidth(str: string, maxWidth: number): string {
|
|
497
|
+
if (!str || maxWidth <= 0) return "";
|
|
437
498
|
let width = 0;
|
|
438
499
|
let sliced = "";
|
|
500
|
+
let pendingZwj = false;
|
|
439
501
|
|
|
440
502
|
for (const char of str) {
|
|
441
503
|
const code = char.codePointAt(0) || 0;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
504
|
+
if (code === 0x200d) {
|
|
505
|
+
if (sliced.length > 0) pendingZwj = true;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (isZeroWidthCode(code)) {
|
|
509
|
+
if (sliced.length > 0) sliced += char;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (pendingZwj) {
|
|
513
|
+
sliced += "\u200d" + char;
|
|
514
|
+
pendingZwj = false;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const charWidth = isFullWidth(code) ? 2 : 1;
|
|
452
518
|
|
|
453
519
|
if (width + charWidth > maxWidth) {
|
|
454
520
|
break;
|
|
@@ -539,11 +605,11 @@ export function renderBeautifiedFooter(
|
|
|
539
605
|
): string[] {
|
|
540
606
|
const preset =
|
|
541
607
|
style === "custom"
|
|
542
|
-
? (customCfg || customStyleConfig || {})
|
|
608
|
+
? (sanitizeCustomConfig(customCfg) || sanitizeCustomConfig(customStyleConfig) || {})
|
|
543
609
|
: (BUILTIN_PRESETS[style] || BUILTIN_PRESETS.minimal);
|
|
544
610
|
|
|
545
611
|
const hasThemeFg = theme && typeof theme.fg === "function";
|
|
546
|
-
const sep = preset.separator
|
|
612
|
+
const sep = typeof preset.separator === "string" && preset.separator ? preset.separator : (preset.brackets ? " " : " ❯ ");
|
|
547
613
|
const styledSep =
|
|
548
614
|
hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯") || sep.includes("»"))
|
|
549
615
|
? theme.fg("dim", sep)
|
|
@@ -563,9 +629,13 @@ export function renderBeautifiedFooter(
|
|
|
563
629
|
let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
|
|
564
630
|
let latestCacheHitRate: number | undefined;
|
|
565
631
|
|
|
566
|
-
|
|
632
|
+
const entriesSource =
|
|
633
|
+
ctx?.sessionManager?.getEntries?.() ||
|
|
634
|
+
ctx?.sessionManager?.getBranch?.();
|
|
635
|
+
|
|
636
|
+
if (entriesSource) {
|
|
567
637
|
try {
|
|
568
|
-
for (const entry of
|
|
638
|
+
for (const entry of entriesSource) {
|
|
569
639
|
if (entry.type === "usage" && entry.usage) {
|
|
570
640
|
input += entry.usage.input || 0;
|
|
571
641
|
output += entry.usage.output || 0;
|
|
@@ -845,16 +915,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
845
915
|
// Write atomically to settings.json
|
|
846
916
|
const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
|
|
847
917
|
try {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
918
|
+
fs.mkdirSync(path.dirname(targetSettingsPath), { recursive: true });
|
|
919
|
+
const config = fs.existsSync(targetSettingsPath)
|
|
920
|
+
? JSON.parse(fs.readFileSync(targetSettingsPath, "utf-8"))
|
|
921
|
+
: {};
|
|
922
|
+
if (!config.beautifier) config.beautifier = {};
|
|
923
|
+
config.beautifier.style = styleKey;
|
|
924
|
+
config.beautifier.fullFooter = isFullFooter;
|
|
925
|
+
const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
|
|
926
|
+
fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
|
|
927
|
+
fs.renameSync(tempSettings, targetSettingsPath);
|
|
858
928
|
} catch (_e) {
|
|
859
929
|
// Silently ignore disk write errors
|
|
860
930
|
}
|
|
@@ -887,32 +957,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
887
957
|
`TUI status style changed to: ${styleKey} (${isFullFooter ? "Full-Footer" : "Status Only"})`,
|
|
888
958
|
"info"
|
|
889
959
|
);
|
|
890
|
-
|
|
891
|
-
if ((VALID_STYLES as readonly string[]).includes(styleKey)) {
|
|
892
|
-
currentStyle = styleKey;
|
|
893
|
-
lastRenderedStatus.clear();
|
|
894
|
-
ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
|
|
895
|
-
|
|
896
|
-
const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
|
|
897
|
-
|
|
898
|
-
try {
|
|
899
|
-
if (fs.existsSync(targetSettingsPath)) {
|
|
900
|
-
const data = fs.readFileSync(targetSettingsPath, "utf-8");
|
|
901
|
-
const config = JSON.parse(data);
|
|
902
|
-
if (!config.beautifier) config.beautifier = {};
|
|
903
|
-
config.beautifier.style = styleKey;
|
|
904
|
-
const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
|
|
905
|
-
fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
|
|
906
|
-
fs.renameSync(tempSettings, targetSettingsPath);
|
|
907
|
-
}
|
|
908
|
-
} catch (_e) {
|
|
909
|
-
// Silently ignore disk write errors
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
for (const [key, val] of originalStatuses.entries()) {
|
|
913
|
-
ctx.ui.setStatus(key, val);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
960
|
},
|
|
917
961
|
});
|
|
918
962
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tui-status-beautifier",
|
|
3
|
-
"version": "1.7.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.7.4",
|
|
4
|
+
"description": "Clean up your Pi terminal status bar — formats extension badges with crisp presets (Stream, Powerline, Matrix, Glow), folds idle items on narrow screens, and adds live spinners.",
|
|
5
5
|
"main": "extensions/tui-status-beautifier.ts",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package"
|