pi-zentui 0.3.3 → 0.5.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
@@ -21,8 +21,10 @@ Zentui brings two popular aesthetics to Pi:
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 session duration
24
25
  - Right side shows context usage, token counts, and cost
25
26
  - Built-in footer segments can be shown or hidden individually from `/zentui`
27
+ - Fully custom Starship-style layout via a `footerFormat` template string — see [Footer Format Template](#footer-format-template)
26
28
  - Third-party Pi extension statuses from `ctx.ui.setStatus()` can be shown on the left,
27
29
  middle, or right side, or hidden per status key from `/zentui`
28
30
 
@@ -127,7 +129,7 @@ pi install git:github.com/lmilojevicc/pi-zentui
127
129
 
128
130
  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.
129
131
 
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`.
132
+ The interactive `/zentui` menu is split into five sections. Use `Tab` and `Shift+Tab` to switch between `Coloring`, `Features`, `Layout`, `Built-in segments`, and `Extension segments`.
131
133
 
132
134
  Useful slash-command shortcuts:
133
135
 
@@ -141,6 +143,8 @@ Useful slash-command shortcuts:
141
143
  /zentui copy-friendly enable
142
144
  /zentui copy-friendly disable
143
145
  /zentui copy-friendly toggle
146
+ /zentui format "$cwd on branch $git_branch$git_status using $runtime $fill $context"
147
+ /zentui format clear
144
148
  ```
145
149
 
146
150
  Default config values — copy this and change any value you want:
@@ -148,7 +152,14 @@ Default config values — copy this and change any value you want:
148
152
  ```json
149
153
  {
150
154
  "projectRefreshIntervalMs": 30000,
155
+ "footerFormat": "",
156
+ "contextStyle": "text",
157
+ "contextThresholds": {
158
+ "warning": 70,
159
+ "error": 90
160
+ },
151
161
  "icons": {
162
+ "mode": "auto",
152
163
  "cwd": "󰝰",
153
164
  "git": "",
154
165
  "ahead": "↑",
@@ -164,7 +175,10 @@ Default config values — copy this and change any value you want:
164
175
  "typechanged": "T",
165
176
  "cacheHit": "󰆼",
166
177
  "editorPrompt": "",
167
- "rail": "│"
178
+ "rail": "│",
179
+ "username": "",
180
+ "time": "",
181
+ "os": ""
168
182
  },
169
183
  "colors": {
170
184
  "cwd": "bold cyan",
@@ -178,6 +192,10 @@ Default config values — copy this and change any value you want:
178
192
  "extensionStatus": "bright-black",
179
193
  "separator": "bright-black",
180
194
  "runtimePrefix": "",
195
+ "sessionDuration": "yellow",
196
+ "username": "bold yellow",
197
+ "time": "bold yellow",
198
+ "os": "bold white",
181
199
  "editorAccent": "accent",
182
200
  "editorPrompt": "accent",
183
201
  "editorBorder": "borderMuted",
@@ -204,10 +222,15 @@ Default config values — copy this and change any value you want:
204
222
  "cwd": true,
205
223
  "gitBranch": true,
206
224
  "gitStatus": true,
225
+ "gitCounts": false,
207
226
  "runtime": true,
208
227
  "context": true,
209
228
  "tokens": true,
210
- "cost": true
229
+ "cost": true,
230
+ "sessionDuration": false,
231
+ "username": false,
232
+ "time": false,
233
+ "os": false
211
234
  },
212
235
  "extensionStatuses": {
213
236
  "defaultPlacement": "right",
@@ -217,12 +240,15 @@ Default config values — copy this and change any value you want:
217
240
  }
218
241
  ```
219
242
 
220
- - Style values can be Starship/terminal strings (`bold purple`, `fg:202`, `#89b4fa`, `bg:blue fg:bright-green`) or Pi theme tokens (`accent`, `borderMuted`, `thinkingHigh`).
221
- - `projectRefreshIntervalMs`: project status polling interval; `0` disables polling.
222
- - `icons`: every shown icon key is configurable; omit any key to use the Zentui default. `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.
243
+ - Style values can be Starship/terminal strings (`bold purple`, `fg:202`, `#89b` / `#89b4fa`, `bg:blue fg:bright-green`) or Pi theme tokens (`accent`, `borderMuted`, `thinkingHigh`). Short `#rgb` hex values expand to `#rrggbb`.
244
+ - `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
+ - `contextStyle`: `text` (default), `gauge`, or `text+gauge` for the context segment.
246
+ - `contextThresholds`: `{ warning, error }` percentages (default `70` / `90`) that select contextNormal / contextWarning / contextError colors.
247
+ - `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.
223
248
  - `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
224
249
  - `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.
225
- - `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`.
250
+ - `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`.
226
252
  - `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.
227
253
  - 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`.
228
254
  - `editorAccent` styles the active editor rail and previous user-message rail when `features.copyFriendly` is disabled.
@@ -232,10 +258,64 @@ Default config values — copy this and change any value you want:
232
258
 
233
259
  Tip: when using copy-friendly mode, setting Pi's `editorPaddingX` to `1` in `~/.pi/agent/settings.json` keeps a small left gutter without copying a rail glyph.
234
260
 
261
+ ## Footer Format Template
262
+
263
+ For full control, set a Starship-style `footerFormat` template string. It supports `$variable` and `${variable}` tokens, a special `$fill` token that splits the line into left and right zones, and conditional groups `( ... )` that drop entirely when every nested variable is empty. When set, it overrides the built-in `footerSegments` layout; when empty or omitted, the segment layout above is used.
264
+
265
+ A second `$fill` creates a **centered middle zone** — content between the two fills is true-centered (`floor((gap - middle) / 2)`), just like third-party statuses placed `middle`.
266
+
267
+ ```json
268
+ {
269
+ "footerFormat": "$os $username $cwd( on $git_branch)( $git_status)( via $runtime)$fill($context)($sep$tokens)($sep$cost)($sep$time)"
270
+ }
271
+ ```
272
+
273
+ Center the branch between directory and cost:
274
+
275
+ ```json
276
+ {
277
+ "footerFormat": "$cwd $fill $git_branch $fill $cost"
278
+ }
279
+ ```
280
+
281
+ ### Variables
282
+
283
+ | Token | Aliases | Renders |
284
+ | ------------------- | ------------ | ------------------------------------------------ |
285
+ | `$cwd` | `$directory` | current directory |
286
+ | `$git_branch` | `$branch` | git branch with icon |
287
+ | `$git_status` | `$status` | `[!?↑]` status block |
288
+ | `$git_state` | `$state` | `REBASING` / `MERGING` / … (optional `n/m`) |
289
+ | `$runtime` | | runtime icon + version |
290
+ | `$session_duration` | `$duration` | session running time |
291
+ | `$username` | | `user@host` |
292
+ | `$os` | | operating-system icon |
293
+ | `$time` | | current time `HH:MM` |
294
+ | `$context` | | context usage (text and/or gauge via config) |
295
+ | `$tokens` | | input/output token counts |
296
+ | `$cost` | | session cost |
297
+ | `$sep` | `$separator` | themed ` | ` using `colors.separator` |
298
+ | `$fill` | — | special: splits zones |
299
+
300
+ ### `$fill` behavior
301
+
302
+ | `$fill` count | Layout |
303
+ | ------------- | ------------------------------------------------------------------------ |
304
+ | 0 | everything left-aligned |
305
+ | 1 | tokens before → left, tokens after → right |
306
+ | 2 | before first → left, between → **centered middle**, after second → right |
307
+ | 3+ | first two count; extras ignored |
308
+
309
+ - Literal text (`on branch`, `using`, `\|`, spaces) is rendered verbatim — you control all spacing.
310
+ - Each variable renders its core value only (no `on`/`via` prefixes); add those words as literal text.
311
+ - Conditional groups: wrap optional pieces in parentheses, e.g. `$cwd( on $git_branch)($git_status)$fill($context)`. If every `$var` inside a group is empty, the whole group (including its literals) is dropped.
312
+ - Unknown `$variables` render empty.
313
+ - Set or clear at runtime: `/zentui format "<template>"` and `/zentui format clear`.
314
+
235
315
  ## Requirements
236
316
 
237
317
  - [Pi](https://pi.dev) coding agent 0.79 or newer
238
- - A [Nerd Font](https://www.nerdfonts.com/) for icons
318
+ - A [Nerd Font](https://www.nerdfonts.com/) for icons (or set `icons.mode` to `"ascii"`)
239
319
 
240
320
  ## Development
241
321
 
@@ -1,10 +1,27 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import {
5
+ ICON_GLYPH_KEYS,
6
+ type IconGlyphs,
7
+ type IconMode,
8
+ NERD_DEFAULT_ICONS,
9
+ normalizeIconMode,
10
+ type ResolvedIcons,
11
+ resolveConfiguredIcons,
12
+ } from "./icons";
4
13
  import { isSupportedColorSpec } from "./style";
5
14
 
6
15
  export type ColorSpec = string;
7
16
  export type ColorSource = "theme" | "terminal";
17
+ export type { IconMode } from "./icons";
18
+
19
+ export type ContextStyle = "text" | "gauge" | "text+gauge";
20
+
21
+ export type ContextThresholds = {
22
+ warning: number;
23
+ error: number;
24
+ };
8
25
 
9
26
  export type ColorSourcesConfig = {
10
27
  starship: ColorSource;
@@ -28,6 +45,9 @@ export type FooterSegmentsConfig = {
28
45
  tokens: boolean;
29
46
  cost: boolean;
30
47
  sessionDuration: boolean;
48
+ username: boolean;
49
+ time: boolean;
50
+ os: boolean;
31
51
  };
32
52
 
33
53
  export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
@@ -46,24 +66,10 @@ const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
46
66
 
47
67
  export type PolishedTuiConfig = {
48
68
  projectRefreshIntervalMs: number;
49
- icons: {
50
- cwd: string;
51
- git: string;
52
- ahead: string;
53
- behind: string;
54
- diverged: string;
55
- conflicted: string;
56
- untracked: string;
57
- stashed: string;
58
- modified: string;
59
- staged: string;
60
- renamed: string;
61
- deleted: string;
62
- typechanged: string;
63
- cacheHit: string;
64
- editorPrompt: string;
65
- rail: string;
66
- };
69
+ footerFormat: string;
70
+ contextStyle: ContextStyle;
71
+ contextThresholds: ContextThresholds;
72
+ icons: ResolvedIcons;
67
73
  colors: {
68
74
  cwd: ColorSpec;
69
75
  gitBranch: ColorSpec;
@@ -77,6 +83,9 @@ export type PolishedTuiConfig = {
77
83
  runtimePrefix: ColorSpec;
78
84
  extensionStatus: ColorSpec;
79
85
  sessionDuration: ColorSpec;
86
+ username: ColorSpec;
87
+ time: ColorSpec;
88
+ os: ColorSpec;
80
89
  editorAccent?: ColorSpec;
81
90
  editorPrompt?: ColorSpec;
82
91
  editorBorder?: ColorSpec;
@@ -95,27 +104,49 @@ export type PolishedTuiConfig = {
95
104
  extensionStatuses: ExtensionStatusesConfig;
96
105
  };
97
106
 
107
+ /**
108
+ * Canonical footer format variable names. In a `footerFormat` string these
109
+ * are written as `$name` or `${name}`.
110
+ */
111
+ export const FOOTER_FORMAT_VARIABLES = [
112
+ "cwd",
113
+ "git_branch",
114
+ "git_status",
115
+ "git_state",
116
+ "runtime",
117
+ "session_duration",
118
+ "username",
119
+ "os",
120
+ "time",
121
+ "context",
122
+ "tokens",
123
+ "cost",
124
+ "sep",
125
+ ] as const;
126
+
127
+ /**
128
+ * Alias → canonical variable name mapping for `footerFormat`.
129
+ * `$fill` is special (not a variable) and handled by the parser.
130
+ */
131
+ export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
132
+ directory: "cwd",
133
+ branch: "git_branch",
134
+ status: "git_status",
135
+ state: "git_state",
136
+ duration: "session_duration",
137
+ separator: "sep",
138
+ };
139
+
98
140
  export const configPath = join(getAgentDir(), "zentui.json");
99
141
 
100
142
  export const defaultConfig: PolishedTuiConfig = {
101
143
  projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
144
+ footerFormat: "",
145
+ contextStyle: "text",
146
+ contextThresholds: { warning: 70, error: 90 },
102
147
  icons: {
103
- cwd: "󰝰",
104
- git: "",
105
- ahead: "↑",
106
- behind: "↓",
107
- diverged: "⇕",
108
- conflicted: "=",
109
- untracked: "?",
110
- stashed: "$",
111
- modified: "!",
112
- staged: "+",
113
- renamed: "»",
114
- deleted: "✘",
115
- typechanged: "T",
116
- cacheHit: "󰆼",
117
- editorPrompt: "",
118
- rail: "│",
148
+ mode: "auto",
149
+ ...NERD_DEFAULT_ICONS,
119
150
  },
120
151
  colors: {
121
152
  cwd: "bold cyan",
@@ -130,6 +161,9 @@ export const defaultConfig: PolishedTuiConfig = {
130
161
  runtimePrefix: "",
131
162
  extensionStatus: "bright-black",
132
163
  sessionDuration: "yellow",
164
+ username: "bold yellow",
165
+ time: "bold yellow",
166
+ os: "bold white",
133
167
  },
134
168
  colorSources: {
135
169
  starship: "theme",
@@ -151,6 +185,9 @@ export const defaultConfig: PolishedTuiConfig = {
151
185
  tokens: true,
152
186
  cost: true,
153
187
  sessionDuration: false,
188
+ username: false,
189
+ time: false,
190
+ os: false,
154
191
  },
155
192
  extensionStatuses: {
156
193
  defaultPlacement: "right",
@@ -159,24 +196,6 @@ export const defaultConfig: PolishedTuiConfig = {
159
196
  },
160
197
  };
161
198
 
162
- const iconKeys = [
163
- "cwd",
164
- "git",
165
- "ahead",
166
- "behind",
167
- "diverged",
168
- "conflicted",
169
- "untracked",
170
- "stashed",
171
- "modified",
172
- "staged",
173
- "renamed",
174
- "deleted",
175
- "typechanged",
176
- "cacheHit",
177
- "editorPrompt",
178
- ] as const satisfies readonly (keyof PolishedTuiConfig["icons"])[];
179
-
180
199
  type ConfigRecord = Record<string, unknown>;
181
200
 
182
201
  function isRecord(value: unknown): value is ConfigRecord {
@@ -186,17 +205,43 @@ function isRecord(value: unknown): value is ConfigRecord {
186
205
  function parseProjectRefreshIntervalMs(value: unknown): number {
187
206
  if (value === 0) return 0;
188
207
  if (typeof value !== "number" || !Number.isFinite(value)) {
189
- return defaultConfig.projectRefreshIntervalMs;
208
+ return DEFAULT_PROJECT_REFRESH_INTERVAL_MS;
190
209
  }
191
210
 
192
211
  const interval = Math.round(value);
193
- return interval >= MIN_PROJECT_REFRESH_INTERVAL_MS
194
- ? interval
195
- : defaultConfig.projectRefreshIntervalMs;
212
+ if (interval <= 0) return 0;
213
+ return Math.max(MIN_PROJECT_REFRESH_INTERVAL_MS, interval);
214
+ }
215
+
216
+ function clampPercent(value: number): number {
217
+ return Math.max(0, Math.min(100, value));
218
+ }
219
+
220
+ function parseContextStyle(value: unknown): ContextStyle {
221
+ if (value === "text" || value === "gauge" || value === "text+gauge") return value;
222
+ return defaultConfig.contextStyle;
196
223
  }
197
224
 
198
- function railValue(value: unknown): string {
199
- return typeof value === "string" && value.trim().length > 0 ? value : defaultConfig.icons.rail;
225
+ function parseContextThresholds(value: unknown): ContextThresholds {
226
+ const defaults = defaultConfig.contextThresholds;
227
+ if (!isRecord(value)) return { ...defaults };
228
+
229
+ const warningRaw = value.warning;
230
+ const errorRaw = value.error;
231
+ let warning =
232
+ typeof warningRaw === "number" && Number.isFinite(warningRaw)
233
+ ? clampPercent(Math.round(warningRaw))
234
+ : defaults.warning;
235
+ let error =
236
+ typeof errorRaw === "number" && Number.isFinite(errorRaw)
237
+ ? clampPercent(Math.round(errorRaw))
238
+ : defaults.error;
239
+ if (error < warning) {
240
+ const swapped = warning;
241
+ warning = error;
242
+ error = swapped;
243
+ }
244
+ return { warning, error };
200
245
  }
201
246
 
202
247
  function stringValue(record: Record<string, unknown>, key: string): string | undefined {
@@ -240,13 +285,13 @@ function definedColors(
240
285
  ) as Partial<PolishedTuiConfig["colors"]>;
241
286
  }
242
287
 
243
- function normalizeIcons(record: Record<string, unknown>): Partial<PolishedTuiConfig["icons"]> {
288
+ function normalizeIconOverrides(record: Record<string, unknown>): Partial<IconGlyphs> {
244
289
  return Object.fromEntries(
245
- iconKeys.flatMap((key) => {
290
+ ICON_GLYPH_KEYS.flatMap((key) => {
246
291
  const value = stringValue(record, key);
247
292
  return value === undefined ? [] : [[key, value]];
248
293
  }),
249
- ) as Partial<PolishedTuiConfig["icons"]>;
294
+ ) as Partial<IconGlyphs>;
250
295
  }
251
296
 
252
297
  function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
@@ -263,6 +308,9 @@ function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiCo
263
308
  runtimePrefix: colorValue(record, "runtimePrefix"),
264
309
  extensionStatus: colorValue(record, "extensionStatus"),
265
310
  sessionDuration: colorValue(record, "sessionDuration"),
311
+ username: colorValue(record, "username"),
312
+ time: colorValue(record, "time"),
313
+ os: colorValue(record, "os"),
266
314
  editorAccent: colorValue(record, "editorAccent"),
267
315
  editorPrompt: colorValue(record, "editorPrompt"),
268
316
  editorBorder: colorValue(record, "editorBorder"),
@@ -304,6 +352,9 @@ function normalizeFooterSegments(record: Record<string, unknown>): FooterSegment
304
352
  tokens: footerSegmentValue(record, "tokens"),
305
353
  cost: footerSegmentValue(record, "cost"),
306
354
  sessionDuration: footerSegmentValue(record, "sessionDuration"),
355
+ username: footerSegmentValue(record, "username"),
356
+ time: footerSegmentValue(record, "time"),
357
+ os: footerSegmentValue(record, "os"),
307
358
  };
308
359
  }
309
360
 
@@ -361,7 +412,10 @@ function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig
361
412
  value === "context" ||
362
413
  value === "tokens" ||
363
414
  value === "cost" ||
364
- value === "sessionDuration"
415
+ value === "sessionDuration" ||
416
+ value === "username" ||
417
+ value === "time" ||
418
+ value === "os"
365
419
  );
366
420
  }
367
421
 
@@ -412,7 +466,8 @@ export function ensureConfigExists(): void {
412
466
  export function mergeConfig(parsed: unknown): PolishedTuiConfig {
413
467
  const config = isRecord(parsed) ? parsed : {};
414
468
  const iconsRecord = isRecord(config.icons) ? (config.icons as Record<string, unknown>) : {};
415
- const icons = normalizeIcons(iconsRecord);
469
+ const iconMode = normalizeIconMode(iconsRecord.mode);
470
+ const iconOverrides = normalizeIconOverrides(iconsRecord);
416
471
  const colors = isRecord(config.colors)
417
472
  ? normalizeColors(config.colors as Record<string, unknown>)
418
473
  : {};
@@ -430,11 +485,10 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
430
485
  : defaultConfig.extensionStatuses;
431
486
  return {
432
487
  projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
433
- icons: {
434
- ...defaultConfig.icons,
435
- ...icons,
436
- rail: railValue(iconsRecord.rail),
437
- },
488
+ footerFormat: stringValue(config, "footerFormat") ?? "",
489
+ contextStyle: parseContextStyle(config.contextStyle),
490
+ contextThresholds: parseContextThresholds(config.contextThresholds),
491
+ icons: resolveConfiguredIcons(iconMode, iconOverrides),
438
492
  colors: {
439
493
  ...defaultConfig.colors,
440
494
  ...colors,
@@ -521,6 +575,47 @@ export function saveFooterSegmentsPatch(
521
575
  return mergeConfig(record);
522
576
  }
523
577
 
578
+ export function saveFooterFormatPatch(value: string, path = configPath): PolishedTuiConfig {
579
+ const record = readConfigRecord(path);
580
+ record.footerFormat = typeof value === "string" ? value : "";
581
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
582
+ return mergeConfig(record);
583
+ }
584
+
585
+ export function saveIconsModePatch(mode: IconMode, path = configPath): PolishedTuiConfig {
586
+ const record = readConfigRecord(path);
587
+ const existing = isRecord(record.icons) ? { ...(record.icons as Record<string, unknown>) } : {};
588
+ record.icons = {
589
+ ...existing,
590
+ mode: normalizeIconMode(mode),
591
+ };
592
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
593
+ return mergeConfig(record);
594
+ }
595
+
596
+ export function saveContextStylePatch(style: ContextStyle, path = configPath): PolishedTuiConfig {
597
+ const record = readConfigRecord(path);
598
+ record.contextStyle = parseContextStyle(style);
599
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
600
+ return mergeConfig(record);
601
+ }
602
+
603
+ export function saveContextThresholdsPatch(
604
+ thresholds: Partial<ContextThresholds>,
605
+ path = configPath,
606
+ ): PolishedTuiConfig {
607
+ const record = readConfigRecord(path);
608
+ const existing = isRecord(record.contextThresholds)
609
+ ? { ...(record.contextThresholds as Record<string, unknown>) }
610
+ : {};
611
+ record.contextThresholds = {
612
+ ...existing,
613
+ ...thresholds,
614
+ };
615
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
616
+ return mergeConfig(record);
617
+ }
618
+
524
619
  export function saveExtensionStatusPlacement(
525
620
  key: string,
526
621
  placement: ExtensionStatusPlacement,