pi-zentui 0.6.0 → 0.8.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 +106 -22
- package/extensions/zentui/config.ts +153 -1
- package/extensions/zentui/fixed-editor/cluster.ts +194 -0
- package/extensions/zentui/fixed-editor/compositor.ts +584 -0
- package/extensions/zentui/fixed-editor/index.ts +206 -0
- package/extensions/zentui/fixed-editor/input.ts +85 -0
- package/extensions/zentui/fixed-editor/selection.ts +217 -0
- package/extensions/zentui/fixed-editor/terminal-modes.ts +75 -0
- package/extensions/zentui/fixed-editor/types.ts +70 -0
- package/extensions/zentui/footer.ts +119 -4
- package/extensions/zentui/format.ts +80 -1
- package/extensions/zentui/git.ts +142 -3
- package/extensions/zentui/icons.ts +31 -4
- package/extensions/zentui/index.ts +49 -1
- package/extensions/zentui/package-version.ts +650 -0
- package/extensions/zentui/project-state.ts +15 -0
- package/extensions/zentui/settings-command.ts +117 -7
- package/extensions/zentui/state.ts +3 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
17
17
|
|
|
18
18
|
### Footer (Starship-inspired)
|
|
19
19
|
|
|
20
|
-
-
|
|
20
|
+
- `dirname` — current directory (`basename` by default; optional `full` path with directory depth via `pathDisplay`)
|
|
21
21
|
- `on branch` — git branch with icon
|
|
22
22
|
- `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
|
|
23
23
|
- `via v5.5.0` — runtime detection with version and Starship-style Nerd Font runtime/language modules
|
|
24
|
-
- Optional segments (off by default): `user@host`, current time, OS icon, and
|
|
24
|
+
- Optional segments (off by default): `user@host`, current time, OS icon, session duration, and the **project package version** (e.g. `package.json` → `0.6.0`) — distinct from the runtime segment, which shows the installed toolchain
|
|
25
25
|
- Right side shows context usage, token counts, and cost
|
|
26
26
|
- Built-in footer segments can be shown or hidden individually from `/zentui`
|
|
27
27
|
- Fully custom Starship-style layout via a `footerFormat` template string — see [Footer Format Template](#footer-format-template)
|
|
@@ -35,6 +35,7 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
35
35
|
- Configurable model, provider, and thinking-level indicator colors
|
|
36
36
|
- Prompt-box-style user messages matching the ZentUI input chrome
|
|
37
37
|
- Copy-friendly mode hides editor and previous-message rail glyphs so terminal selection copies less chrome
|
|
38
|
+
- **Fixed editor** (experimental, opt-in): Pin the editor and footer at the bottom of the terminal while the transcript scrolls above
|
|
38
39
|
|
|
39
40
|
### Git Status Icons
|
|
40
41
|
|
|
@@ -143,6 +144,9 @@ Useful slash-command shortcuts:
|
|
|
143
144
|
/zentui copy-friendly enable
|
|
144
145
|
/zentui copy-friendly disable
|
|
145
146
|
/zentui copy-friendly toggle
|
|
147
|
+
/zentui fixed-editor enable
|
|
148
|
+
/zentui fixed-editor disable
|
|
149
|
+
/zentui fixed-editor toggle
|
|
146
150
|
/zentui format "$cwd on branch $git_branch$git_status using $runtime $fill $context"
|
|
147
151
|
/zentui format clear
|
|
148
152
|
```
|
|
@@ -164,7 +168,7 @@ Default config values — copy this and change any value you want:
|
|
|
164
168
|
},
|
|
165
169
|
"icons": {
|
|
166
170
|
"mode": "auto",
|
|
167
|
-
"cwd": "
|
|
171
|
+
"cwd": "",
|
|
168
172
|
"git": "",
|
|
169
173
|
"ahead": "↑",
|
|
170
174
|
"behind": "↓",
|
|
@@ -197,6 +201,10 @@ Default config values — copy this and change any value you want:
|
|
|
197
201
|
"separator": "bright-black",
|
|
198
202
|
"runtimePrefix": "",
|
|
199
203
|
"sessionDuration": "yellow",
|
|
204
|
+
"packageVersion": "208",
|
|
205
|
+
"gitCommit": "bold green",
|
|
206
|
+
"gitMetricsAdded": "bold green",
|
|
207
|
+
"gitMetricsDeleted": "bold red",
|
|
200
208
|
"username": "bold yellow",
|
|
201
209
|
"time": "bold yellow",
|
|
202
210
|
"os": "bold white",
|
|
@@ -234,12 +242,29 @@ Default config values — copy this and change any value you want:
|
|
|
234
242
|
"sessionDuration": false,
|
|
235
243
|
"username": false,
|
|
236
244
|
"time": false,
|
|
237
|
-
"os": false
|
|
245
|
+
"os": false,
|
|
246
|
+
"packageVersion": false,
|
|
247
|
+
"gitCommit": false,
|
|
248
|
+
"gitMetrics": false
|
|
249
|
+
},
|
|
250
|
+
"gitCommit": {
|
|
251
|
+
"hashLength": 7,
|
|
252
|
+
"onlyDetached": true,
|
|
253
|
+
"showTag": true
|
|
254
|
+
},
|
|
255
|
+
"gitMetrics": {
|
|
256
|
+
"onlyNonzero": true,
|
|
257
|
+
"ignoreSubmodules": false
|
|
238
258
|
},
|
|
239
259
|
"extensionStatuses": {
|
|
240
260
|
"defaultPlacement": "right",
|
|
241
261
|
"placements": {},
|
|
242
262
|
"colorModes": {}
|
|
263
|
+
},
|
|
264
|
+
"fixedEditor": {
|
|
265
|
+
"enabled": false,
|
|
266
|
+
"mouseScroll": true,
|
|
267
|
+
"copyNotice": true
|
|
243
268
|
}
|
|
244
269
|
}
|
|
245
270
|
```
|
|
@@ -252,8 +277,10 @@ Default config values — copy this and change any value you want:
|
|
|
252
277
|
- `icons`: every shown icon key is configurable; omit any key to use the Zentui default. `icons.mode` is `auto` | `nerd` | `ascii` (default `auto`, same glyphs as nerd). ASCII mode swaps in plain fallbacks for statusline icons and runtime symbols — useful without a Nerd Font. Custom per-icon strings always win over mode defaults. Custom `icons.os` always wins; when left at the mode default, Zentui maps the OS icon by platform. `rail` sets the vertical glyph drawn as the left rail of the active editor frame and previous user messages when `copyFriendly` is disabled (default `│`; any single Unicode vertical or block glyph). `editorPrompt` controls an optional copy-friendly editor prompt glyph; the default is `""` so copy-friendly mode stays rail-free.
|
|
253
278
|
- `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
|
|
254
279
|
- `features`: `editor` enables Zentui's custom editor, selector borders, and previous-message chrome. `statusLine` enables Zentui's custom footer/status line. `copyFriendly` hides editor and previous-message rail glyphs so native terminal selection copies less chrome. All three can be changed from `/zentui` or direct slash-command arguments.
|
|
255
|
-
- `footerSegments`: show or hide individual built-in footer segments (`cwd`, `gitBranch`, `gitStatus`, `gitCounts`, `runtime`, `sessionDuration`, `username`, `time`, `os`, `context`, `tokens`, `cost`). Toggle them from the `Built-in segments` tab in `/zentui`.
|
|
280
|
+
- `footerSegments`: show or hide individual built-in footer segments (`cwd`, `gitBranch`, `gitStatus`, `gitCounts`, `gitCommit`, `gitMetrics`, `runtime`, `packageVersion`, `sessionDuration`, `username`, `time`, `os`, `context`, `tokens`, `cost`). Toggle them from the `Built-in segments` tab in `/zentui`.
|
|
256
281
|
- `footerFormat`: optional Starship-style template string that fully controls the footer layout. When set, it overrides `footerSegments`. See [Footer Format Template](#footer-format-template) below. The `/zentui` **Layout** tab configures context style, path display mode/depth, and icon mode; set or clear custom formats with `/zentui format`.
|
|
282
|
+
- `gitCommit`: Starship [`git_commit`](https://starship.rs/config/#git-commit)-style options for the `gitCommit` footer segment. `hashLength` (default `7`, clamped to `4`–`40`) controls the short-hash display length. `onlyDetached` (default `true`) shows the hash mainly on detached HEAD. `showTag` (default `true`) appends an exact-match tag (`git describe --tags --exact-match HEAD`). The tag probe piggybacks on the existing git refresh — it only runs when both the segment and `showTag` are on, and misses/failures degrade silently.
|
|
283
|
+
- `gitMetrics`: Starship [`git_metrics`](https://starship.rs/config/#git-metrics)-style options for the `gitMetrics` footer segment. Uses `git diff HEAD --numstat` (staged + unstaged combined — the Starship “total dirty” view) to show aggregate `+added −deleted` line counts. `onlyNonzero` (default `true`) omits each zero component independently and hides the segment entirely at `0/0`. `ignoreSubmodules` (default `false`) adds `--ignore-submodules=all`. The numstat diff piggybacks on the existing git refresh and uses a hard 2s timeout; a metrics-only failure degrades silently without discarding fresh branch/status data. On very large monorepos the diff may lag or be omitted on timeout.
|
|
257
284
|
- `extensionStatuses`: controls third-party statuses published by other Pi extensions through `ctx.ui.setStatus()`. `defaultPlacement` and each `placements` value can be `off`, `left`, `middle`, or `right`. The `Extension segments` tab in `/zentui` lists only statuses that are currently active.
|
|
258
285
|
- The shown `editor*` values match the default `theme` source. Omit those keys to keep Zentui's source-aware defaults when switching between `theme` and `terminal`.
|
|
259
286
|
- `editorAccent` styles the active editor rail and previous user-message rail when `features.copyFriendly` is disabled.
|
|
@@ -285,22 +312,29 @@ Center the branch between directory and cost:
|
|
|
285
312
|
|
|
286
313
|
### Variables
|
|
287
314
|
|
|
288
|
-
| Token | Aliases | Renders
|
|
289
|
-
| ------------------- | ------------ |
|
|
290
|
-
| `$cwd` | `$directory` | current directory
|
|
291
|
-
| `$git_branch` | `$branch` | git branch with icon
|
|
292
|
-
| `$git_status` | `$status` | `[!?↑]` status block
|
|
293
|
-
| `$git_state` | `$state` | `REBASING` / `MERGING` / … (optional `n/m`)
|
|
294
|
-
| `$
|
|
295
|
-
| `$
|
|
296
|
-
| `$
|
|
297
|
-
| `$
|
|
298
|
-
| `$
|
|
299
|
-
| `$
|
|
300
|
-
| `$
|
|
301
|
-
| `$
|
|
302
|
-
| `$
|
|
303
|
-
| `$
|
|
315
|
+
| Token | Aliases | Renders |
|
|
316
|
+
| ------------------- | ------------ | ------------------------------------------------------------------- |
|
|
317
|
+
| `$cwd` | `$directory` | current directory |
|
|
318
|
+
| `$git_branch` | `$branch` | git branch with icon |
|
|
319
|
+
| `$git_status` | `$status` | `[!?↑]` status block |
|
|
320
|
+
| `$git_state` | `$state` | `REBASING` / `MERGING` / … (optional `n/m`) |
|
|
321
|
+
| `$git_commit` | `$commit` | short commit hash (+ exact-match tag when present) |
|
|
322
|
+
| `$git_tag` | `$tag` | exact-match tag at HEAD |
|
|
323
|
+
| `$git_metrics` | | aggregate line changes `+added −deleted` |
|
|
324
|
+
| `$git_added` | | added line count (`+N`) |
|
|
325
|
+
| `$git_deleted` | | deleted line count (`−N`) |
|
|
326
|
+
| `$runtime` | | runtime icon + version |
|
|
327
|
+
| `$package` | | project package version, `is <glyph> <version>` (manifest-derived) |
|
|
328
|
+
| `$package_version` | | raw project package version (no icon) |
|
|
329
|
+
| `$session_duration` | `$duration` | session running time |
|
|
330
|
+
| `$username` | | `user@host` |
|
|
331
|
+
| `$os` | | operating-system icon |
|
|
332
|
+
| `$time` | | current time `HH:MM` |
|
|
333
|
+
| `$context` | | context usage (text and/or gauge via config) |
|
|
334
|
+
| `$tokens` | | input/output token counts |
|
|
335
|
+
| `$cost` | | session cost |
|
|
336
|
+
| `$sep` | `$separator` | themed `\|` using `colors.separator` |
|
|
337
|
+
| `$fill` | — | special: splits zones |
|
|
304
338
|
|
|
305
339
|
### `$fill` behavior
|
|
306
340
|
|
|
@@ -317,9 +351,59 @@ Center the branch between directory and cost:
|
|
|
317
351
|
- Unknown `$variables` render empty.
|
|
318
352
|
- Set or clear at runtime: `/zentui format "<template>"` and `/zentui format clear`.
|
|
319
353
|
|
|
354
|
+
## Fixed editor (experimental, opt-in)
|
|
355
|
+
|
|
356
|
+
The fixed editor pins the Zentui editor and footer at the bottom of the terminal while the transcript scrolls above. This enables composing follow-up messages while referencing earlier conversation history.
|
|
357
|
+
|
|
358
|
+
### How to enable
|
|
359
|
+
|
|
360
|
+
```text
|
|
361
|
+
/zentui fixed-editor enable
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Or in `~/.pi/agent/zentui.json`:
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"fixedEditor": {
|
|
369
|
+
"enabled": true
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Keyboard controls
|
|
375
|
+
|
|
376
|
+
| Key | Action |
|
|
377
|
+
| --- | ------ |
|
|
378
|
+
| `PageUp` / `PageDown` | Scroll transcript one viewport up/down |
|
|
379
|
+
| `Ctrl+Shift+↑` / `Ctrl+Shift+↓` | Scroll transcript up/down (Kitty protocol variants supported) |
|
|
380
|
+
| `Enter` | Jump to bottom (and submit message) |
|
|
381
|
+
|
|
382
|
+
### Mouse scroll (default on)
|
|
383
|
+
|
|
384
|
+
Mouse wheel scrolling is enabled by default when the fixed editor is on. Disable it via `/zentui` Features or:
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
{
|
|
388
|
+
"fixedEditor": {
|
|
389
|
+
"enabled": true,
|
|
390
|
+
"mouseScroll": true
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Warning**: Mouse scroll enables SGR mouse reporting, which disables native terminal text selection, URL click-through, and tmux/Herdr scrollback for the Pi session. Toggle off if you need those features.
|
|
396
|
+
|
|
397
|
+
### Conflicts and limitations
|
|
398
|
+
|
|
399
|
+
- **Incompatible with** `pi-powerline-footer`, `@tifan/pi-fixed-editor`, and `pi-sticky-input`. These packages patch the same Pi TUI internals; only one rendering owner can be active at a time.
|
|
400
|
+
- **Alternate screen**: Uses the terminal's alternate screen buffer. Native scrollback history is not accessible while the fixed editor is active.
|
|
401
|
+
- **Pi version fragility**: Patches internal TUI methods (`doRender`, `render`, `terminal.write`, `terminal.rows`) that may change across Pi versions. If the TUI layout is unsupported, Zentui falls back to normal rendering with a console warning.
|
|
402
|
+
- If your terminal is stuck after a crash, run `reset` or restart the terminal.
|
|
403
|
+
|
|
320
404
|
## Requirements
|
|
321
405
|
|
|
322
|
-
- [Pi](https://pi.dev) coding agent 0.
|
|
406
|
+
- [Pi](https://pi.dev) coding agent 0.80 or newer
|
|
323
407
|
- A [Nerd Font](https://www.nerdfonts.com/) for icons (or set `icons.mode` to `"ascii"`)
|
|
324
408
|
|
|
325
409
|
## Development
|
|
@@ -48,6 +48,8 @@ export type FooterSegmentsConfig = {
|
|
|
48
48
|
gitBranch: boolean;
|
|
49
49
|
gitStatus: boolean;
|
|
50
50
|
gitCounts: boolean;
|
|
51
|
+
gitCommit: boolean;
|
|
52
|
+
gitMetrics: boolean;
|
|
51
53
|
runtime: boolean;
|
|
52
54
|
context: boolean;
|
|
53
55
|
tokens: boolean;
|
|
@@ -56,11 +58,37 @@ export type FooterSegmentsConfig = {
|
|
|
56
58
|
username: boolean;
|
|
57
59
|
time: boolean;
|
|
58
60
|
os: boolean;
|
|
61
|
+
packageVersion: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type FixedEditorConfig = {
|
|
65
|
+
enabled: boolean;
|
|
66
|
+
mouseScroll: boolean;
|
|
67
|
+
copyNotice: boolean;
|
|
59
68
|
};
|
|
60
69
|
|
|
61
70
|
export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
|
|
62
71
|
export type ExtensionStatusColorMode = "zentui" | "original";
|
|
63
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Starship `git_commit`-style options.
|
|
75
|
+
* See https://starship.rs/config/#git-commit
|
|
76
|
+
*/
|
|
77
|
+
export type GitCommitConfig = {
|
|
78
|
+
hashLength: number;
|
|
79
|
+
onlyDetached: boolean;
|
|
80
|
+
showTag: boolean;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Starship `git_metrics`-style options.
|
|
85
|
+
* See https://starship.rs/config/#git-metrics
|
|
86
|
+
*/
|
|
87
|
+
export type GitMetricsConfig = {
|
|
88
|
+
onlyNonzero: boolean;
|
|
89
|
+
ignoreSubmodules: boolean;
|
|
90
|
+
};
|
|
91
|
+
|
|
64
92
|
const DEFAULT_EXTENSION_STATUS_COLOR_MODE: ExtensionStatusColorMode = "zentui";
|
|
65
93
|
|
|
66
94
|
export type ExtensionStatusesConfig = {
|
|
@@ -92,6 +120,10 @@ export type PolishedTuiConfig = {
|
|
|
92
120
|
runtimePrefix: ColorSpec;
|
|
93
121
|
extensionStatus: ColorSpec;
|
|
94
122
|
sessionDuration: ColorSpec;
|
|
123
|
+
packageVersion: ColorSpec;
|
|
124
|
+
gitCommit: ColorSpec;
|
|
125
|
+
gitMetricsAdded: ColorSpec;
|
|
126
|
+
gitMetricsDeleted: ColorSpec;
|
|
95
127
|
username: ColorSpec;
|
|
96
128
|
time: ColorSpec;
|
|
97
129
|
os: ColorSpec;
|
|
@@ -110,7 +142,10 @@ export type PolishedTuiConfig = {
|
|
|
110
142
|
colorSources: ColorSourcesConfig;
|
|
111
143
|
features: UiFeaturesConfig;
|
|
112
144
|
footerSegments: FooterSegmentsConfig;
|
|
145
|
+
gitCommit: GitCommitConfig;
|
|
146
|
+
gitMetrics: GitMetricsConfig;
|
|
113
147
|
extensionStatuses: ExtensionStatusesConfig;
|
|
148
|
+
fixedEditor: FixedEditorConfig;
|
|
114
149
|
};
|
|
115
150
|
|
|
116
151
|
/**
|
|
@@ -130,6 +165,13 @@ export const FOOTER_FORMAT_VARIABLES = [
|
|
|
130
165
|
"context",
|
|
131
166
|
"tokens",
|
|
132
167
|
"cost",
|
|
168
|
+
"package",
|
|
169
|
+
"package_version",
|
|
170
|
+
"git_commit",
|
|
171
|
+
"git_tag",
|
|
172
|
+
"git_metrics",
|
|
173
|
+
"git_added",
|
|
174
|
+
"git_deleted",
|
|
133
175
|
"sep",
|
|
134
176
|
] as const;
|
|
135
177
|
|
|
@@ -142,6 +184,8 @@ export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
|
|
|
142
184
|
branch: "git_branch",
|
|
143
185
|
status: "git_status",
|
|
144
186
|
state: "git_state",
|
|
187
|
+
commit: "git_commit",
|
|
188
|
+
tag: "git_tag",
|
|
145
189
|
duration: "session_duration",
|
|
146
190
|
separator: "sep",
|
|
147
191
|
};
|
|
@@ -171,6 +215,10 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
171
215
|
runtimePrefix: "",
|
|
172
216
|
extensionStatus: "bright-black",
|
|
173
217
|
sessionDuration: "yellow",
|
|
218
|
+
packageVersion: "208",
|
|
219
|
+
gitCommit: "bold green",
|
|
220
|
+
gitMetricsAdded: "bold green",
|
|
221
|
+
gitMetricsDeleted: "bold red",
|
|
174
222
|
username: "bold yellow",
|
|
175
223
|
time: "bold yellow",
|
|
176
224
|
os: "bold white",
|
|
@@ -198,12 +246,29 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
198
246
|
username: false,
|
|
199
247
|
time: false,
|
|
200
248
|
os: false,
|
|
249
|
+
packageVersion: false,
|
|
250
|
+
gitCommit: false,
|
|
251
|
+
gitMetrics: false,
|
|
252
|
+
},
|
|
253
|
+
gitCommit: {
|
|
254
|
+
hashLength: 7,
|
|
255
|
+
onlyDetached: true,
|
|
256
|
+
showTag: true,
|
|
257
|
+
},
|
|
258
|
+
gitMetrics: {
|
|
259
|
+
onlyNonzero: true,
|
|
260
|
+
ignoreSubmodules: false,
|
|
201
261
|
},
|
|
202
262
|
extensionStatuses: {
|
|
203
263
|
defaultPlacement: "right",
|
|
204
264
|
placements: {},
|
|
205
265
|
colorModes: {},
|
|
206
266
|
},
|
|
267
|
+
fixedEditor: {
|
|
268
|
+
enabled: false,
|
|
269
|
+
mouseScroll: true,
|
|
270
|
+
copyNotice: true,
|
|
271
|
+
},
|
|
207
272
|
};
|
|
208
273
|
|
|
209
274
|
type ConfigRecord = Record<string, unknown>;
|
|
@@ -330,6 +395,10 @@ function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiCo
|
|
|
330
395
|
runtimePrefix: colorValue(record, "runtimePrefix"),
|
|
331
396
|
extensionStatus: colorValue(record, "extensionStatus"),
|
|
332
397
|
sessionDuration: colorValue(record, "sessionDuration"),
|
|
398
|
+
packageVersion: colorValue(record, "packageVersion"),
|
|
399
|
+
gitCommit: colorValue(record, "gitCommit"),
|
|
400
|
+
gitMetricsAdded: colorValue(record, "gitMetricsAdded"),
|
|
401
|
+
gitMetricsDeleted: colorValue(record, "gitMetricsDeleted"),
|
|
333
402
|
username: colorValue(record, "username"),
|
|
334
403
|
time: colorValue(record, "time"),
|
|
335
404
|
os: colorValue(record, "os"),
|
|
@@ -377,6 +446,41 @@ function normalizeFooterSegments(record: Record<string, unknown>): FooterSegment
|
|
|
377
446
|
username: footerSegmentValue(record, "username"),
|
|
378
447
|
time: footerSegmentValue(record, "time"),
|
|
379
448
|
os: footerSegmentValue(record, "os"),
|
|
449
|
+
packageVersion: footerSegmentValue(record, "packageVersion"),
|
|
450
|
+
gitCommit: footerSegmentValue(record, "gitCommit"),
|
|
451
|
+
gitMetrics: footerSegmentValue(record, "gitMetrics"),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Clamp hashLength to Git's valid abbreviation range [4, 40]. */
|
|
456
|
+
function normalizeGitHashLength(value: unknown): number {
|
|
457
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
458
|
+
if (!Number.isFinite(parsed)) return defaultConfig.gitCommit.hashLength;
|
|
459
|
+
const rounded = Math.round(parsed);
|
|
460
|
+
return Math.min(40, Math.max(4, rounded));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function normalizeGitCommitConfig(record: Record<string, unknown>): GitCommitConfig {
|
|
464
|
+
return {
|
|
465
|
+
hashLength: normalizeGitHashLength(record.hashLength),
|
|
466
|
+
onlyDetached:
|
|
467
|
+
typeof record.onlyDetached === "boolean"
|
|
468
|
+
? record.onlyDetached
|
|
469
|
+
: defaultConfig.gitCommit.onlyDetached,
|
|
470
|
+
showTag: typeof record.showTag === "boolean" ? record.showTag : defaultConfig.gitCommit.showTag,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function normalizeGitMetricsConfig(record: Record<string, unknown>): GitMetricsConfig {
|
|
475
|
+
return {
|
|
476
|
+
onlyNonzero:
|
|
477
|
+
typeof record.onlyNonzero === "boolean"
|
|
478
|
+
? record.onlyNonzero
|
|
479
|
+
: defaultConfig.gitMetrics.onlyNonzero,
|
|
480
|
+
ignoreSubmodules:
|
|
481
|
+
typeof record.ignoreSubmodules === "boolean"
|
|
482
|
+
? record.ignoreSubmodules
|
|
483
|
+
: defaultConfig.gitMetrics.ignoreSubmodules,
|
|
380
484
|
};
|
|
381
485
|
}
|
|
382
486
|
|
|
@@ -416,6 +520,21 @@ function normalizeExtensionStatuses(record: Record<string, unknown>): ExtensionS
|
|
|
416
520
|
};
|
|
417
521
|
}
|
|
418
522
|
|
|
523
|
+
function normalizeFixedEditorConfig(record: Record<string, unknown>): FixedEditorConfig {
|
|
524
|
+
return {
|
|
525
|
+
enabled:
|
|
526
|
+
typeof record.enabled === "boolean" ? record.enabled : defaultConfig.fixedEditor.enabled,
|
|
527
|
+
mouseScroll:
|
|
528
|
+
typeof record.mouseScroll === "boolean"
|
|
529
|
+
? record.mouseScroll
|
|
530
|
+
: defaultConfig.fixedEditor.mouseScroll,
|
|
531
|
+
copyNotice:
|
|
532
|
+
typeof record.copyNotice === "boolean"
|
|
533
|
+
? record.copyNotice
|
|
534
|
+
: defaultConfig.fixedEditor.copyNotice,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
419
538
|
function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
|
|
420
539
|
return value === "starship" || value === "editor" || value === "userMessages";
|
|
421
540
|
}
|
|
@@ -437,7 +556,10 @@ function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig
|
|
|
437
556
|
value === "sessionDuration" ||
|
|
438
557
|
value === "username" ||
|
|
439
558
|
value === "time" ||
|
|
440
|
-
value === "os"
|
|
559
|
+
value === "os" ||
|
|
560
|
+
value === "packageVersion" ||
|
|
561
|
+
value === "gitCommit" ||
|
|
562
|
+
value === "gitMetrics"
|
|
441
563
|
);
|
|
442
564
|
}
|
|
443
565
|
|
|
@@ -505,6 +627,15 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
505
627
|
const extensionStatuses = isRecord(config.extensionStatuses)
|
|
506
628
|
? normalizeExtensionStatuses(config.extensionStatuses as Record<string, unknown>)
|
|
507
629
|
: defaultConfig.extensionStatuses;
|
|
630
|
+
const gitCommit = isRecord(config.gitCommit)
|
|
631
|
+
? normalizeGitCommitConfig(config.gitCommit as Record<string, unknown>)
|
|
632
|
+
: defaultConfig.gitCommit;
|
|
633
|
+
const gitMetrics = isRecord(config.gitMetrics)
|
|
634
|
+
? normalizeGitMetricsConfig(config.gitMetrics as Record<string, unknown>)
|
|
635
|
+
: defaultConfig.gitMetrics;
|
|
636
|
+
const fixedEditor = isRecord(config.fixedEditor)
|
|
637
|
+
? normalizeFixedEditorConfig(config.fixedEditor as Record<string, unknown>)
|
|
638
|
+
: defaultConfig.fixedEditor;
|
|
508
639
|
return {
|
|
509
640
|
projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
|
|
510
641
|
footerFormat: stringValue(config, "footerFormat") ?? "",
|
|
@@ -519,11 +650,14 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
519
650
|
colorSources: { ...colorSources },
|
|
520
651
|
features: { ...features },
|
|
521
652
|
footerSegments: { ...footerSegments },
|
|
653
|
+
gitCommit,
|
|
654
|
+
gitMetrics,
|
|
522
655
|
extensionStatuses: {
|
|
523
656
|
defaultPlacement: extensionStatuses.defaultPlacement,
|
|
524
657
|
placements: { ...extensionStatuses.placements },
|
|
525
658
|
colorModes: { ...extensionStatuses.colorModes },
|
|
526
659
|
},
|
|
660
|
+
fixedEditor,
|
|
527
661
|
};
|
|
528
662
|
}
|
|
529
663
|
|
|
@@ -709,3 +843,21 @@ export function saveExtensionStatusColorMode(
|
|
|
709
843
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
710
844
|
return mergeConfig(record);
|
|
711
845
|
}
|
|
846
|
+
|
|
847
|
+
export function saveFixedEditorPatch(
|
|
848
|
+
patch: Partial<FixedEditorConfig>,
|
|
849
|
+
path = configPath,
|
|
850
|
+
): PolishedTuiConfig {
|
|
851
|
+
const record = readConfigRecord(path);
|
|
852
|
+
const existing = isRecord(record.fixedEditor)
|
|
853
|
+
? { ...(record.fixedEditor as Record<string, unknown>) }
|
|
854
|
+
: {};
|
|
855
|
+
record.fixedEditor = {
|
|
856
|
+
...existing,
|
|
857
|
+
...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
|
|
858
|
+
...(patch.mouseScroll !== undefined ? { mouseScroll: patch.mouseScroll } : {}),
|
|
859
|
+
...(patch.copyNotice !== undefined ? { copyNotice: patch.copyNotice } : {}),
|
|
860
|
+
};
|
|
861
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
862
|
+
return mergeConfig(record);
|
|
863
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster discovery and rendering for the fixed editor.
|
|
3
|
+
*
|
|
4
|
+
* The "cluster" is the set of Pi TUI children around the editor that should be
|
|
5
|
+
* pinned at the bottom: status container, above-editor widget, editor,
|
|
6
|
+
* below-editor widget, and footer.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { CURSOR_MARKER, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
|
+
|
|
13
|
+
import type { ClusterRender } from "./types";
|
|
14
|
+
|
|
15
|
+
/** Minimal Component shape needed for rendering. */
|
|
16
|
+
type Renderable = {
|
|
17
|
+
render(width: number): string[];
|
|
18
|
+
/** Saved original render when the compositor has patched render → [] */
|
|
19
|
+
__zentuiOriginalRender?: (width: number) => string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Minimal Container shape for child scanning. */
|
|
23
|
+
type ContainerLike = Renderable & {
|
|
24
|
+
children: unknown[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Check if a value is a container-like object (has children + render). */
|
|
28
|
+
function isContainerLike(value: unknown): value is ContainerLike {
|
|
29
|
+
return (
|
|
30
|
+
typeof value === "object" &&
|
|
31
|
+
value !== null &&
|
|
32
|
+
Array.isArray(Reflect.get(value, "children")) &&
|
|
33
|
+
typeof Reflect.get(value, "render") === "function"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Check if a value looks like an editor component (duck-typed). */
|
|
38
|
+
function isEditorLike(value: unknown): boolean {
|
|
39
|
+
return (
|
|
40
|
+
typeof value === "object" &&
|
|
41
|
+
value !== null &&
|
|
42
|
+
typeof Reflect.get(value, "getText") === "function" &&
|
|
43
|
+
typeof Reflect.get(value, "setText") === "function" &&
|
|
44
|
+
typeof Reflect.get(value, "handleInput") === "function"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find the index in `children` of the container holding the editor.
|
|
50
|
+
* Prefers the focused component's parent; falls back to scanning for
|
|
51
|
+
* an editor-like grandchild.
|
|
52
|
+
*/
|
|
53
|
+
export function findEditorContainerIndex(
|
|
54
|
+
children: unknown[],
|
|
55
|
+
focusedComponent?: unknown,
|
|
56
|
+
): number | undefined {
|
|
57
|
+
// Try focused component first.
|
|
58
|
+
if (focusedComponent && typeof focusedComponent === "object") {
|
|
59
|
+
const idx = children.findIndex(
|
|
60
|
+
(c) => isContainerLike(c) && c.children.includes(focusedComponent),
|
|
61
|
+
);
|
|
62
|
+
if (idx !== -1) return idx;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Scan for a container with an editor-like child.
|
|
66
|
+
const idx = children.findIndex(
|
|
67
|
+
(c) => isContainerLike(c) && c.children.some((gc) => isEditorLike(gc)),
|
|
68
|
+
);
|
|
69
|
+
return idx === -1 ? undefined : idx;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The 5-component cluster pinned at the bottom. */
|
|
73
|
+
export type FixedCluster = {
|
|
74
|
+
status: Renderable | null;
|
|
75
|
+
aboveWidget: Renderable | null;
|
|
76
|
+
editor: Renderable;
|
|
77
|
+
belowWidget: Renderable | null;
|
|
78
|
+
footer: Renderable | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Patch a cluster component's render to return [] (hide from transcript).
|
|
83
|
+
* Saves the original render for cluster painting.
|
|
84
|
+
*/
|
|
85
|
+
export function hideRenderable(component: Renderable | null): void {
|
|
86
|
+
if (!component || component.__zentuiOriginalRender) return;
|
|
87
|
+
component.__zentuiOriginalRender = component.render.bind(component);
|
|
88
|
+
component.render = () => [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Restore a cluster component's original render. */
|
|
92
|
+
export function restoreRenderable(component: Renderable | null): void {
|
|
93
|
+
if (!component?.__zentuiOriginalRender) return;
|
|
94
|
+
component.render = component.__zentuiOriginalRender;
|
|
95
|
+
delete component.__zentuiOriginalRender;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build the cluster from children around the editor index. */
|
|
99
|
+
export function buildCluster(children: unknown[], editorIdx: number): FixedCluster | null {
|
|
100
|
+
const editor = children[editorIdx];
|
|
101
|
+
if (!editor || typeof (editor as Renderable).render !== "function") return null;
|
|
102
|
+
return {
|
|
103
|
+
status: (children[editorIdx - 2] as Renderable | undefined) ?? null,
|
|
104
|
+
aboveWidget: (children[editorIdx - 1] as Renderable | undefined) ?? null,
|
|
105
|
+
editor: editor as Renderable,
|
|
106
|
+
belowWidget: (children[editorIdx + 1] as Renderable | undefined) ?? null,
|
|
107
|
+
footer: (children[editorIdx + 2] as Renderable | undefined) ?? null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Render a component at `width`, using the saved original render if hidden. */
|
|
112
|
+
function renderComponent(component: Renderable | null, width: number): string[] {
|
|
113
|
+
if (!component) return [];
|
|
114
|
+
const renderFn = component.__zentuiOriginalRender ?? component.render;
|
|
115
|
+
const lines = renderFn.call(component, width);
|
|
116
|
+
// Strip only trailing blank lines — internal blank lines (e.g. editor
|
|
117
|
+
// padding in copy-friendly mode) must be preserved.
|
|
118
|
+
let end = lines.length;
|
|
119
|
+
while (end > 0 && visibleWidth(lines[end - 1]) === 0) end--;
|
|
120
|
+
return lines.slice(0, Math.max(end, 1));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Cap editor lines to `maxLines`, keeping the cursor row visible.
|
|
125
|
+
* If the cursor marker is found, the window centers on it; otherwise
|
|
126
|
+
* the last `maxLines` are kept.
|
|
127
|
+
*/
|
|
128
|
+
export function capEditorLines(lines: string[], maxLines: number): string[] {
|
|
129
|
+
if (maxLines <= 0) return [];
|
|
130
|
+
if (lines.length <= maxLines) return lines;
|
|
131
|
+
|
|
132
|
+
const cursorRow = lines.findIndex((line) => line.includes(CURSOR_MARKER));
|
|
133
|
+
if (cursorRow !== -1) {
|
|
134
|
+
const start = Math.max(0, Math.min(cursorRow - maxLines + 1, lines.length - maxLines));
|
|
135
|
+
return lines.slice(start, start + maxLines);
|
|
136
|
+
}
|
|
137
|
+
return lines.slice(lines.length - maxLines);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sanitizeLines(lines: string[], width: number): string[] {
|
|
141
|
+
return lines.map((line) =>
|
|
142
|
+
visibleWidth(line) > width ? truncateToWidth(line, width, "", true) : line,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render the full cluster (status + widgets + editor + footer) and extract
|
|
148
|
+
* the cursor position from the CURSOR_MARKER.
|
|
149
|
+
*/
|
|
150
|
+
export function renderCluster(
|
|
151
|
+
cluster: FixedCluster,
|
|
152
|
+
width: number,
|
|
153
|
+
maxHeight: number,
|
|
154
|
+
): ClusterRender {
|
|
155
|
+
const w = Math.max(1, width);
|
|
156
|
+
const maxRows = Math.max(1, maxHeight - 1);
|
|
157
|
+
|
|
158
|
+
const statusLines = sanitizeLines(renderComponent(cluster.status, w), w);
|
|
159
|
+
const aboveLines = sanitizeLines(renderComponent(cluster.aboveWidget, w), w);
|
|
160
|
+
const editorSource = sanitizeLines(renderComponent(cluster.editor, w), w);
|
|
161
|
+
const belowLines = sanitizeLines(renderComponent(cluster.belowWidget, w), w);
|
|
162
|
+
const footerLines = sanitizeLines(renderComponent(cluster.footer, w), w);
|
|
163
|
+
|
|
164
|
+
const editorLines = capEditorLines(editorSource, maxRows);
|
|
165
|
+
let remaining = maxRows - editorLines.length;
|
|
166
|
+
|
|
167
|
+
const footer = footerLines.slice(-remaining);
|
|
168
|
+
remaining -= footer.length;
|
|
169
|
+
|
|
170
|
+
const below = belowLines.slice(-remaining);
|
|
171
|
+
remaining -= below.length;
|
|
172
|
+
|
|
173
|
+
const above = aboveLines.slice(-remaining);
|
|
174
|
+
remaining -= above.length;
|
|
175
|
+
|
|
176
|
+
const status = statusLines.slice(-remaining);
|
|
177
|
+
|
|
178
|
+
let allLines = [...status, ...above, ...editorLines, ...below, ...footer];
|
|
179
|
+
|
|
180
|
+
// Strip leading blank lines (e.g. empty status line above the editor border).
|
|
181
|
+
let start = 0;
|
|
182
|
+
while (start < allLines.length - 1 && visibleWidth(allLines[start]) === 0) start++;
|
|
183
|
+
allLines = allLines.slice(start);
|
|
184
|
+
|
|
185
|
+
let cursor: { row: number; col: number } | null = null;
|
|
186
|
+
const cleaned = allLines.map((line, row) => {
|
|
187
|
+
const markerIndex = line.indexOf(CURSOR_MARKER);
|
|
188
|
+
if (markerIndex === -1) return line;
|
|
189
|
+
cursor ??= { row, col: visibleWidth(line.slice(0, markerIndex)) };
|
|
190
|
+
return line.slice(0, markerIndex) + line.slice(markerIndex + CURSOR_MARKER.length);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return { lines: cleaned, cursor };
|
|
194
|
+
}
|