pi-zentui 0.5.0 → 0.6.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
@@ -17,7 +17,7 @@ Zentui brings two popular aesthetics to Pi:
17
17
 
18
18
  ### Footer (Starship-inspired)
19
19
 
20
- - `󰝰 dirname` — current directory with icon
20
+ - `󰝰 dirname` — current directory with icon (`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
@@ -158,6 +158,10 @@ Default config values — copy this and change any value you want:
158
158
  "warning": 70,
159
159
  "error": 90
160
160
  },
161
+ "pathDisplay": {
162
+ "mode": "basename",
163
+ "depth": 0
164
+ },
161
165
  "icons": {
162
166
  "mode": "auto",
163
167
  "cwd": "󰝰",
@@ -244,11 +248,12 @@ Default config values — copy this and change any value you want:
244
248
  - `projectRefreshIntervalMs`: project status polling interval; `0` disables polling. Values `1..4999` clamp up to `5000` (minimum 5s); invalid/non-finite values fall back to `30000`.
245
249
  - `contextStyle`: `text` (default), `gauge`, or `text+gauge` for the context segment.
246
250
  - `contextThresholds`: `{ warning, error }` percentages (default `70` / `90`) that select contextNormal / contextWarning / contextError colors.
251
+ - `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`.
247
252
  - `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.
248
253
  - `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
249
254
  - `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.
250
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`.
251
- - `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 and icon mode; set or clear custom formats with `/zentui format`.
256
+ - `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`.
252
257
  - `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.
253
258
  - 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`.
254
259
  - `editorAccent` styles the active editor rail and previous user-message rail when `features.copyFriendly` is disabled.
@@ -23,6 +23,14 @@ export type ContextThresholds = {
23
23
  error: number;
24
24
  };
25
25
 
26
+ export type PathDisplayMode = "basename" | "full";
27
+
28
+ export type PathDisplayConfig = {
29
+ mode: PathDisplayMode;
30
+ /** Trailing directories to show in full mode. 0 = unlimited; clamped to 0..5. */
31
+ depth: number;
32
+ };
33
+
26
34
  export type ColorSourcesConfig = {
27
35
  starship: ColorSource;
28
36
  editor: ColorSource;
@@ -69,6 +77,7 @@ export type PolishedTuiConfig = {
69
77
  footerFormat: string;
70
78
  contextStyle: ContextStyle;
71
79
  contextThresholds: ContextThresholds;
80
+ pathDisplay: PathDisplayConfig;
72
81
  icons: ResolvedIcons;
73
82
  colors: {
74
83
  cwd: ColorSpec;
@@ -144,6 +153,7 @@ export const defaultConfig: PolishedTuiConfig = {
144
153
  footerFormat: "",
145
154
  contextStyle: "text",
146
155
  contextThresholds: { warning: 70, error: 90 },
156
+ pathDisplay: { mode: "basename", depth: 0 },
147
157
  icons: {
148
158
  mode: "auto",
149
159
  ...NERD_DEFAULT_ICONS,
@@ -244,6 +254,18 @@ function parseContextThresholds(value: unknown): ContextThresholds {
244
254
  return { warning, error };
245
255
  }
246
256
 
257
+ function parsePathDisplay(value: unknown): PathDisplayConfig {
258
+ const defaults = defaultConfig.pathDisplay;
259
+ if (!isRecord(value)) return { ...defaults };
260
+ const mode = value.mode === "full" || value.mode === "basename" ? value.mode : defaults.mode;
261
+ const rawDepth = value.depth;
262
+ const depth =
263
+ typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 0
264
+ ? Math.min(5, Math.floor(rawDepth))
265
+ : defaults.depth;
266
+ return { mode, depth };
267
+ }
268
+
247
269
  function stringValue(record: Record<string, unknown>, key: string): string | undefined {
248
270
  const value = record[key];
249
271
  return typeof value === "string" ? value : undefined;
@@ -488,6 +510,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
488
510
  footerFormat: stringValue(config, "footerFormat") ?? "",
489
511
  contextStyle: parseContextStyle(config.contextStyle),
490
512
  contextThresholds: parseContextThresholds(config.contextThresholds),
513
+ pathDisplay: parsePathDisplay(config.pathDisplay),
491
514
  icons: resolveConfiguredIcons(iconMode, iconOverrides),
492
515
  colors: {
493
516
  ...defaultConfig.colors,
@@ -616,6 +639,21 @@ export function saveContextThresholdsPatch(
616
639
  return mergeConfig(record);
617
640
  }
618
641
 
642
+ export function savePathDisplayPatch(
643
+ patch: Partial<PathDisplayConfig>,
644
+ path = configPath,
645
+ ): PolishedTuiConfig {
646
+ const record = readConfigRecord(path);
647
+ const existing = isRecord(record.pathDisplay)
648
+ ? { ...(record.pathDisplay as Record<string, unknown>) }
649
+ : {};
650
+ if (patch.mode !== undefined) existing.mode = patch.mode;
651
+ if (patch.depth !== undefined) existing.depth = patch.depth;
652
+ record.pathDisplay = existing;
653
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
654
+ return mergeConfig(record);
655
+ }
656
+
619
657
  export function saveExtensionStatusPlacement(
620
658
  key: string,
621
659
  placement: ExtensionStatusPlacement,
@@ -172,7 +172,10 @@ export function installFooter(
172
172
  theme,
173
173
  colorSource,
174
174
  config.colors.cwd,
175
- formatCwdLabel(ctx.cwd, config.icons.cwd),
175
+ formatCwdLabel(ctx.cwd, config.icons.cwd, {
176
+ mode: config.pathDisplay.mode,
177
+ depth: config.pathDisplay.depth,
178
+ }),
176
179
  );
177
180
  const branch = state.branch;
178
181
  const contextUsage = ctx.getContextUsage();
@@ -1,7 +1,13 @@
1
- import { hostname, userInfo } from "node:os";
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 type { ColorSource, ColorSpec, ContextStyle, ContextThresholds } from "./config";
4
+ import type {
5
+ ColorSource,
6
+ ColorSpec,
7
+ ContextStyle,
8
+ ContextThresholds,
9
+ PathDisplayMode,
10
+ } from "./config";
5
11
  import type { IconMode } from "./icons";
6
12
  import { resolveOsIcon, resolveRuntimeSymbol } from "./icons";
7
13
  import type { RuntimeInfo } from "./runtime";
@@ -238,11 +244,72 @@ export function formatRuntimeSegment(
238
244
  return `${renderStyleForSource(theme, colorSource, prefixStyle, "via")} ${renderStyleForSource(theme, colorSource, runtime.style, label)}`;
239
245
  }
240
246
 
241
- export function formatCwdLabel(cwd: string, cwdIcon: string): string {
242
- const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
243
- const parts = normalized.split("/").filter(Boolean);
244
- const last = parts[parts.length - 1] ?? cwd;
245
- return cwdIcon ? `${cwdIcon} ${last}` : last;
247
+ export type FormatCwdOptions = {
248
+ mode?: PathDisplayMode;
249
+ /** Trailing directory components to keep in full mode. 0 = unlimited. */
250
+ depth?: number;
251
+ home?: string;
252
+ };
253
+
254
+ function normalizeDisplayPath(cwd: string): string {
255
+ const withSlashes = cwd.replace(/\\/g, "/");
256
+ if (withSlashes === "/" || /^\/+$/.test(withSlashes)) return "/";
257
+ const stripped = withSlashes.replace(/\/+$/, "");
258
+ return stripped === "" ? withSlashes : stripped;
259
+ }
260
+
261
+ function toHomePath(path: string, home: string): string {
262
+ if (!home) return path;
263
+ const homeNorm = home.replace(/\\/g, "/").replace(/\/+$/, "");
264
+ if (!homeNorm) return path;
265
+ if (path === homeNorm) return "~";
266
+ if (path.startsWith(`${homeNorm}/`)) return `~${path.slice(homeNorm.length)}`;
267
+ return path;
268
+ }
269
+
270
+ /** Starship-style: keep last `depth` components; prefix with `…/` when parents were dropped. */
271
+ function applyPathDepth(path: string, depth: number): string {
272
+ if (!Number.isFinite(depth) || depth <= 0) return path;
273
+ const limit = Math.floor(depth);
274
+ if (path === "~" || path === "/") return path;
275
+
276
+ let components: string[];
277
+ if (path.startsWith("~/")) {
278
+ components = path.slice(2).split("/").filter(Boolean);
279
+ } else if (/^[A-Za-z]:\//.test(path)) {
280
+ components = path.slice(3).split("/").filter(Boolean);
281
+ } else if (path.startsWith("/")) {
282
+ components = path.slice(1).split("/").filter(Boolean);
283
+ } else {
284
+ components = path.split("/").filter(Boolean);
285
+ }
286
+
287
+ if (components.length <= limit) return path;
288
+ return `…/${components.slice(-limit).join("/")}`;
289
+ }
290
+
291
+ export function formatCwdLabel(cwd: string, cwdIcon: string, options?: FormatCwdOptions): string {
292
+ const mode = options?.mode ?? "basename";
293
+ const normalized = normalizeDisplayPath(cwd);
294
+ let pathText: string;
295
+ if (mode === "full") {
296
+ const home =
297
+ options?.home ??
298
+ (() => {
299
+ try {
300
+ return homedir();
301
+ } catch {
302
+ return "";
303
+ }
304
+ })();
305
+ pathText = applyPathDepth(toHomePath(normalized, home), options?.depth ?? 0);
306
+ } else if (normalized === "/") {
307
+ pathText = "/";
308
+ } else {
309
+ const parts = normalized.split("/").filter(Boolean);
310
+ pathText = parts[parts.length - 1] ?? cwd;
311
+ }
312
+ return cwdIcon ? `${cwdIcon} ${pathText}` : pathText;
246
313
  }
247
314
 
248
315
  export function formatUsernameHostLabel(icon: string): string {
@@ -14,6 +14,7 @@ import {
14
14
  type FooterSegmentsConfig,
15
15
  type IconMode,
16
16
  loadConfig,
17
+ type PathDisplayConfig,
17
18
  type PolishedTuiConfig,
18
19
  saveColorSourcesPatch,
19
20
  saveContextStylePatch,
@@ -22,6 +23,7 @@ import {
22
23
  saveFooterFormatPatch,
23
24
  saveFooterSegmentsPatch,
24
25
  saveIconsModePatch,
26
+ savePathDisplayPatch,
25
27
  saveUiFeaturesPatch,
26
28
  type UiFeaturesConfig,
27
29
  } from "./config";
@@ -408,6 +410,9 @@ export default function (pi: ExtensionAPI) {
408
410
  setContextStyle(style: ContextStyle) {
409
411
  currentConfig = saveContextStylePatch(style);
410
412
  },
413
+ setPathDisplay(patch: Partial<PathDisplayConfig>) {
414
+ currentConfig = savePathDisplayPatch(patch);
415
+ },
411
416
  getActiveExtensionStatuses() {
412
417
  return getActiveExtensionStatuses();
413
418
  },
@@ -21,6 +21,8 @@ import {
21
21
  type IconMode,
22
22
  isExtensionStatusColorMode,
23
23
  isExtensionStatusPlacement,
24
+ type PathDisplayConfig,
25
+ type PathDisplayMode,
24
26
  type PolishedTuiConfig,
25
27
  type UiFeaturesConfig,
26
28
  } from "./config";
@@ -37,6 +39,8 @@ const extensionStatusPlacementValues: ExtensionStatusPlacement[] = [
37
39
  ];
38
40
  const extensionStatusColorModeValues: ExtensionStatusColorMode[] = ["zentui", "original"];
39
41
  const contextStyleValues: ContextStyle[] = ["text", "gauge", "text+gauge"];
42
+ const pathDisplayModeValues: PathDisplayMode[] = ["basename", "full"];
43
+ const pathDepthValues = ["0", "1", "2", "3", "4", "5"] as const;
40
44
  const iconModeValues: IconMode[] = ["auto", "nerd", "ascii"];
41
45
  type FeatureState = "enabled" | "disabled";
42
46
 
@@ -53,7 +57,7 @@ type ColorSettingId = "starship" | "editorMessages";
53
57
  type FeatureSettingId = keyof UiFeaturesConfig;
54
58
  type FooterSegmentSettingId = keyof FooterSegmentsConfig;
55
59
  type SettingsSection = (typeof settingsSections)[number];
56
- type LayoutSettingId = "contextStyle" | "iconMode";
60
+ type LayoutSettingId = "contextStyle" | "pathDisplay" | "pathDepth" | "iconMode";
57
61
 
58
62
  type SettingsCommandDeps = {
59
63
  getConfig: () => PolishedTuiConfig;
@@ -66,6 +70,7 @@ type SettingsCommandDeps = {
66
70
  setFooterFormat: (value: string) => void;
67
71
  setIconMode: (mode: IconMode) => void;
68
72
  setContextStyle: (style: ContextStyle) => void;
73
+ setPathDisplay: (patch: Partial<PathDisplayConfig>) => void;
69
74
  getActiveExtensionStatuses: () => ReadonlyMap<string, string>;
70
75
  setExtensionStatusPlacement: (key: string, placement: ExtensionStatusPlacement) => void;
71
76
  setExtensionStatusColorMode: (key: string, colorMode: ExtensionStatusColorMode) => void;
@@ -194,8 +199,21 @@ function isContextStyle(value: string): value is ContextStyle {
194
199
  return value === "text" || value === "gauge" || value === "text+gauge";
195
200
  }
196
201
 
202
+ function isPathDisplayMode(value: string): value is PathDisplayMode {
203
+ return value === "basename" || value === "full";
204
+ }
205
+
206
+ function isPathDepthValue(value: string): boolean {
207
+ return (pathDepthValues as readonly string[]).includes(value);
208
+ }
209
+
197
210
  function isLayoutSettingId(value: string): value is LayoutSettingId {
198
- return value === "contextStyle" || value === "iconMode";
211
+ return (
212
+ value === "contextStyle" ||
213
+ value === "pathDisplay" ||
214
+ value === "pathDepth" ||
215
+ value === "iconMode"
216
+ );
199
217
  }
200
218
 
201
219
  function editorMessageValue(config: PolishedTuiConfig): ColorSource | "mixed" {
@@ -349,6 +367,21 @@ function buildItems(
349
367
  currentValue: config.contextStyle,
350
368
  values: contextStyleValues,
351
369
  },
370
+ {
371
+ id: "pathDisplay",
372
+ label: "Path display",
373
+ description: "Show cwd as basename or full path (home contracted to ~).",
374
+ currentValue: config.pathDisplay.mode,
375
+ values: pathDisplayModeValues,
376
+ },
377
+ {
378
+ id: "pathDepth",
379
+ label: "Path depth",
380
+ description:
381
+ "In full mode, trailing directories to show (0 = all, max 5). Ignored for basename.",
382
+ currentValue: String(config.pathDisplay.depth),
383
+ values: [...pathDepthValues],
384
+ },
352
385
  {
353
386
  id: "iconMode",
354
387
  label: "Icon mode",
@@ -561,6 +594,24 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
561
594
  return;
562
595
  }
563
596
 
597
+ if (id === "pathDisplay" && isPathDisplayMode(newValue)) {
598
+ deps.setPathDisplay({ mode: newValue });
599
+ settingsList.updateValue(id, newValue);
600
+ deps.requestRender();
601
+ ctx.ui.notify(`Path display: ${newValue}`, "info");
602
+ tui.requestRender();
603
+ return;
604
+ }
605
+
606
+ if (id === "pathDepth" && isPathDepthValue(newValue)) {
607
+ deps.setPathDisplay({ depth: Number(newValue) });
608
+ settingsList.updateValue(id, newValue);
609
+ deps.requestRender();
610
+ ctx.ui.notify(`Path depth: ${newValue}`, "info");
611
+ tui.requestRender();
612
+ return;
613
+ }
614
+
564
615
  if (id === "iconMode" && isIconMode(newValue)) {
565
616
  deps.setIconMode(newValue);
566
617
  settingsList.updateValue(id, newValue);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",