Skip to content

Commit c211fb5

Browse files
committed
feat(pi-footer): use Pi theme for plain footer
1 parent 91860fb commit c211fb5

5 files changed

Lines changed: 90 additions & 115 deletions

File tree

‎packages/pi-footer/src/constants.ts‎

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,7 @@ export const FOOTER_LAYOUT = {
77
hidden: [],
88
} as const;
99

10-
export const BLOCK_COLORS = {
11-
path: { bg: "#222222", fg: "#cccccc" },
12-
branch: { bg: "#95ffa4", fg: "#222222" },
13-
provider: { bg: "#24292f", fg: "#ffffff" },
14-
model: { bg: "#007acc", fg: "#ffffff" },
15-
thinking: { bg: "#8b5cf6", fg: "#ffffff" },
16-
mcp: { bg: "#7c3aed", fg: "#ffffff" },
17-
context: { bg: "#06b6d4", fg: "#062b33" },
18-
} as const;
19-
20-
export const PLAIN_COLORS = {
21-
bg: "#000000",
22-
fg: "#9ca3af",
23-
} as const;
24-
25-
export const PLAIN_SEPARATOR_COLORS = {
26-
bg: "#000000",
27-
fg: "#6b7280",
28-
} as const;
29-
3010
export const CUSTOM_SLOT_COLORS = {
31-
bg: "#374151",
32-
fg: "#e5e7eb",
11+
bg: "",
12+
fg: "",
3313
} as const;

‎packages/pi-footer/src/footer-rendering.ts‎

Lines changed: 54 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
22

3-
import {
4-
ACTIVE_FOOTER_VARIANT,
5-
BLOCK_COLORS,
6-
BRANCH_ICON,
7-
PLAIN_COLORS,
8-
PLAIN_SEPARATOR_COLORS,
9-
} from "./constants.ts";
3+
import { ACTIVE_FOOTER_VARIANT, BRANCH_ICON } from "./constants.ts";
104
import { getFooterSlotSnapshots, subscribeFooterSlotUpdates } from "./footer-slot-api.ts";
115
import { DEFAULT_FOOTER_CONFIG, type FooterConfig } from "./settings.ts";
126
import type {
@@ -19,10 +13,12 @@ import type {
1913
FooterSlotSnapshot,
2014
FooterSide,
2115
FooterVariant,
22-
Rgb,
23-
SegmentColors,
2416
} from "./types.ts";
2517

18+
export type PlainFooterTheme = {
19+
fg(role: "muted" | "dim", text: string): string;
20+
};
21+
2622
function sanitizeStatusText(text: string): string {
2723
return text
2824
.replace(/[\r\n\t]/g, " ")
@@ -46,45 +42,21 @@ function collapseHome(path: string): string {
4642
return path;
4743
}
4844

49-
function hexToRgb(hex: string): Rgb {
50-
const value = hex.replace(/^#/, "");
51-
if (!/^[0-9a-fA-F]{6}$/.test(value)) {
52-
throw new Error(`Invalid hex color: ${hex}`);
53-
}
54-
return [
55-
parseInt(value.slice(0, 2), 16),
56-
parseInt(value.slice(2, 4), 16),
57-
parseInt(value.slice(4, 6), 16),
58-
];
59-
}
60-
61-
function ansiColor(text: string, options: { fg?: string; bg?: string; bold?: boolean }): string {
62-
const codes: string[] = [];
63-
64-
if (options.bold === true) codes.push("1");
65-
if (options.fg !== undefined && options.fg.length > 0) {
66-
const [r, g, b] = hexToRgb(options.fg);
67-
codes.push(`38;2;${r};${g};${b}`);
68-
}
69-
if (options.bg !== undefined && options.bg.length > 0) {
70-
const [r, g, b] = hexToRgb(options.bg);
71-
codes.push(`48;2;${r};${g};${b}`);
72-
}
73-
74-
if (codes.length === 0) return text;
75-
return `\x1b[${codes.join(";")}m${text}\x1b[0m`;
76-
}
77-
78-
function renderColoredText(text: string, colors: SegmentColors): string {
79-
return ansiColor(text, { fg: colors.fg, bg: colors.bg });
45+
function renderBlockItem(item: FooterItem): string {
46+
return ` ${item.text} `;
8047
}
8148

82-
function renderBlockItem(item: FooterItem): string {
83-
return renderColoredText(` ${item.text} `, item.colors);
49+
function renderThemeText(
50+
text: string,
51+
role: "muted" | "dim",
52+
theme: PlainFooterTheme | undefined,
53+
): string {
54+
if (theme === undefined) return text;
55+
return theme.fg(role, text);
8456
}
8557

86-
function renderPlainItem(item: FooterItem): string {
87-
return renderColoredText(item.text, PLAIN_COLORS);
58+
function renderPlainItem(item: FooterItem, theme: PlainFooterTheme | undefined): string {
59+
return renderThemeText(item.text, "muted", theme);
8860
}
8961

9062
function getFallbackProviderDisplayName(provider: string): string {
@@ -114,32 +86,6 @@ function getProviderDisplayName(ctx: FooterContext, provider: string): string {
11486
return getFallbackProviderDisplayName(provider);
11587
}
11688

117-
function getThinkingColors(level: string): SegmentColors {
118-
switch (level) {
119-
case "minimal":
120-
return { bg: "#4338ca", fg: "#ffffff" };
121-
case "low":
122-
return { bg: "#0369a1", fg: "#ffffff" };
123-
case "medium":
124-
return { bg: "#0891b2", fg: "#062b33" };
125-
case "high":
126-
return { bg: "#8b5cf6", fg: "#ffffff" };
127-
case "xhigh":
128-
return { bg: "#dc2626", fg: "#ffffff" };
129-
case "off":
130-
default:
131-
return { bg: "#374151", fg: "#e5e7eb" };
132-
}
133-
}
134-
135-
function getContextColors(percent: number | null | undefined): SegmentColors {
136-
if (percent !== null && percent !== undefined) {
137-
if (percent > 90) return { bg: "#dc2626", fg: "#ffffff" };
138-
if (percent > 70) return { bg: "#f59e0b", fg: "#1f1300" };
139-
}
140-
return { bg: "#06b6d4", fg: "#062b33" };
141-
}
142-
14389
function getContextText(usage: ContextUsage, fallbackWindow?: number): string {
14490
const contextWindow = usage?.contextWindow ?? fallbackWindow ?? 0;
14591
const contextPercent = usage?.percent;
@@ -165,28 +111,38 @@ function getMcpText(ctx: FooterContext, footerData: FooterData): string | null {
165111
return null;
166112
}
167113

168-
function getSeparator(variant: FooterVariant, side: FooterSide, config: FooterConfig): string {
114+
function getSeparator(
115+
variant: FooterVariant,
116+
side: FooterSide,
117+
config: FooterConfig,
118+
theme: PlainFooterTheme | undefined,
119+
): string {
169120
if (variant === "blocks") return "";
170121
if (side === "left") {
171-
return renderColoredText(` ${config.separator} `, PLAIN_SEPARATOR_COLORS);
122+
return renderThemeText(` ${config.separator} `, "dim", theme);
172123
}
173-
return renderColoredText(" ", PLAIN_SEPARATOR_COLORS);
124+
return renderThemeText(" ", "dim", theme);
174125
}
175126

176-
function renderItem(item: FooterItem, variant: FooterVariant): string {
127+
function renderItem(
128+
item: FooterItem,
129+
variant: FooterVariant,
130+
theme: PlainFooterTheme | undefined,
131+
): string {
177132
if (variant === "blocks") {
178133
return renderBlockItem(item);
179134
}
180-
return renderPlainItem(item);
135+
return renderPlainItem(item, theme);
181136
}
182137

183138
function joinRenderedItems(
184139
rendered: string[],
185140
variant: FooterVariant,
186141
side: FooterSide,
187142
config: FooterConfig,
143+
theme: PlainFooterTheme | undefined,
188144
): string {
189-
return rendered.join(getSeparator(variant, side, config));
145+
return rendered.join(getSeparator(variant, side, config, theme));
190146
}
191147

192148
function buildSideVariants(
@@ -195,6 +151,7 @@ function buildSideVariants(
195151
variant: FooterVariant,
196152
side: FooterSide,
197153
config: FooterConfig,
154+
theme: PlainFooterTheme | undefined,
198155
): string[] {
199156
const items = keys
200157
.map((key) => itemsByKey.get(key))
@@ -209,10 +166,11 @@ function buildSideVariants(
209166
if (side === "left") {
210167
for (let count = items.length; count >= 1; count--) {
211168
const rendered = joinRenderedItems(
212-
items.slice(0, count).map((item) => renderItem(item, variant)),
169+
items.slice(0, count).map((item) => renderItem(item, variant, theme)),
213170
variant,
214171
side,
215172
config,
173+
theme,
216174
);
217175
if (!seen.has(rendered)) {
218176
seen.add(rendered);
@@ -222,10 +180,11 @@ function buildSideVariants(
222180
} else {
223181
for (let start = 0; start < items.length; start++) {
224182
const rendered = joinRenderedItems(
225-
items.slice(start).map((item) => renderItem(item, variant)),
183+
items.slice(start).map((item) => renderItem(item, variant, theme)),
226184
variant,
227185
side,
228186
config,
187+
theme,
229188
);
230189
if (!seen.has(rendered)) {
231190
seen.add(rendered);
@@ -238,11 +197,15 @@ function buildSideVariants(
238197
return variants;
239198
}
240199

241-
function renderPadding(width: number, variant: FooterVariant): string {
200+
function renderPadding(
201+
width: number,
202+
variant: FooterVariant,
203+
theme: PlainFooterTheme | undefined,
204+
): string {
242205
if (width <= 0) return "";
243206
const padding = " ".repeat(width);
244207
if (variant === "plain") {
245-
return renderColoredText(padding, PLAIN_COLORS);
208+
return renderThemeText(padding, "muted", theme);
246209
}
247210
return padding;
248211
}
@@ -266,42 +229,42 @@ function buildFooterItems(
266229
items.set("path", {
267230
key: "path",
268231
text: pathText,
269-
colors: BLOCK_COLORS.path,
232+
colors: { bg: "", fg: "" },
270233
});
271234
items.set("provider", {
272235
key: "provider",
273236
text: providerLabel,
274-
colors: BLOCK_COLORS.provider,
237+
colors: { bg: "", fg: "" },
275238
});
276239
items.set("model", {
277240
key: "model",
278241
text: modelLabel,
279-
colors: BLOCK_COLORS.model,
242+
colors: { bg: "", fg: "" },
280243
});
281244
items.set("thinking", {
282245
key: "thinking",
283246
text: thinkingLevel,
284-
colors: getThinkingColors(thinkingLevel),
247+
colors: { bg: "", fg: "" },
285248
});
286249
items.set("context", {
287250
key: "context",
288251
text: contextText,
289-
colors: getContextColors(usage?.percent),
252+
colors: { bg: "", fg: "" },
290253
});
291254

292255
if (branch !== null && branch.length > 0) {
293256
items.set("branch", {
294257
key: "branch",
295258
text: `${BRANCH_ICON} ${branch}`,
296-
colors: BLOCK_COLORS.branch,
259+
colors: { bg: "", fg: "" },
297260
});
298261
}
299262

300263
if (mcpText !== null && mcpText.length > 0) {
301264
items.set("mcp", {
302265
key: "mcp",
303266
text: mcpText,
304-
colors: BLOCK_COLORS.mcp,
267+
colors: { bg: "", fg: "" },
305268
});
306269
}
307270

@@ -357,6 +320,7 @@ export function createFooterComponent(
357320
getThinkingLevel: () => string,
358321
requestRender: () => void,
359322
config: FooterConfig = DEFAULT_FOOTER_CONFIG,
323+
theme?: PlainFooterTheme,
360324
) {
361325
const unsubscribeBranchChange = footerData.onBranchChange(() => requestRender());
362326
const unsubscribeSlotUpdates = subscribeFooterSlotUpdates(() => requestRender());
@@ -384,13 +348,15 @@ export function createFooterComponent(
384348
variant,
385349
"left",
386350
config,
351+
theme,
387352
);
388353
const rightVariants = buildSideVariants(
389354
itemsByKey,
390355
layout.right,
391356
variant,
392357
"right",
393358
config,
359+
theme,
394360
);
395361

396362
for (const left of leftVariants) {
@@ -413,7 +379,7 @@ export function createFooterComponent(
413379
minimumInnerGap,
414380
renderWidth - edgePaddingWidth - leftWidth - rightWidth,
415381
);
416-
const padding = renderPadding(paddingWidth, variant);
382+
const padding = renderPadding(paddingWidth, variant, theme);
417383
if (right.length > 0) {
418384
return [truncateToWidth(` ${left}${padding}${right} `, renderWidth, "")];
419385
}

‎packages/pi-footer/src/footer-slot-api.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type FooterSlotRegistration = {
1919
readonly defaultSide?: FooterSide;
2020
/** Initial visible text. Empty or whitespace-only text hides the slot. */
2121
readonly text?: string;
22-
/** Colors used by block footer rendering variants. */
22+
/** Optional colors preserved for compatibility with block footer variants. */
2323
readonly colors?: SegmentColors;
2424
};
2525

‎packages/pi-footer/src/footer-transition.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from "@earendil-works/pi-coding-agent";
66

77
import { markFooterComponent, getFooterComponentKind } from "./footer-component.ts";
8-
import { createFooterComponent } from "./footer-rendering.ts";
8+
import { createFooterComponent, type PlainFooterTheme } from "./footer-rendering.ts";
99
import type { FooterConfig } from "./settings.ts";
1010
import type { ContextUsage, FooterContext, FooterData, FooterModel } from "./types.ts";
1111

@@ -19,7 +19,11 @@ type FooterTui = {
1919
requestRender(): void;
2020
setClearOnShrink?(enabled: boolean): void;
2121
};
22-
type FooterFactory = (tui: FooterTui, theme: unknown, footerData: FooterData) => FooterComponent;
22+
type FooterFactory = (
23+
tui: FooterTui,
24+
theme: PlainFooterTheme,
25+
footerData: FooterData,
26+
) => FooterComponent;
2327

2428
type FooterResetHost = {
2529
customFooter?: unknown;
@@ -154,13 +158,14 @@ function shouldBridgeFooter(kind: string | undefined, state: FooterTransitionSta
154158
function installBridgeFooter(host: FooterResetHost, snapshot: FooterSnapshot): void {
155159
const generationAtInstall = getTransitionState().liveInstallGeneration;
156160

157-
host.setExtensionFooter((tui, _theme, footerData) => {
161+
host.setExtensionFooter((tui, theme, footerData) => {
158162
const component = createFooterComponent(
159163
snapshot.context,
160164
footerData,
161165
() => snapshot.thinkingLevel,
162166
() => tui.requestRender(),
163167
snapshot.config,
168+
theme,
164169
);
165170
return markFooterComponent(component, "bridge");
166171
});
@@ -228,13 +233,14 @@ export function installLiveFooter(
228233
getThinkingLevel: () => string,
229234
config: FooterConfig,
230235
): void {
231-
ctx.ui.setFooter((tui, _theme, footerData) => {
236+
ctx.ui.setFooter((tui, theme, footerData) => {
232237
const component = createFooterComponent(
233238
ctx,
234239
footerData,
235240
getThinkingLevel,
236241
() => tui.requestRender(),
237242
config,
243+
theme,
238244
);
239245
return markFooterComponent(component, "live");
240246
});

0 commit comments

Comments
 (0)