pi-zentui 0.1.7 → 0.1.9

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
@@ -20,12 +20,12 @@ Zentui brings two popular aesthetics to Pi:
20
20
  - `󰝰 dirname` — current directory with icon
21
21
  - `on branch` — git branch with icon
22
22
  - `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
23
- - `via v5.5.0` — runtime detection with version and Starship terminal styles for Nerd Font runtime/language modules
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
25
 
26
26
  ### Editor (Opencode-inspired)
27
27
 
28
- - Bordered input box with accent-colored left rail
28
+ - Bordered input box with theme accent rail and thinking-level border color
29
29
  - Model name and provider displayed inside the editor frame
30
30
  - Thinking level indicator when enabled
31
31
  - Prompt-box-style user messages matching the ZentUI input chrome
@@ -47,7 +47,7 @@ Zentui brings two popular aesthetics to Pi:
47
47
 
48
48
  ### Runtime Detection
49
49
 
50
- Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and styles each runtime with Starship's terminal style strings (for example, Node.js uses `bold green`, so your terminal colorscheme supplies the actual green):
50
+ Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and keeps Starship-style defaults such as `bold green` for Node.js. By default Zentui maps those styles through your active Pi theme; switch the Starship/footer color source to `terminal` in `/zentui` if you want your terminal colorscheme to supply the exact ANSI colors.
51
51
 
52
52
  | Runtime/language | Detection examples |
53
53
  | ---------------- | ------------------------------------------------------------ |
@@ -148,15 +148,21 @@ On first run, Zentui creates a config file at:
148
148
  "typechanged": "T"
149
149
  },
150
150
  "colors": {
151
- "cwdText": "syntaxOperator",
152
- "git": "syntaxKeyword",
153
- "gitStatus": "error",
154
- "contextNormal": "muted",
155
- "contextWarning": "warning",
156
- "contextError": "error",
157
- "tokens": "muted",
158
- "cost": "success",
159
- "separator": "borderMuted"
151
+ "cwd": "bold cyan",
152
+ "gitBranch": "bold purple",
153
+ "gitStatus": "bold red",
154
+ "contextNormal": "dimmed",
155
+ "contextWarning": "bold yellow",
156
+ "contextError": "bold red",
157
+ "tokens": "dimmed",
158
+ "cost": "bold green",
159
+ "separator": "dimmed",
160
+ "runtimePrefix": ""
161
+ },
162
+ "colorSources": {
163
+ "starship": "theme",
164
+ "editor": "theme",
165
+ "userMessages": "theme"
160
166
  }
161
167
  }
162
168
  ```
@@ -165,12 +171,21 @@ On first run, Zentui creates a config file at:
165
171
 
166
172
  ### Color values
167
173
 
168
- Colors can be:
174
+ Use `/zentui` inside Pi to switch color sources between Pi theme colors and terminal colors:
175
+
176
+ - `starship` — footer/runtime/git/context/cost colors
177
+ - `editor + previous messages` — input editor and previous user-message rails/borders
178
+
179
+ Both settings default to `theme`. The config still stores editor and previous user-message sources separately as `editor` and `userMessages`, but `/zentui` changes them together so the prompt chrome stays consistent.
180
+
181
+ Color values can use terminal-palette style strings, hex colors, or Pi theme color tokens:
169
182
 
170
- - Pi theme token names (e.g., `accent`, `error`, `syntaxKeyword`)
171
- - Hex colors (e.g., `#89b4fa`)
183
+ - Named terminal colors: `bold purple`, `yellow`, `bright-cyan`
184
+ - 256-color / hex: `bold 149`, `fg:202`, `#89b4fa`
185
+ - Backgrounds and modifiers: `bg:blue fg:bright-green`, `underline bg:#bf5700`
186
+ - Pi theme tokens: `accent`, `borderMuted`, `syntaxKeyword`
172
187
 
173
- This means Zentui works with any Pi theme — it uses your theme's colors by default.
188
+ Use the `colors` config key for these values.
174
189
 
175
190
  ## Requirements
176
191
 
@@ -3,6 +3,13 @@ import { join } from "node:path";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  export type ColorSpec = string;
6
+ export type ColorSource = "theme" | "terminal";
7
+
8
+ export type ColorSourcesConfig = {
9
+ starship: ColorSource;
10
+ editor: ColorSource;
11
+ userMessages: ColorSource;
12
+ };
6
13
 
7
14
  const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
8
15
  const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
@@ -25,8 +32,8 @@ export type PolishedTuiConfig = {
25
32
  typechanged: string;
26
33
  };
27
34
  colors: {
28
- cwdText: ColorSpec;
29
- git: ColorSpec;
35
+ cwd: ColorSpec;
36
+ gitBranch: ColorSpec;
30
37
  gitStatus: ColorSpec;
31
38
  contextNormal: ColorSpec;
32
39
  contextWarning: ColorSpec;
@@ -34,59 +41,13 @@ export type PolishedTuiConfig = {
34
41
  tokens: ColorSpec;
35
42
  cost: ColorSpec;
36
43
  separator: ColorSpec;
44
+ runtimePrefix: ColorSpec;
37
45
  };
46
+ colorSources: ColorSourcesConfig;
38
47
  };
39
48
 
40
49
  export const configPath = join(getAgentDir(), "zentui.json");
41
50
 
42
- const themeColorTokens = new Set([
43
- "accent",
44
- "border",
45
- "borderAccent",
46
- "borderMuted",
47
- "success",
48
- "error",
49
- "warning",
50
- "muted",
51
- "dim",
52
- "text",
53
- "thinkingText",
54
- "userMessageText",
55
- "customMessageText",
56
- "customMessageLabel",
57
- "toolTitle",
58
- "toolOutput",
59
- "mdHeading",
60
- "mdLink",
61
- "mdLinkUrl",
62
- "mdCode",
63
- "mdCodeBlock",
64
- "mdCodeBlockBorder",
65
- "mdQuote",
66
- "mdQuoteBorder",
67
- "mdHr",
68
- "mdListBullet",
69
- "toolDiffAdded",
70
- "toolDiffRemoved",
71
- "toolDiffContext",
72
- "syntaxComment",
73
- "syntaxKeyword",
74
- "syntaxFunction",
75
- "syntaxVariable",
76
- "syntaxString",
77
- "syntaxNumber",
78
- "syntaxType",
79
- "syntaxOperator",
80
- "syntaxPunctuation",
81
- "thinkingOff",
82
- "thinkingMinimal",
83
- "thinkingLow",
84
- "thinkingMedium",
85
- "thinkingHigh",
86
- "thinkingXhigh",
87
- "bashMode",
88
- ]);
89
-
90
51
  export const defaultConfig: PolishedTuiConfig = {
91
52
  projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
92
53
  icons: {
@@ -105,72 +66,22 @@ export const defaultConfig: PolishedTuiConfig = {
105
66
  typechanged: "T",
106
67
  },
107
68
  colors: {
108
- cwdText: "syntaxOperator",
109
- git: "syntaxKeyword",
110
- gitStatus: "error",
111
- contextNormal: "muted",
112
- contextWarning: "warning",
113
- contextError: "error",
114
- tokens: "muted",
115
- cost: "success",
116
- separator: "borderMuted",
69
+ cwd: "bold cyan",
70
+ gitBranch: "bold purple",
71
+ gitStatus: "bold red",
72
+ contextNormal: "dimmed",
73
+ contextWarning: "bold yellow",
74
+ contextError: "bold red",
75
+ tokens: "dimmed",
76
+ cost: "bold green",
77
+ separator: "dimmed",
78
+ runtimePrefix: "",
79
+ },
80
+ colorSources: {
81
+ starship: "theme",
82
+ editor: "theme",
83
+ userMessages: "theme",
117
84
  },
118
- };
119
-
120
- function isHexColor(value: string): boolean {
121
- return /^#(?:[0-9a-fA-F]{6})$/.test(value);
122
- }
123
-
124
- function hexToAnsi(hex: string, isBackground = false): string {
125
- const normalized = hex.slice(1);
126
- const r = Number.parseInt(normalized.slice(0, 2), 16);
127
- const g = Number.parseInt(normalized.slice(2, 4), 16);
128
- const b = Number.parseInt(normalized.slice(4, 6), 16);
129
- return `\x1b[${isBackground ? 48 : 38};2;${r};${g};${b}m`;
130
- }
131
-
132
- const terminalColorCodes = new Map([
133
- ["black", 30],
134
- ["red", 31],
135
- ["green", 32],
136
- ["yellow", 33],
137
- ["blue", 34],
138
- ["purple", 35],
139
- ["cyan", 36],
140
- ["white", 37],
141
- ["bright-black", 90],
142
- ["bright-red", 91],
143
- ["bright-green", 92],
144
- ["bright-yellow", 93],
145
- ["bright-blue", 94],
146
- ["bright-purple", 95],
147
- ["bright-cyan", 96],
148
- ["bright-white", 97],
149
- ]);
150
-
151
- const terminalStyleModifiers = new Map([
152
- ["bold", 1],
153
- ["dim", 2],
154
- ["dimmed", 2],
155
- ["italic", 3],
156
- ["underline", 4],
157
- ]);
158
-
159
- function terminalColorToAnsi(color: string): string | undefined {
160
- const normalized = color.toLowerCase();
161
- const colorCode = terminalColorCodes.get(normalized);
162
- if (colorCode !== undefined) return `${colorCode}`;
163
-
164
- if (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized)) {
165
- return `38;5;${normalized}`;
166
- }
167
-
168
- if (isHexColor(normalized)) return hexToAnsi(normalized).slice(2, -1);
169
- return undefined;
170
- }
171
-
172
- type ThemeLike = {
173
- fg(color: string, text: string): string;
174
85
  };
175
86
 
176
87
  type ConfigRecord = Record<string, unknown>;
@@ -191,34 +102,73 @@ function parseProjectRefreshIntervalMs(value: unknown): number {
191
102
  : defaultConfig.projectRefreshIntervalMs;
192
103
  }
193
104
 
194
- export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
195
- if (themeColorTokens.has(color)) {
196
- return theme.fg(color, text);
197
- }
198
- if (isHexColor(color)) {
199
- return `${hexToAnsi(color)}${text}\x1b[39m`;
200
- }
201
- return theme.fg("text", text);
105
+ function stringValue(record: Record<string, unknown>, key: string): string | undefined {
106
+ const value = record[key];
107
+ return typeof value === "string" ? value : undefined;
202
108
  }
203
109
 
204
- export function renderTerminalStyle(style: string, text: string): string {
205
- const codes: string[] = [];
206
- for (const token of style.trim().split(/\s+/)) {
207
- if (!token) continue;
110
+ function colorSourceValue(
111
+ record: Record<string, unknown>,
112
+ key: keyof ColorSourcesConfig,
113
+ ): ColorSource {
114
+ const value = record[key];
115
+ return value === "terminal" || value === "theme" ? value : defaultConfig.colorSources[key];
116
+ }
208
117
 
209
- const normalized = token.toLowerCase();
210
- const modifier = terminalStyleModifiers.get(normalized);
211
- if (modifier !== undefined) {
212
- codes.push(`${modifier}`);
213
- continue;
214
- }
118
+ function definedColors(
119
+ colors: Partial<Record<keyof PolishedTuiConfig["colors"], string | undefined>>,
120
+ ): Partial<PolishedTuiConfig["colors"]> {
121
+ return Object.fromEntries(
122
+ Object.entries(colors).filter(
123
+ (entry): entry is [keyof PolishedTuiConfig["colors"], string] => typeof entry[1] === "string",
124
+ ),
125
+ ) as Partial<PolishedTuiConfig["colors"]>;
126
+ }
215
127
 
216
- const foreground = normalized.startsWith("fg:") ? normalized.slice(3) : normalized;
217
- const color = terminalColorToAnsi(foreground);
218
- if (color) codes.push(color);
219
- }
128
+ function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
129
+ return definedColors({
130
+ cwd: stringValue(record, "cwd") ?? stringValue(record, "cwdText"),
131
+ gitBranch: stringValue(record, "gitBranch") ?? stringValue(record, "git"),
132
+ gitStatus: stringValue(record, "gitStatus"),
133
+ contextNormal: stringValue(record, "contextNormal"),
134
+ contextWarning: stringValue(record, "contextWarning"),
135
+ contextError: stringValue(record, "contextError"),
136
+ tokens: stringValue(record, "tokens"),
137
+ cost: stringValue(record, "cost"),
138
+ separator: stringValue(record, "separator"),
139
+ runtimePrefix: stringValue(record, "runtimePrefix"),
140
+ });
141
+ }
142
+
143
+ function normalizeColorSources(record: Record<string, unknown>): ColorSourcesConfig {
144
+ return {
145
+ starship: colorSourceValue(record, "starship"),
146
+ editor: colorSourceValue(record, "editor"),
147
+ userMessages: colorSourceValue(record, "userMessages"),
148
+ };
149
+ }
150
+
151
+ function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
152
+ return value === "starship" || value === "editor" || value === "userMessages";
153
+ }
220
154
 
221
- return codes.length ? `\x1b[${codes.join(";")}m${text}\x1b[0m` : text;
155
+ function validColorSourceEntries(record: Record<string, unknown>): Partial<ColorSourcesConfig> {
156
+ return Object.fromEntries(
157
+ Object.entries(record).filter((entry): entry is [keyof ColorSourcesConfig, ColorSource] => {
158
+ const [key, value] = entry;
159
+ return isColorSourceKey(key) && (value === "theme" || value === "terminal");
160
+ }),
161
+ ) as Partial<ColorSourcesConfig>;
162
+ }
163
+
164
+ function readConfigRecord(path = configPath): ConfigRecord {
165
+ try {
166
+ if (!existsSync(path)) return {};
167
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
168
+ return isRecord(parsed) ? parsed : {};
169
+ } catch {
170
+ return {};
171
+ }
222
172
  }
223
173
 
224
174
  export function ensureConfigExists(): void {
@@ -235,8 +185,11 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
235
185
  const config = isRecord(parsed) ? parsed : {};
236
186
  const icons = isRecord(config.icons) ? (config.icons as Partial<PolishedTuiConfig["icons"]>) : {};
237
187
  const colors = isRecord(config.colors)
238
- ? (config.colors as Partial<PolishedTuiConfig["colors"]>)
188
+ ? normalizeColors(config.colors as Record<string, unknown>)
239
189
  : {};
190
+ const colorSources = isRecord(config.colorSources)
191
+ ? normalizeColorSources(config.colorSources as Record<string, unknown>)
192
+ : defaultConfig.colorSources;
240
193
  return {
241
194
  projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
242
195
  icons: {
@@ -247,6 +200,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
247
200
  ...defaultConfig.colors,
248
201
  ...colors,
249
202
  },
203
+ colorSources,
250
204
  };
251
205
  }
252
206
 
@@ -258,3 +212,20 @@ export function loadConfig(): PolishedTuiConfig {
258
212
  return defaultConfig;
259
213
  }
260
214
  }
215
+
216
+ export function saveColorSourcesPatch(
217
+ patch: Partial<ColorSourcesConfig>,
218
+ path = configPath,
219
+ ): PolishedTuiConfig {
220
+ const record = readConfigRecord(path);
221
+ const existing = isRecord(record.colorSources)
222
+ ? validColorSourceEntries(record.colorSources as Record<string, unknown>)
223
+ : {};
224
+ record.colorSources = {
225
+ ...defaultConfig.colorSources,
226
+ ...existing,
227
+ ...validColorSourceEntries(patch),
228
+ };
229
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
230
+ return mergeConfig(record);
231
+ }
@@ -0,0 +1,107 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { PolishedTuiConfig } from "./config";
4
+ import { formatCwdLabel, formatRuntimeSegment } from "./format";
5
+ import type { FooterState } from "./state";
6
+ import { renderStyleForSource } from "./style";
7
+
8
+ export function installFooter(
9
+ ctx: ExtensionContext,
10
+ state: FooterState,
11
+ getConfig: () => PolishedTuiConfig,
12
+ hooks: {
13
+ setRequestRender: (fn: (() => void) | undefined) => void;
14
+ scheduleProjectRefresh: (ctx: ExtensionContext) => void;
15
+ },
16
+ ): void {
17
+ ctx.ui.setFooter((tui, theme, footerData) => {
18
+ hooks.setRequestRender(() => tui.requestRender());
19
+ const unsubscribeBranch = footerData.onBranchChange(() => {
20
+ hooks.scheduleProjectRefresh(ctx);
21
+ tui.requestRender();
22
+ });
23
+
24
+ return {
25
+ dispose: () => {
26
+ unsubscribeBranch();
27
+ hooks.setRequestRender(undefined);
28
+ },
29
+ invalidate() {},
30
+ render(width: number): string[] {
31
+ if (width <= 0) return [""];
32
+ const config = getConfig();
33
+ const colorSource = config.colorSources.starship;
34
+ const separator = renderStyleForSource(theme, colorSource, config.colors.separator, " | ");
35
+ const innerWidth = Math.max(1, width - 2);
36
+ const cwdLabel = renderStyleForSource(
37
+ theme,
38
+ colorSource,
39
+ config.colors.cwd,
40
+ formatCwdLabel(ctx.cwd, config.icons.cwd),
41
+ );
42
+ const branch = state.branch;
43
+ const contextUsage = ctx.getContextUsage();
44
+ const contextColor =
45
+ contextUsage?.percent !== null && contextUsage?.percent !== undefined
46
+ ? contextUsage.percent >= 90
47
+ ? config.colors.contextError
48
+ : contextUsage.percent >= 70
49
+ ? config.colors.contextWarning
50
+ : config.colors.contextNormal
51
+ : config.colors.contextNormal;
52
+ const gitColor = (text: string) =>
53
+ renderStyleForSource(theme, colorSource, config.colors.gitBranch, text);
54
+ const gitStatusColor = (text: string) =>
55
+ renderStyleForSource(theme, colorSource, config.colors.gitStatus, text);
56
+ const gitIcon = gitColor(config.icons.git);
57
+ const allStatus = [
58
+ state.conflicted > 0 ? config.icons.conflicted : "",
59
+ state.stashed ? config.icons.stashed : "",
60
+ state.deleted > 0 ? config.icons.deleted : "",
61
+ state.renamed > 0 ? config.icons.renamed : "",
62
+ state.modified > 0 ? config.icons.modified : "",
63
+ state.typechanged > 0 ? config.icons.typechanged : "",
64
+ state.staged > 0 ? config.icons.staged : "",
65
+ state.untracked > 0 ? config.icons.untracked : "",
66
+ ].join("");
67
+ const aheadBehind =
68
+ state.ahead > 0 && state.behind > 0
69
+ ? config.icons.diverged
70
+ : state.ahead > 0
71
+ ? config.icons.ahead
72
+ : state.behind > 0
73
+ ? config.icons.behind
74
+ : "";
75
+ const statusBlock =
76
+ allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
77
+ const branchLabel = branch
78
+ ? `on ${gitIcon} ${gitColor(branch)}${statusBlock ? ` ${statusBlock}` : ""}`
79
+ : "";
80
+ const runtimeLabel = formatRuntimeSegment(
81
+ theme,
82
+ state.runtime,
83
+ config.colors.runtimePrefix,
84
+ colorSource,
85
+ );
86
+
87
+ const left = [cwdLabel, branchLabel, runtimeLabel].filter(Boolean).join(" ");
88
+ const right = [
89
+ renderStyleForSource(theme, colorSource, contextColor, state.contextLabel),
90
+ renderStyleForSource(theme, colorSource, config.colors.tokens, state.tokenLabel),
91
+ renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel),
92
+ ].join(separator);
93
+
94
+ const leftWidth = visibleWidth(left);
95
+ const rightWidth = visibleWidth(right);
96
+ const content =
97
+ leftWidth >= innerWidth
98
+ ? truncateToWidth(left, innerWidth, "")
99
+ : leftWidth + 1 + rightWidth <= innerWidth
100
+ ? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
101
+ : truncateToWidth(left, innerWidth, "");
102
+ const framed = width > 2 ? ` ${truncateToWidth(content, width - 2, "")} ` : content;
103
+ return [truncateToWidth(framed, width, "")];
104
+ },
105
+ };
106
+ });
107
+ }
@@ -0,0 +1,87 @@
1
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
+ import type { ColorSource, ColorSpec } from "./config";
4
+ import type { RuntimeInfo } from "./runtime";
5
+ import { renderStyleForSource } from "./style";
6
+
7
+ export type UsageTotals = {
8
+ input: number;
9
+ output: number;
10
+ cost: number;
11
+ };
12
+
13
+ export function formatCount(value: number): string {
14
+ if (value < 1000) return `${value}`;
15
+ if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
16
+ return `${Math.round(value / 1000)}k`;
17
+ }
18
+
19
+ export function formatProviderLabel(provider: string | undefined): string {
20
+ if (!provider) return "Unknown";
21
+
22
+ const known: Record<string, string> = {
23
+ anthropic: "Anthropic",
24
+ gemini: "Google",
25
+ google: "Google",
26
+ ollama: "Ollama",
27
+ openai: "OpenAI",
28
+ "openai-codex": "OpenAI",
29
+ };
30
+
31
+ return (
32
+ known[provider] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
33
+ );
34
+ }
35
+
36
+ export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
37
+ let input = 0;
38
+ let output = 0;
39
+ let cost = 0;
40
+
41
+ for (const entry of ctx.sessionManager.getBranch()) {
42
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
43
+ const message = entry.message as AssistantMessage;
44
+ input += message.usage?.input ?? 0;
45
+ output += message.usage?.output ?? 0;
46
+ cost += message.usage?.cost?.total ?? 0;
47
+ }
48
+
49
+ return { input, output, cost };
50
+ }
51
+
52
+ export function buildTokenLabel(totals: UsageTotals): string {
53
+ return `↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`;
54
+ }
55
+
56
+ export function buildCostLabel(totals: UsageTotals): string {
57
+ return `$${totals.cost.toFixed(3)}`;
58
+ }
59
+
60
+ export function buildContextLabel(ctx: ExtensionContext): string {
61
+ const usage = ctx.getContextUsage();
62
+ const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
63
+
64
+ if (!usage || !contextWindow || contextWindow <= 0) return "--";
65
+
66
+ const percent =
67
+ usage.percent === null ? "?" : `${Math.max(0, Math.min(999, Math.round(usage.percent)))}%`;
68
+ return `${percent}/${formatCount(contextWindow)}`;
69
+ }
70
+
71
+ export function formatRuntimeSegment(
72
+ theme: Pick<Theme, "fg">,
73
+ runtime: RuntimeInfo | undefined,
74
+ prefixStyle: ColorSpec,
75
+ colorSource: ColorSource,
76
+ ): string {
77
+ if (!runtime) return "";
78
+ const label = runtime.version ? `${runtime.symbol} ${runtime.version}` : runtime.symbol;
79
+ return `${renderStyleForSource(theme, colorSource, prefixStyle, "via")} ${renderStyleForSource(theme, colorSource, runtime.style, label)}`;
80
+ }
81
+
82
+ export function formatCwdLabel(cwd: string, cwdIcon: string): string {
83
+ const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
84
+ const parts = normalized.split("/").filter(Boolean);
85
+ const last = parts[parts.length - 1] ?? cwd;
86
+ return cwdIcon ? `${cwdIcon} ${last}` : last;
87
+ }