pi-zentui 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -167,6 +167,9 @@ Default config values — copy this and change any value you want:
|
|
|
167
167
|
"mode": "basename",
|
|
168
168
|
"depth": 0
|
|
169
169
|
},
|
|
170
|
+
"gitBranch": {
|
|
171
|
+
"maxLength": "full"
|
|
172
|
+
},
|
|
170
173
|
"icons": {
|
|
171
174
|
"mode": "auto",
|
|
172
175
|
"cwd": "",
|
|
@@ -276,11 +279,12 @@ Default config values — copy this and change any value you want:
|
|
|
276
279
|
- `separator`: controls the default footer layout and extension-status connectors: `pipe` (default, ` | `), `dot` (` · `), `chevron` (` › `), or `none` (one space). Cycle it from the `/zentui` **Layout** tab. This selects the separator glyph; `colors.separator` controls its color. Custom `footerFormat` literals and `$sep` keep their existing behavior.
|
|
277
280
|
- `contextThresholds`: `{ warning, error }` percentages (default `70` / `90`) that select contextNormal / contextWarning / contextError colors.
|
|
278
281
|
- `pathDisplay`: controls how the cwd/`$cwd` path is shown. `mode` is `basename` (default, last segment only) or `full` (path with home contracted to `~`). In `full` mode, `depth` keeps only the last N trailing directories (`0` = entire path after `~`, max `5`); when parents are dropped the path is prefixed with `…/` (Starship-style). The `/zentui` **Layout** tab cycles path mode and path depth (`0`–`5`; depth is ignored for basename). Example: `~/Projects/foo/bar` with `depth: 2` → `…/foo/bar`.
|
|
282
|
+
- `gitBranch.maxLength`: visible width of the built-in branch name and `$git_branch` / `$branch`. The default `full` preserves the complete name; any positive integer uses that width including the trailing `…`. `/zentui` **Layout** cycles `full`, `10`, `20`, `30`, `40`, and `50`; custom positive integers can be set in JSON.
|
|
279
283
|
- `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.
|
|
280
284
|
- `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
|
|
281
285
|
- `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.
|
|
282
286
|
- `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`.
|
|
283
|
-
- `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, separator, path display mode/depth, and icon mode; set or clear custom formats with `/zentui format`.
|
|
287
|
+
- `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, separator, path display mode/depth, branch length, and icon mode; set or clear custom formats with `/zentui format`.
|
|
284
288
|
- `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.
|
|
285
289
|
- `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.
|
|
286
290
|
- `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.
|
|
@@ -32,6 +32,12 @@ export type PathDisplayConfig = {
|
|
|
32
32
|
depth: number;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
export type GitBranchMaxLength = "full" | number;
|
|
36
|
+
|
|
37
|
+
export type GitBranchConfig = {
|
|
38
|
+
maxLength: GitBranchMaxLength;
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
export type ColorSourcesConfig = {
|
|
36
42
|
starship: ColorSource;
|
|
37
43
|
editor: ColorSource;
|
|
@@ -108,6 +114,7 @@ export type PolishedTuiConfig = {
|
|
|
108
114
|
contextStyle: ContextStyle;
|
|
109
115
|
contextThresholds: ContextThresholds;
|
|
110
116
|
pathDisplay: PathDisplayConfig;
|
|
117
|
+
gitBranch: GitBranchConfig;
|
|
111
118
|
icons: ResolvedIcons;
|
|
112
119
|
colors: {
|
|
113
120
|
cwd: ColorSpec;
|
|
@@ -201,6 +208,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
201
208
|
contextStyle: "text",
|
|
202
209
|
contextThresholds: { warning: 70, error: 90 },
|
|
203
210
|
pathDisplay: { mode: "basename", depth: 0 },
|
|
211
|
+
gitBranch: { maxLength: "full" },
|
|
204
212
|
icons: {
|
|
205
213
|
mode: "auto",
|
|
206
214
|
...NERD_DEFAULT_ICONS,
|
|
@@ -342,6 +350,20 @@ function parsePathDisplay(value: unknown): PathDisplayConfig {
|
|
|
342
350
|
return { mode, depth };
|
|
343
351
|
}
|
|
344
352
|
|
|
353
|
+
function normalizeGitBranchMaxLength(value: unknown): GitBranchMaxLength {
|
|
354
|
+
if (value === "full") return value;
|
|
355
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
356
|
+
return defaultConfig.gitBranch.maxLength;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseGitBranchConfig(value: unknown): GitBranchConfig {
|
|
360
|
+
const defaults = defaultConfig.gitBranch;
|
|
361
|
+
if (!isRecord(value)) return { ...defaults };
|
|
362
|
+
return {
|
|
363
|
+
maxLength: normalizeGitBranchMaxLength(value.maxLength),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
345
367
|
function stringValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
346
368
|
const value = record[key];
|
|
347
369
|
return typeof value === "string" ? value : undefined;
|
|
@@ -644,6 +666,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
644
666
|
const gitMetrics = isRecord(config.gitMetrics)
|
|
645
667
|
? normalizeGitMetricsConfig(config.gitMetrics as Record<string, unknown>)
|
|
646
668
|
: defaultConfig.gitMetrics;
|
|
669
|
+
const gitBranch = parseGitBranchConfig(config.gitBranch);
|
|
647
670
|
const fixedEditor = isRecord(config.fixedEditor)
|
|
648
671
|
? normalizeFixedEditorConfig(config.fixedEditor as Record<string, unknown>)
|
|
649
672
|
: defaultConfig.fixedEditor;
|
|
@@ -654,6 +677,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
654
677
|
contextStyle: parseContextStyle(config.contextStyle),
|
|
655
678
|
contextThresholds: parseContextThresholds(config.contextThresholds),
|
|
656
679
|
pathDisplay: parsePathDisplay(config.pathDisplay),
|
|
680
|
+
gitBranch,
|
|
657
681
|
icons: resolveConfiguredIcons(iconMode, iconOverrides),
|
|
658
682
|
colors: {
|
|
659
683
|
...defaultConfig.colors,
|
|
@@ -810,6 +834,21 @@ export function savePathDisplayPatch(
|
|
|
810
834
|
return mergeConfig(record);
|
|
811
835
|
}
|
|
812
836
|
|
|
837
|
+
export function saveGitBranchPatch(
|
|
838
|
+
patch: Partial<GitBranchConfig>,
|
|
839
|
+
path = configPath,
|
|
840
|
+
): PolishedTuiConfig {
|
|
841
|
+
const record = readConfigRecord(path);
|
|
842
|
+
const existing = isRecord(record.gitBranch)
|
|
843
|
+
? { ...(record.gitBranch as Record<string, unknown>) }
|
|
844
|
+
: {};
|
|
845
|
+
if (patch.maxLength !== undefined)
|
|
846
|
+
existing.maxLength = normalizeGitBranchMaxLength(patch.maxLength);
|
|
847
|
+
record.gitBranch = existing;
|
|
848
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
849
|
+
return mergeConfig(record);
|
|
850
|
+
}
|
|
851
|
+
|
|
813
852
|
export function saveExtensionStatusPlacement(
|
|
814
853
|
key: string,
|
|
815
854
|
placement: ExtensionStatusPlacement,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
buildSessionDurationLabel,
|
|
10
10
|
contextColorTier,
|
|
11
11
|
formatCwdLabel,
|
|
12
|
+
formatGitBranchText,
|
|
12
13
|
formatGitCommitSegment,
|
|
13
14
|
formatGitMetricsSegment,
|
|
14
15
|
formatOsLabel,
|
|
@@ -193,6 +194,9 @@ export function installFooter(
|
|
|
193
194
|
}),
|
|
194
195
|
);
|
|
195
196
|
const branch = state.branch;
|
|
197
|
+
const branchText = branch
|
|
198
|
+
? formatGitBranchText(branch, config.gitBranch.maxLength)
|
|
199
|
+
: undefined;
|
|
196
200
|
const contextUsage = ctx.getContextUsage();
|
|
197
201
|
const contextWindow = ctx.model?.contextWindow ?? contextUsage?.contextWindow;
|
|
198
202
|
const contextLabel = buildContextDisplayLabel({
|
|
@@ -252,7 +256,11 @@ export function installFooter(
|
|
|
252
256
|
case "cwd":
|
|
253
257
|
return cwdLabel;
|
|
254
258
|
case "git_branch":
|
|
255
|
-
return
|
|
259
|
+
return branchText
|
|
260
|
+
? gitIcon
|
|
261
|
+
? `${gitIcon} ${gitColor(branchText)}`
|
|
262
|
+
: gitColor(branchText)
|
|
263
|
+
: "";
|
|
256
264
|
case "git_status":
|
|
257
265
|
return statusBlock;
|
|
258
266
|
case "git_state":
|
|
@@ -378,8 +386,8 @@ export function installFooter(
|
|
|
378
386
|
};
|
|
379
387
|
const branchParts: string[] = [];
|
|
380
388
|
if (config.footerSegments.gitBranch) {
|
|
381
|
-
if (
|
|
382
|
-
branchParts.push("on", gitIcon, gitColor(
|
|
389
|
+
if (branchText) {
|
|
390
|
+
branchParts.push("on", gitIcon, gitColor(branchText));
|
|
383
391
|
} else if (state.commit?.detached) {
|
|
384
392
|
// `HEAD` uses git-branch style; `(hash)` uses git-commit style
|
|
385
393
|
// (bold green) per Starship `git_commit` format.
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { homedir, hostname, userInfo } from "node:os";
|
|
2
2
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
5
|
import type {
|
|
5
6
|
ColorSource,
|
|
6
7
|
ColorSpec,
|
|
7
8
|
ContextStyle,
|
|
8
9
|
ContextThresholds,
|
|
10
|
+
GitBranchMaxLength,
|
|
9
11
|
PathDisplayMode,
|
|
10
12
|
} from "./config";
|
|
11
13
|
import type { GitCommitInfo, GitMetricsInfo } from "./git";
|
|
@@ -391,6 +393,18 @@ export function formatCwdLabel(cwd: string, cwdIcon: string, options?: FormatCwd
|
|
|
391
393
|
return cwdIcon ? `${cwdIcon} ${pathText}` : pathText;
|
|
392
394
|
}
|
|
393
395
|
|
|
396
|
+
function stripAnsi(text: string): string {
|
|
397
|
+
return text.replace(/\u001B\[[0-9;]*m/g, "");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function formatGitBranchText(
|
|
401
|
+
branch: string,
|
|
402
|
+
maxLength: GitBranchMaxLength = "full",
|
|
403
|
+
): string {
|
|
404
|
+
if (maxLength === "full" || visibleWidth(branch) <= maxLength) return branch;
|
|
405
|
+
return stripAnsi(truncateToWidth(branch, maxLength, "…"));
|
|
406
|
+
}
|
|
407
|
+
|
|
394
408
|
export function formatUsernameHostLabel(icon: string): string {
|
|
395
409
|
try {
|
|
396
410
|
const user = userInfo().username;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ensureConfigExists,
|
|
14
14
|
type FixedEditorConfig,
|
|
15
15
|
type FooterSegmentsConfig,
|
|
16
|
+
type GitBranchConfig,
|
|
16
17
|
type IconMode,
|
|
17
18
|
loadConfig,
|
|
18
19
|
type PathDisplayConfig,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
saveFixedEditorPatch,
|
|
26
27
|
saveFooterFormatPatch,
|
|
27
28
|
saveFooterSegmentsPatch,
|
|
29
|
+
saveGitBranchPatch,
|
|
28
30
|
saveIconsModePatch,
|
|
29
31
|
savePathDisplayPatch,
|
|
30
32
|
saveSeparatorPatch,
|
|
@@ -457,6 +459,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
457
459
|
setPathDisplay(patch: Partial<PathDisplayConfig>) {
|
|
458
460
|
currentConfig = savePathDisplayPatch(patch);
|
|
459
461
|
},
|
|
462
|
+
setGitBranch(patch: Partial<GitBranchConfig>) {
|
|
463
|
+
currentConfig = saveGitBranchPatch(patch);
|
|
464
|
+
},
|
|
460
465
|
getActiveExtensionStatuses() {
|
|
461
466
|
return getActiveExtensionStatuses();
|
|
462
467
|
},
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
type ExtensionStatusPlacement,
|
|
18
18
|
type FixedEditorConfig,
|
|
19
19
|
type FooterSegmentsConfig,
|
|
20
|
+
type GitBranchConfig,
|
|
21
|
+
type GitBranchMaxLength,
|
|
20
22
|
getExtensionStatusColorMode,
|
|
21
23
|
getExtensionStatusPlacement,
|
|
22
24
|
type IconMode,
|
|
@@ -45,6 +47,7 @@ const contextStyleValues: ContextStyle[] = ["text", "gauge", "text+gauge"];
|
|
|
45
47
|
const separatorStyleValues: SeparatorStyle[] = ["pipe", "dot", "chevron", "none"];
|
|
46
48
|
const pathDisplayModeValues: PathDisplayMode[] = ["basename", "full"];
|
|
47
49
|
const pathDepthValues = ["0", "1", "2", "3", "4", "5"] as const;
|
|
50
|
+
const branchLengthPresetValues = ["full", "10", "20", "30", "40", "50"] as const;
|
|
48
51
|
const iconModeValues: IconMode[] = ["auto", "nerd", "ascii"];
|
|
49
52
|
type FeatureState = "enabled" | "disabled";
|
|
50
53
|
|
|
@@ -61,7 +64,13 @@ type ColorSettingId = "starship" | "editorMessages";
|
|
|
61
64
|
type FeatureSettingId = keyof UiFeaturesConfig;
|
|
62
65
|
type FooterSegmentSettingId = keyof FooterSegmentsConfig;
|
|
63
66
|
type SettingsSection = (typeof settingsSections)[number];
|
|
64
|
-
type LayoutSettingId =
|
|
67
|
+
type LayoutSettingId =
|
|
68
|
+
| "contextStyle"
|
|
69
|
+
| "separator"
|
|
70
|
+
| "pathDisplay"
|
|
71
|
+
| "pathDepth"
|
|
72
|
+
| "branchLength"
|
|
73
|
+
| "iconMode";
|
|
65
74
|
|
|
66
75
|
type SettingsCommandDeps = {
|
|
67
76
|
getConfig: () => PolishedTuiConfig;
|
|
@@ -76,6 +85,7 @@ type SettingsCommandDeps = {
|
|
|
76
85
|
setContextStyle: (style: ContextStyle) => void;
|
|
77
86
|
setSeparator: (separator: SeparatorStyle) => void;
|
|
78
87
|
setPathDisplay: (patch: Partial<PathDisplayConfig>) => void;
|
|
88
|
+
setGitBranch: (patch: Partial<GitBranchConfig>) => void;
|
|
79
89
|
getActiveExtensionStatuses: () => ReadonlyMap<string, string>;
|
|
80
90
|
setExtensionStatusPlacement: (key: string, placement: ExtensionStatusPlacement) => void;
|
|
81
91
|
setExtensionStatusColorMode: (key: string, colorMode: ExtensionStatusColorMode) => void;
|
|
@@ -228,12 +238,26 @@ function isPathDepthValue(value: string): boolean {
|
|
|
228
238
|
return (pathDepthValues as readonly string[]).includes(value);
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
function parseGitBranchLengthValue(value: string): GitBranchMaxLength | undefined {
|
|
242
|
+
if (value === "full") return value;
|
|
243
|
+
const parsed = Number(value);
|
|
244
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function branchLengthValues(maxLength: GitBranchMaxLength): string[] {
|
|
248
|
+
const current = String(maxLength);
|
|
249
|
+
return (branchLengthPresetValues as readonly string[]).includes(current)
|
|
250
|
+
? [...branchLengthPresetValues]
|
|
251
|
+
: [current, ...branchLengthPresetValues];
|
|
252
|
+
}
|
|
253
|
+
|
|
231
254
|
function isLayoutSettingId(value: string): value is LayoutSettingId {
|
|
232
255
|
return (
|
|
233
256
|
value === "contextStyle" ||
|
|
234
257
|
value === "separator" ||
|
|
235
258
|
value === "pathDisplay" ||
|
|
236
259
|
value === "pathDepth" ||
|
|
260
|
+
value === "branchLength" ||
|
|
237
261
|
value === "iconMode"
|
|
238
262
|
);
|
|
239
263
|
}
|
|
@@ -464,6 +488,13 @@ function buildItems(
|
|
|
464
488
|
currentValue: String(config.pathDisplay.depth),
|
|
465
489
|
values: [...pathDepthValues],
|
|
466
490
|
},
|
|
491
|
+
{
|
|
492
|
+
id: "branchLength",
|
|
493
|
+
label: "Branch length",
|
|
494
|
+
description: "Show the full branch name or truncate it to a preset visible width.",
|
|
495
|
+
currentValue: String(config.gitBranch.maxLength),
|
|
496
|
+
values: branchLengthValues(config.gitBranch.maxLength),
|
|
497
|
+
},
|
|
467
498
|
{
|
|
468
499
|
id: "iconMode",
|
|
469
500
|
label: "Icon mode",
|
|
@@ -718,6 +749,17 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
718
749
|
return;
|
|
719
750
|
}
|
|
720
751
|
|
|
752
|
+
if (id === "branchLength") {
|
|
753
|
+
const maxLength = parseGitBranchLengthValue(newValue);
|
|
754
|
+
if (maxLength === undefined) return;
|
|
755
|
+
deps.setGitBranch({ maxLength });
|
|
756
|
+
settingsList.updateValue(id, newValue);
|
|
757
|
+
deps.requestRender();
|
|
758
|
+
ctx.ui.notify(`Branch length: ${newValue}`, "info");
|
|
759
|
+
tui.requestRender();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
721
763
|
if (id === "iconMode" && isIconMode(newValue)) {
|
|
722
764
|
deps.setIconMode(newValue);
|
|
723
765
|
settingsList.updateValue(id, newValue);
|