pi-zentui 0.2.7 → 0.3.1

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
@@ -22,6 +22,7 @@ Zentui brings two popular aesthetics to Pi:
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
24
  - Right side shows context usage, token counts, and cost
25
+ - Built-in footer segments can be shown or hidden individually from `/zentui`
25
26
  - Third-party Pi extension statuses from `ctx.ui.setStatus()` can be shown on the left,
26
27
  middle, or right side, or hidden per status key from `/zentui`
27
28
 
@@ -124,9 +125,9 @@ pi install git:github.com/lmilojevicc/pi-zentui
124
125
 
125
126
  ## Config
126
127
 
127
- User config lives at `~/.pi/agent/zentui.json`. The file is optional: missing or invalid known values fall back to Zentui defaults, unknown keys are ignored at runtime, and `/zentui` can patch color-source settings, UI feature toggles, and active third-party status placements.
128
+ User config lives at `~/.pi/agent/zentui.json`. The file is optional: missing or invalid known values fall back to Zentui defaults, unknown keys are ignored at runtime, and `/zentui` can patch color-source settings, UI feature toggles, built-in footer segment visibility, and active third-party status placements.
128
129
 
129
- The interactive `/zentui` menu is split into three sections. Use `Tab` to switch between `Coloring`, `Features`, and `Status line`.
130
+ The interactive `/zentui` menu is split into four sections. Use `Tab` and `Shift+Tab` to switch between `Coloring`, `Features`, `Built-in segments`, and `Extension segments`.
130
131
 
131
132
  Useful slash-command shortcuts:
132
133
 
@@ -198,6 +199,15 @@ Default config values — copy this and change any value you want:
198
199
  "statusLine": true,
199
200
  "copyFriendly": false
200
201
  },
202
+ "footerSegments": {
203
+ "cwd": true,
204
+ "gitBranch": true,
205
+ "gitStatus": true,
206
+ "runtime": true,
207
+ "context": true,
208
+ "tokens": true,
209
+ "cost": true
210
+ },
201
211
  "extensionStatuses": {
202
212
  "defaultPlacement": "right",
203
213
  "placements": {},
@@ -211,7 +221,8 @@ Default config values — copy this and change any value you want:
211
221
  - `icons`: every shown icon key is configurable; omit any key to use the Zentui default. `editorPrompt` controls an optional copy-friendly editor prompt glyph; the default is `""` so copy-friendly mode stays rail-free.
212
222
  - `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
213
223
  - `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.
214
- - `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`. `/zentui` lists only statuses that are currently active.
224
+ - `footerSegments`: show or hide individual built-in footer segments (`cwd`, `gitBranch`, `gitStatus`, `runtime`, `context`, `tokens`, `cost`). Toggle them from the `Built-in segments` tab in `/zentui`.
225
+ - `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.
215
226
  - 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`.
216
227
  - `editorAccent` styles the active editor rail and previous user-message rail when `features.copyFriendly` is disabled.
217
228
  - `editorPrompt` styles the copy-friendly editor prompt glyph. Omit it to use `editorAccent`, then the default accent fallback.
@@ -222,7 +233,7 @@ Tip: when using copy-friendly mode, setting Pi's `editorPaddingX` to `1` in `~/.
222
233
 
223
234
  ## Requirements
224
235
 
225
- - [Pi](https://pi.dev) coding agent 0.74 or newer
236
+ - [Pi](https://pi.dev) coding agent 0.79 or newer
226
237
  - A [Nerd Font](https://www.nerdfonts.com/) for icons
227
238
 
228
239
  ## Development
@@ -18,6 +18,16 @@ export type UiFeaturesConfig = {
18
18
  copyFriendly: boolean;
19
19
  };
20
20
 
21
+ export type FooterSegmentsConfig = {
22
+ cwd: boolean;
23
+ gitBranch: boolean;
24
+ gitStatus: boolean;
25
+ runtime: boolean;
26
+ context: boolean;
27
+ tokens: boolean;
28
+ cost: boolean;
29
+ };
30
+
21
31
  export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
22
32
  export type ExtensionStatusColorMode = "zentui" | "original";
23
33
 
@@ -77,6 +87,7 @@ export type PolishedTuiConfig = {
77
87
  };
78
88
  colorSources: ColorSourcesConfig;
79
89
  features: UiFeaturesConfig;
90
+ footerSegments: FooterSegmentsConfig;
80
91
  extensionStatuses: ExtensionStatusesConfig;
81
92
  };
82
93
 
@@ -124,6 +135,15 @@ export const defaultConfig: PolishedTuiConfig = {
124
135
  statusLine: true,
125
136
  copyFriendly: false,
126
137
  },
138
+ footerSegments: {
139
+ cwd: true,
140
+ gitBranch: true,
141
+ gitStatus: true,
142
+ runtime: true,
143
+ context: true,
144
+ tokens: true,
145
+ cost: true,
146
+ },
127
147
  extensionStatuses: {
128
148
  defaultPlacement: "right",
129
149
  placements: {},
@@ -190,6 +210,14 @@ function booleanValue(record: Record<string, unknown>, key: keyof UiFeaturesConf
190
210
  return typeof value === "boolean" ? value : defaultConfig.features[key];
191
211
  }
192
212
 
213
+ function footerSegmentValue(
214
+ record: Record<string, unknown>,
215
+ key: keyof FooterSegmentsConfig,
216
+ ): boolean {
217
+ const value = record[key];
218
+ return typeof value === "boolean" ? value : defaultConfig.footerSegments[key];
219
+ }
220
+
193
221
  function definedColors(
194
222
  colors: Partial<Record<keyof PolishedTuiConfig["colors"], string | undefined>>,
195
223
  ): Partial<PolishedTuiConfig["colors"]> {
@@ -252,6 +280,18 @@ function normalizeUiFeatures(record: Record<string, unknown>): UiFeaturesConfig
252
280
  };
253
281
  }
254
282
 
283
+ function normalizeFooterSegments(record: Record<string, unknown>): FooterSegmentsConfig {
284
+ return {
285
+ cwd: footerSegmentValue(record, "cwd"),
286
+ gitBranch: footerSegmentValue(record, "gitBranch"),
287
+ gitStatus: footerSegmentValue(record, "gitStatus"),
288
+ runtime: footerSegmentValue(record, "runtime"),
289
+ context: footerSegmentValue(record, "context"),
290
+ tokens: footerSegmentValue(record, "tokens"),
291
+ cost: footerSegmentValue(record, "cost"),
292
+ };
293
+ }
294
+
255
295
  export function isExtensionStatusPlacement(value: unknown): value is ExtensionStatusPlacement {
256
296
  return value === "off" || value === "left" || value === "middle" || value === "right";
257
297
  }
@@ -296,6 +336,18 @@ function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
296
336
  return value === "editor" || value === "statusLine" || value === "copyFriendly";
297
337
  }
298
338
 
339
+ function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig {
340
+ return (
341
+ value === "cwd" ||
342
+ value === "gitBranch" ||
343
+ value === "gitStatus" ||
344
+ value === "runtime" ||
345
+ value === "context" ||
346
+ value === "tokens" ||
347
+ value === "cost"
348
+ );
349
+ }
350
+
299
351
  function validColorSourceEntries(record: Record<string, unknown>): Partial<ColorSourcesConfig> {
300
352
  return Object.fromEntries(
301
353
  Object.entries(record).filter((entry): entry is [keyof ColorSourcesConfig, ColorSource] => {
@@ -314,6 +366,15 @@ function validUiFeatureEntries(record: Record<string, unknown>): Partial<UiFeatu
314
366
  ) as Partial<UiFeaturesConfig>;
315
367
  }
316
368
 
369
+ function validFooterSegmentEntries(record: Record<string, unknown>): Partial<FooterSegmentsConfig> {
370
+ return Object.fromEntries(
371
+ Object.entries(record).filter((entry): entry is [keyof FooterSegmentsConfig, boolean] => {
372
+ const [key, value] = entry;
373
+ return isFooterSegmentKey(key) && typeof value === "boolean";
374
+ }),
375
+ ) as Partial<FooterSegmentsConfig>;
376
+ }
377
+
317
378
  function readConfigRecord(path = configPath): ConfigRecord {
318
379
  try {
319
380
  if (!existsSync(path)) return {};
@@ -345,6 +406,9 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
345
406
  const features = isRecord(config.features)
346
407
  ? normalizeUiFeatures(config.features as Record<string, unknown>)
347
408
  : defaultConfig.features;
409
+ const footerSegments = isRecord(config.footerSegments)
410
+ ? normalizeFooterSegments(config.footerSegments as Record<string, unknown>)
411
+ : defaultConfig.footerSegments;
348
412
  const extensionStatuses = isRecord(config.extensionStatuses)
349
413
  ? normalizeExtensionStatuses(config.extensionStatuses as Record<string, unknown>)
350
414
  : defaultConfig.extensionStatuses;
@@ -360,6 +424,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
360
424
  },
361
425
  colorSources: { ...colorSources },
362
426
  features: { ...features },
427
+ footerSegments: { ...footerSegments },
363
428
  extensionStatuses: {
364
429
  defaultPlacement: extensionStatuses.defaultPlacement,
365
430
  placements: { ...extensionStatuses.placements },
@@ -423,6 +488,22 @@ export function saveUiFeaturesPatch(
423
488
  return mergeConfig(record);
424
489
  }
425
490
 
491
+ export function saveFooterSegmentsPatch(
492
+ patch: Partial<FooterSegmentsConfig>,
493
+ path = configPath,
494
+ ): PolishedTuiConfig {
495
+ const record = readConfigRecord(path);
496
+ const existing = isRecord(record.footerSegments)
497
+ ? { ...(record.footerSegments as Record<string, unknown>) }
498
+ : {};
499
+ record.footerSegments = {
500
+ ...existing,
501
+ ...validFooterSegmentEntries(patch),
502
+ };
503
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
504
+ return mergeConfig(record);
505
+ }
506
+
426
507
  export function saveExtensionStatusPlacement(
427
508
  key: string,
428
509
  placement: ExtensionStatusPlacement,
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import type { PolishedTuiConfig } from "./config";
4
- import { type ExtensionStatusSegment, collectExtensionStatusSegments } from "./extension-status";
4
+ import { collectExtensionStatusSegments, type ExtensionStatusSegment } from "./extension-status";
5
5
  import { formatCwdLabel, formatRuntimeSegment } from "./format";
6
6
  import type { FooterState } from "./state";
7
7
  import { renderStyleForSource } from "./style";
@@ -196,24 +196,32 @@ export function installFooter(
196
196
  : "";
197
197
  const statusBlock =
198
198
  allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
199
- const branchLabel = branch
200
- ? [...["on", gitIcon, gitColor(branch)].filter(Boolean), statusBlock]
201
- .filter(Boolean)
202
- .join(" ")
199
+ const branchParts =
200
+ config.footerSegments.gitBranch && branch
201
+ ? ["on", gitIcon, gitColor(branch)].filter(Boolean)
202
+ : [];
203
+ const gitStatusParts = config.footerSegments.gitStatus && statusBlock ? [statusBlock] : [];
204
+ const branchLabel = [...branchParts, ...gitStatusParts].filter(Boolean).join(" ");
205
+ const runtimeLabel = config.footerSegments.runtime
206
+ ? formatRuntimeSegment(theme, state.runtime, config.colors.runtimePrefix, colorSource)
203
207
  : "";
204
- const runtimeLabel = formatRuntimeSegment(
205
- theme,
206
- state.runtime,
207
- config.colors.runtimePrefix,
208
- colorSource,
209
- );
210
208
 
211
- const left = [cwdLabel, branchLabel, runtimeLabel].filter(Boolean).join(" ");
209
+ const left = [config.footerSegments.cwd ? cwdLabel : "", branchLabel, runtimeLabel]
210
+ .filter(Boolean)
211
+ .join(" ");
212
212
  const right = [
213
- renderStyleForSource(theme, colorSource, contextColor, state.contextLabel),
214
- renderStyleForSource(theme, colorSource, config.colors.tokens, state.tokenLabel),
215
- renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel),
216
- ].join(separator);
213
+ config.footerSegments.context
214
+ ? renderStyleForSource(theme, colorSource, contextColor, state.contextLabel)
215
+ : "",
216
+ config.footerSegments.tokens
217
+ ? renderStyleForSource(theme, colorSource, config.colors.tokens, state.tokenLabel)
218
+ : "",
219
+ config.footerSegments.cost
220
+ ? renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel)
221
+ : "",
222
+ ]
223
+ .filter(Boolean)
224
+ .join(separator);
217
225
  const extensionStatuses = collectExtensionStatusSegments(
218
226
  footerData.getExtensionStatuses(),
219
227
  config,
@@ -9,27 +9,29 @@ import {
9
9
  type ColorSourcesConfig,
10
10
  type ExtensionStatusColorMode,
11
11
  type ExtensionStatusPlacement,
12
- type PolishedTuiConfig,
13
- type UiFeaturesConfig,
14
12
  ensureConfigExists,
13
+ type FooterSegmentsConfig,
15
14
  loadConfig,
15
+ type PolishedTuiConfig,
16
16
  saveColorSourcesPatch,
17
17
  saveExtensionStatusColorMode,
18
18
  saveExtensionStatusPlacement,
19
+ saveFooterSegmentsPatch,
19
20
  saveUiFeaturesPatch,
21
+ type UiFeaturesConfig,
20
22
  } from "./config";
21
23
  import { installFooter } from "./footer";
22
24
  import { emptyGitStatus, readGitStatus } from "./git";
23
25
  import {
26
+ createProjectRefreshScheduler,
24
27
  type ScheduleProjectRefreshOptions,
25
28
  type StopProjectRefreshInterval,
26
- createProjectRefreshScheduler,
27
29
  startProjectRefreshInterval,
28
30
  } from "./project-refresh";
29
31
  import { readRuntimeInfo } from "./runtime";
30
32
  import { installSelectorBorderStyle } from "./selector-border";
31
33
  import { registerZentuiSettingsCommand } from "./settings-command";
32
- import { type FooterState, createInitialState, syncState } from "./state";
34
+ import { createInitialState, type FooterState, syncState } from "./state";
33
35
  import { PolishedEditor, WrappedPolishedEditor } from "./ui";
34
36
  import { installUserMessageStyle } from "./user-message";
35
37
 
@@ -58,8 +60,12 @@ function getZentuiEditorBaseFactory(factory: EditorFactory | undefined): EditorF
58
60
  }
59
61
 
60
62
  function isTuiContext(ctx: ExtensionContext): boolean {
61
- const mode = (ctx as ExtensionContext & { mode?: string }).mode;
62
- return ctx.hasUI && (mode === undefined || mode === "tui");
63
+ try {
64
+ const mode = (ctx as ExtensionContext & { mode?: string }).mode;
65
+ return ctx.hasUI && (mode === undefined || mode === "tui");
66
+ } catch {
67
+ return false;
68
+ }
63
69
  }
64
70
 
65
71
  export default function (pi: ExtensionAPI) {
@@ -341,6 +347,9 @@ export default function (pi: ExtensionAPI) {
341
347
  : undefined,
342
348
  };
343
349
  },
350
+ setFooterSegments(patch: Partial<FooterSegmentsConfig>) {
351
+ currentConfig = saveFooterSegmentsPatch(patch);
352
+ },
344
353
  getActiveExtensionStatuses() {
345
354
  return getActiveExtensionStatuses();
346
355
  },
@@ -2,6 +2,8 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  type AutocompleteItem,
5
+ Key,
6
+ matchesKey,
5
7
  type SettingItem,
6
8
  SettingsList,
7
9
  type SettingsListTheme,
@@ -12,12 +14,13 @@ import {
12
14
  type ColorSourcesConfig,
13
15
  type ExtensionStatusColorMode,
14
16
  type ExtensionStatusPlacement,
15
- type PolishedTuiConfig,
16
- type UiFeaturesConfig,
17
+ type FooterSegmentsConfig,
17
18
  getExtensionStatusColorMode,
18
19
  getExtensionStatusPlacement,
19
20
  isExtensionStatusColorMode,
20
21
  isExtensionStatusPlacement,
22
+ type PolishedTuiConfig,
23
+ type UiFeaturesConfig,
21
24
  } from "./config";
22
25
  import { sanitizeExtensionStatusText } from "./extension-status";
23
26
  import { EDITOR_BORDER_STYLE, renderChromeBorder, safeThemeFg } from "./style";
@@ -33,10 +36,11 @@ const extensionStatusColorModeValues: ExtensionStatusColorMode[] = ["zentui", "o
33
36
  type FeatureState = "enabled" | "disabled";
34
37
 
35
38
  const featureStateValues: FeatureState[] = ["enabled", "disabled"];
36
- const settingsSections = ["coloring", "features", "statusLine"] as const;
39
+ const settingsSections = ["coloring", "features", "builtinSegments", "extensionSegments"] as const;
37
40
 
38
41
  type ColorSettingId = "starship" | "editorMessages";
39
42
  type FeatureSettingId = keyof UiFeaturesConfig;
43
+ type FooterSegmentSettingId = keyof FooterSegmentsConfig;
40
44
  type SettingsSection = (typeof settingsSections)[number];
41
45
 
42
46
  type SettingsCommandDeps = {
@@ -46,6 +50,7 @@ type SettingsCommandDeps = {
46
50
  patch: Partial<UiFeaturesConfig>,
47
51
  ctx: ExtensionContext,
48
52
  ) => { applied: boolean; reason?: string };
53
+ setFooterSegments: (patch: Partial<FooterSegmentsConfig>) => void;
49
54
  getActiveExtensionStatuses: () => ReadonlyMap<string, string>;
50
55
  setExtensionStatusPlacement: (key: string, placement: ExtensionStatusPlacement) => void;
51
56
  setExtensionStatusColorMode: (key: string, colorMode: ExtensionStatusColorMode) => void;
@@ -79,6 +84,26 @@ const featureSettingDescriptions: Record<FeatureSettingId, string> = {
79
84
  "Hide editor and previous-message rail glyphs for cleaner native terminal selection.",
80
85
  };
81
86
 
87
+ const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
88
+ cwd: "Current directory",
89
+ gitBranch: "Git branch",
90
+ gitStatus: "Git status",
91
+ runtime: "Runtime",
92
+ context: "Context usage",
93
+ tokens: "Token counts",
94
+ cost: "Session cost",
95
+ };
96
+
97
+ const footerSegmentSettingDescriptions: Record<FooterSegmentSettingId, string> = {
98
+ cwd: "Show or hide the current working directory segment on the left.",
99
+ gitBranch: "Show or hide the git branch name on the left.",
100
+ gitStatus: "Show or hide git status icons and ahead/behind markers.",
101
+ runtime: "Show or hide the detected runtime/language segment on the left.",
102
+ context: "Show or hide context usage on the right.",
103
+ tokens: "Show or hide input/output token counts on the right.",
104
+ cost: "Show or hide session cost on the right.",
105
+ };
106
+
82
107
  const directCommandSuggestions = [
83
108
  "editor enable",
84
109
  "editor disable",
@@ -94,10 +119,12 @@ const directCommandSuggestions = [
94
119
  const sectionLabels: Record<SettingsSection, string> = {
95
120
  coloring: "Coloring",
96
121
  features: "Features",
97
- statusLine: "Status line",
122
+ builtinSegments: "Built-in segments",
123
+ extensionSegments: "Extension segments",
98
124
  };
99
125
 
100
126
  const thirdPartyStatusSettingPrefix = "thirdPartyStatus:";
127
+ const footerSegmentSettingPrefix = "footerSegment:";
101
128
  type ThirdPartyStatusSettingKind = "placement" | "colorMode";
102
129
 
103
130
  function isColorSource(value: string): value is ColorSource {
@@ -112,6 +139,18 @@ function isFeatureSettingId(value: string): value is FeatureSettingId {
112
139
  return value === "editor" || value === "statusLine" || value === "copyFriendly";
113
140
  }
114
141
 
142
+ function isFooterSegmentSettingId(value: string): value is FooterSegmentSettingId {
143
+ return (
144
+ value === "cwd" ||
145
+ value === "gitBranch" ||
146
+ value === "gitStatus" ||
147
+ value === "runtime" ||
148
+ value === "context" ||
149
+ value === "tokens" ||
150
+ value === "cost"
151
+ );
152
+ }
153
+
115
154
  function isFeatureState(value: string): value is FeatureState {
116
155
  return value === "enabled" || value === "disabled";
117
156
  }
@@ -134,6 +173,23 @@ function featurePatch(id: FeatureSettingId, value: FeatureState): Partial<UiFeat
134
173
  return { [id]: value === "enabled" } as Partial<UiFeaturesConfig>;
135
174
  }
136
175
 
176
+ function footerSegmentSettingId(key: FooterSegmentSettingId): string {
177
+ return `${footerSegmentSettingPrefix}${key}`;
178
+ }
179
+
180
+ function footerSegmentSettingFromId(id: string): FooterSegmentSettingId | undefined {
181
+ if (!id.startsWith(footerSegmentSettingPrefix)) return undefined;
182
+ const key = id.slice(footerSegmentSettingPrefix.length);
183
+ return isFooterSegmentSettingId(key) ? key : undefined;
184
+ }
185
+
186
+ function footerSegmentPatch(
187
+ id: FooterSegmentSettingId,
188
+ value: FeatureState,
189
+ ): Partial<FooterSegmentsConfig> {
190
+ return { [id]: value === "enabled" } as Partial<FooterSegmentsConfig>;
191
+ }
192
+
137
193
  function usageText(): string {
138
194
  return "Usage: /zentui [editor|statusline|copy-friendly] [enable|disable|toggle]";
139
195
  }
@@ -229,6 +285,16 @@ function buildItems(
229
285
  }));
230
286
  }
231
287
 
288
+ if (section === "builtinSegments") {
289
+ return (Object.keys(footerSegmentSettingLabels) as FooterSegmentSettingId[]).map((key) => ({
290
+ id: footerSegmentSettingId(key),
291
+ label: footerSegmentSettingLabels[key],
292
+ description: footerSegmentSettingDescriptions[key],
293
+ currentValue: featureValue(config.footerSegments[key]),
294
+ values: featureStateValues,
295
+ }));
296
+ }
297
+
232
298
  const statuses = Array.from(activeStatuses.entries()).sort(([a], [b]) =>
233
299
  a < b ? -1 : a > b ? 1 : 0,
234
300
  );
@@ -237,8 +303,7 @@ function buildItems(
237
303
  {
238
304
  id: "noThirdPartyStatuses",
239
305
  label: "No active statuses",
240
- description:
241
- "This section only lists statuses currently published through ctx.ui.setStatus().",
306
+ description: "This tab only lists statuses currently published through ctx.ui.setStatus().",
242
307
  currentValue: "—",
243
308
  },
244
309
  ];
@@ -271,6 +336,14 @@ function nextSection(section: SettingsSection): SettingsSection {
271
336
  return settingsSections[(currentIndex + 1) % settingsSections.length] ?? "coloring";
272
337
  }
273
338
 
339
+ function previousSection(section: SettingsSection): SettingsSection {
340
+ const currentIndex = settingsSections.indexOf(section);
341
+ return (
342
+ settingsSections[(currentIndex - 1 + settingsSections.length) % settingsSections.length] ??
343
+ "coloring"
344
+ );
345
+ }
346
+
274
347
  function formatSectionTabs(
275
348
  activeSection: SettingsSection,
276
349
  theme: ExtensionContext["ui"]["theme"],
@@ -289,7 +362,7 @@ function withSectionFooter(lines: string[], theme: ExtensionContext["ui"]["theme
289
362
  next[index] = safeThemeFg(
290
363
  theme,
291
364
  "muted",
292
- " Enter/Space to change · Tab to switch sections · Esc to close",
365
+ " Enter/Space to change · Tab/Shift+Tab to switch sections · Esc to close",
293
366
  );
294
367
  break;
295
368
  }
@@ -384,6 +457,19 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
384
457
  return;
385
458
  }
386
459
 
460
+ const footerSegmentSetting = footerSegmentSettingFromId(id);
461
+ if (footerSegmentSetting && isFeatureState(newValue)) {
462
+ deps.setFooterSegments(footerSegmentPatch(footerSegmentSetting, newValue));
463
+ settingsList.updateValue(id, newValue);
464
+ deps.requestRender();
465
+ ctx.ui.notify(
466
+ `${footerSegmentSettingLabels[footerSegmentSetting]}: ${newValue}`,
467
+ "info",
468
+ );
469
+ tui.requestRender();
470
+ return;
471
+ }
472
+
387
473
  const thirdPartyStatusSetting = thirdPartyStatusSettingFromId(id);
388
474
  if (
389
475
  thirdPartyStatusSetting?.kind === "placement" &&
@@ -421,8 +507,9 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
421
507
  () => done(undefined),
422
508
  );
423
509
  settingsList = makeSettingsList();
424
- const switchSection = () => {
425
- activeSection = nextSection(activeSection);
510
+ const switchSection = (direction: "forward" | "backward") => {
511
+ activeSection =
512
+ direction === "forward" ? nextSection(activeSection) : previousSection(activeSection);
426
513
  settingsList = makeSettingsList();
427
514
  tui.requestRender();
428
515
  };
@@ -450,8 +537,12 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
450
537
  settingsList.invalidate();
451
538
  },
452
539
  handleInput(data: string) {
453
- if (data === "\t") {
454
- switchSection();
540
+ if (matchesKey(data, Key.tab)) {
541
+ switchSection("forward");
542
+ return;
543
+ }
544
+ if (matchesKey(data, Key.shift("tab"))) {
545
+ switchSection("backward");
455
546
  return;
456
547
  }
457
548
  settingsList.handleInput(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,8 +12,16 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/lmilojevicc/pi-zentui/issues"
14
14
  },
15
- "keywords": ["pi-package", "pi-extension", "pi-theme", "starship", "tui"],
16
- "files": ["extensions"],
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi-extension",
18
+ "pi-theme",
19
+ "starship",
20
+ "tui"
21
+ ],
22
+ "files": [
23
+ "extensions"
24
+ ],
17
25
  "scripts": {
18
26
  "lint": "biome check .",
19
27
  "typecheck": "tsc --noEmit",
@@ -32,18 +40,20 @@
32
40
  "@earendil-works/pi-tui": "*"
33
41
  },
34
42
  "pi": {
35
- "extensions": ["./extensions"],
43
+ "extensions": [
44
+ "./extensions"
45
+ ],
36
46
  "image": "https://raw.githubusercontent.com/lmilojevicc/pi-zentui/main/assets/zentui.png"
37
47
  },
38
48
  "publishConfig": {
39
49
  "access": "public"
40
50
  },
41
51
  "devDependencies": {
42
- "@biomejs/biome": "^1.9.4",
43
- "@earendil-works/pi-ai": "^0.74.0",
44
- "@earendil-works/pi-coding-agent": "^0.74.0",
45
- "@earendil-works/pi-tui": "^0.74.0",
46
- "@types/node": "^25.5.2",
52
+ "@biomejs/biome": "^2.5.0",
53
+ "@earendil-works/pi-ai": "^0.79.3",
54
+ "@earendil-works/pi-coding-agent": "^0.79.3",
55
+ "@earendil-works/pi-tui": "^0.79.3",
56
+ "@types/node": "^26.0.0",
47
57
  "typescript": "^6.0.2",
48
58
  "vitest": "^4.1.8"
49
59
  }