Skip to content

Commit 0cefb77

Browse files
authored
Merge pull request #57 from narumiruna/feat/statusline-theme-env
feat(statusline): add theme presets
2 parents b0eed17 + 67a710e commit 0cefb77

9 files changed

Lines changed: 350 additions & 129 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
## Goal
2+
3+
`pi-statusline` 可用環境變數切換 statusline preset,先支援 `classic``tokyo-night`,並保留目前資訊內容、emoji、第二行 extension statuses 與安全截斷行為。
4+
5+
## Context
6+
7+
目前 `extensions/pi-statusline/src/statusline.ts` 已改為 Tokyo Night 配色的 Starship-inspired powerline renderer。靈感來源是 Starship Tokyo Night preset(https://starship.rs/presets/tokyo-night),其格式使用 `░▒▓` 開頭、`` powerline 銜接與 `#a3aed2``#769ff0``#394260``#212736``#1d2230` 色階。為避免破壞偏好原本樣式的使用者,下一步應恢復 classic renderer 並新增 preset selector,而不是拆成另一個 extension。
8+
9+
## Non-Goals
10+
11+
- 不解析 Starship TOML。
12+
- 不新增 YAML/JSON config。
13+
- 不做任意 palette/layout 自訂。
14+
- 不新增 runtime dependency。
15+
16+
## Assumptions
17+
18+
- `PI_STATUSLINE_PRESET=tokyo-night` 啟用新版 powerline 外觀。
19+
- `PI_STATUSLINE_PRESET=classic` 啟用原本外觀。
20+
- 未設定或設定無效時先使用 `tokyo-night` 作為目前新版預設;若要避免 breaking change,可在實作前改成 `classic`
21+
22+
## Plan
23+
24+
- [x]`extensions/pi-statusline/src/statusline.ts` 新增 `StatuslinePresetName = "classic" | "tokyo-night"``readStatuslinePreset()`,從 `process.env.PI_STATUSLINE_PRESET` 讀取 preset;已由 `rg "PI_STATUSLINE_PRESET|StatuslinePresetName" extensions/pi-statusline/src``npm run check --workspace @narumitw/pi-statusline` 驗證。
25+
- [x] 將目前 Tokyo Night powerline renderer 保留為 `renderTokyoNightStatusline()`,並讓 `renderStatusline()` 依 config preset dispatch;已由 `extensions/pi-statusline/presets/tokyo-night.ts``renderStatusline()` switch 驗證,`tokyo-night` 路徑保留 `░▒▓` / `` truecolor blocks。
26+
- [x] 從 git diff 或先前版本還原 classic renderer 所需邏輯:`RIGHT_SEGMENTS`、palette/density/separator、`joinSegments()``styleSegment()``thinkingColor()``contextColor()` 與 labeled segment color;已由 `extensions/pi-statusline/presets/classic.ts``pickColor()``thinkingColor()``contextColor()` 與 typecheck 驗證。
27+
- [x] 依使用者要求將各 preset 拆成不同 `.ts` 檔,包含 `extensions/pi-statusline/presets/classic.ts``extensions/pi-statusline/presets/tokyo-night.ts``extensions/pi-statusline/presets/ansi.ts``extensions/pi-statusline/presets/types.ts`;已由 `find extensions/pi-statusline -path '*presets/*.ts' -type f` 與 typecheck 驗證。
28+
- [x] 調整 shared segment model,確保 classic 需要的 `color` 與 tokyo-night 需要的 `block` 不互相污染;已由 `RenderSegment` 同時帶 `color` / `block`,preset renderer 各自只消費需要欄位,並由 `npm run check --workspace @narumitw/pi-statusline` 驗證。
29+
- [x] 讓第二行 extension statuses 依 preset 使用 separator:classic 使用 Pi theme dim ``,tokyo-night 使用 truecolor powerline-compatible ``;已由 `extensionStatusSeparator()``classicExtensionSeparator()``tokyoNightExtensionSeparator()` 與 typecheck 驗證。
30+
- [x] 更新 `extensions/pi-statusline/README.md`,記錄 `PI_STATUSLINE_PRESET=classic|tokyo-night`、預設值、無效值 fallback、emoji 仍保留、以及 Tokyo Night 靈感來源連結;已由 `rg "PI_STATUSLINE_PRESET|https://starship.rs/presets/tokyo-night" extensions/pi-statusline/README.md` 驗證。
31+
- [x] 執行 `npm run check --workspace @narumitw/pi-statusline``npm run check``just pack-statusline`;三個命令皆成功,pack dry-run 列出預期 package files:`LICENSE``README.md``package.json``src/statusline.ts``presets/*.ts`
32+
33+
## Risks
34+
35+
- 同時維護 classic 與 tokyo-night renderer 會增加少量重複邏輯;已以共用資料收集與小型 renderer function 控制範圍。
36+
- ANSI truecolor/bold reset 可能影響截斷或背景延續;已保留 `truncateToWidth(..., "")`,且 Tokyo Night renderer 不手動依 visible string 長度切割 ANSI 字串。
37+
38+
## Completion Checklist
39+
40+
- [x] `PI_STATUSLINE_PRESET` 支援 `classic``tokyo-night`,由 `extensions/pi-statusline/src/statusline.ts` 中的 preset selector 與 dispatch 邏輯驗證。
41+
- [x] Classic 外觀可用且保留原本 emojis、左右分欄與 separator,由 `extensions/pi-statusline/presets/classic.ts`、shared segment builder 與 typecheck 驗證。
42+
- [x] Tokyo Night 外觀可用且保留 `░▒▓` / `` blocks 與 emojis,由 `extensions/pi-statusline/presets/tokyo-night.ts`、shared segment builder 與 typecheck 驗證。
43+
- [x] Extension statuses 第二行仍保留 emoji icon preservation,由 `formatExtensionStatus()` / `splitExtensionStatusIcon()` 未破壞、preset-specific separator 與 check 通過驗證。
44+
- [x] README 記錄環境變數切換方式與 Tokyo Night preset 靈感來源,由 `extensions/pi-statusline/README.md` 內容驗證。
45+
- [x] 品質門檻通過,由 `npm run check --workspace @narumitw/pi-statusline``npm run check``just pack-statusline` 成功輸出驗證。

extensions/pi-statusline/README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ Use it to monitor model selection, thinking level, git branch, working directory
88

99
## ✨ Features
1010

11-
- Replaces the default Pi footer with a compact rich statusline.
11+
- Replaces the default Pi footer with a compact preset-based statusline.
1212
- Shows model, thinking level, git branch, project directory, active tool, context usage, tokens, cost, and clock.
1313
- Displays compact statuses published through Pi's generic extension status API.
1414
- Preserves extension-provided status icons when the status text starts with one.
1515
- Warns when the same extension package is installed from multiple sources.
16-
- Uses emoji-labeled segments for readability.
16+
- Uses emoji-labeled segments for readability in both classic and Tokyo Night presets.
1717
- Adapts to terminal width and truncates safely.
18-
- Requires no configuration.
18+
- Requires no configuration, with optional preset selection through `PI_STATUSLINE_PRESET`.
1919

2020
## 📦 Install
2121

@@ -35,22 +35,38 @@ Try this package locally from the repository root:
3535
pi -e ./extensions/pi-statusline
3636
```
3737

38+
## 🎨 Presets
39+
40+
`pi-statusline` supports presets through the `PI_STATUSLINE_PRESET` environment variable:
41+
42+
```bash
43+
PI_STATUSLINE_PRESET=tokyo-night pi
44+
PI_STATUSLINE_PRESET=classic pi
45+
```
46+
47+
Supported presets:
48+
49+
- `tokyo-night` — the default, inspired by the [Starship Tokyo Night preset](https://starship.rs/presets/tokyo-night), using `░▒▓` / `` powerline blocks and the Tokyo Night color ramp.
50+
- `classic` — a compact Pi-themed statusline with left-aligned `` separators.
51+
52+
Unset or invalid values fall back to `tokyo-night`. Both presets keep the same emoji-labeled information and preserve extension-provided status icons.
53+
3854
## 👀 What it shows
3955

40-
The default statusline includes:
56+
The default `tokyo-night` statusline uses a Starship-inspired `░▒▓` / `` powerline layout and includes:
4157

4258
- `π` brand marker.
4359
- 🤖 current model.
4460
- 🧠 thinking level.
45-
- 🌿 git branch.
4661
- 📁 current project directory.
47-
- 🔧 active or last tool.
48-
- 📊 context usage percentage.
62+
- 🌿 git branch.
63+
- ⚙ active or last tool.
64+
- 🪟 context usage percentage.
4965
- 🔢 token totals.
50-
- 💰 estimated cost.
66+
- 💸 estimated cost.
5167
- 🕒 clock.
5268

53-
Statuses from other extensions appear on their own compact line below the main statusline and are separated with ``.
69+
Statuses from other extensions appear on their own compact line below the main statusline and use each preset's separator.
5470

5571
`pi-statusline` is extension-agnostic: it consumes Pi's generic extension status API and does not import or depend on status-producing extensions. If an extension wants a custom icon, it should include that icon at the start of its status text, for example `ctx.ui.setStatus("goal", "🎯 active")`. Statuses without a leading icon use the generic `🔌` icon.
5672

@@ -76,6 +92,11 @@ Examples:
7692
extensions/pi-statusline/
7793
├── src/
7894
│ └── statusline.ts
95+
├── presets/
96+
│ ├── ansi.ts
97+
│ ├── classic.ts
98+
│ ├── tokyo-night.ts
99+
│ └── types.ts
79100
├── README.md
80101
├── LICENSE
81102
├── tsconfig.json

extensions/pi-statusline/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
],
1515
"files": [
1616
"src",
17+
"presets",
1718
"README.md",
1819
"LICENSE"
1920
],
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function ansiStyle(text: string, colors: { fg?: string; bg?: string }): string {
2+
const codes = [
3+
colors.fg ? truecolorCode("38", colors.fg) : undefined,
4+
colors.bg ? truecolorCode("48", colors.bg) : undefined,
5+
].filter((code): code is string => code !== undefined);
6+
if (codes.length === 0) return text;
7+
return `\u001b[${codes.join(";")}m${text}\u001b[0m`;
8+
}
9+
10+
export function ansiFg(hex: string, text: string): string {
11+
return ansiStyle(text, { fg: hex });
12+
}
13+
14+
function truecolorCode(prefix: "38" | "48", hex: string): string {
15+
const { red, green, blue } = hexToRgb(hex);
16+
return `${prefix};2;${red};${green};${blue}`;
17+
}
18+
19+
function hexToRgb(hex: string): { red: number; green: number; blue: number } {
20+
const normalized = hex.replace(/^#/, "");
21+
return {
22+
red: Number.parseInt(normalized.slice(0, 2), 16),
23+
green: Number.parseInt(normalized.slice(2, 4), 16),
24+
blue: Number.parseInt(normalized.slice(4, 6), 16),
25+
};
26+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
2+
import { truncateToWidth } from "@mariozechner/pi-tui";
3+
import type { RenderSegment, SeparatorName, StatuslineConfig } from "./types.js";
4+
5+
export function renderClassicStatusline(
6+
width: number,
7+
segments: RenderSegment[],
8+
theme: Theme,
9+
config: StatuslineConfig,
10+
): string {
11+
return truncateToWidth(joinSegments(segments, theme, config), width, "");
12+
}
13+
14+
export function classicExtensionSeparator(theme: Theme): string {
15+
return theme.fg("dim", " • ");
16+
}
17+
18+
function joinSegments(segments: RenderSegment[], theme: Theme, config: StatuslineConfig): string {
19+
const separator = separatorText(config.separator);
20+
return segments
21+
.map((segment, index) => styleSegment(segment, index, theme, config))
22+
.join(theme.fg("dim", separator));
23+
}
24+
25+
function styleSegment(
26+
segment: RenderSegment,
27+
index: number,
28+
theme: Theme,
29+
config: StatuslineConfig,
30+
): string {
31+
const padding = config.density === "cozy" ? " " : "";
32+
const text = `${padding}${segment.text}${padding}`;
33+
const styledText = segment.emphasis ? theme.bold(text) : text;
34+
35+
if (config.palette === "mono") {
36+
return index === 0 ? theme.fg("muted", styledText) : theme.fg("dim", styledText);
37+
}
38+
39+
return theme.fg(segment.color as ThemeColor, styledText);
40+
}
41+
42+
function separatorText(separator: SeparatorName): string {
43+
switch (separator) {
44+
case "powerline":
45+
return "  ";
46+
case "bar":
47+
return " │ ";
48+
case "round":
49+
return " ❯ ";
50+
case "none":
51+
return " ";
52+
case "dot":
53+
return " • ";
54+
}
55+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { Theme } from "@mariozechner/pi-coding-agent";
2+
import { truncateToWidth } from "@mariozechner/pi-tui";
3+
import { ansiFg, ansiStyle } from "./ansi.js";
4+
import type { RenderSegment, TokyoNightBlockName } from "./types.js";
5+
6+
interface TokyoNightBlock {
7+
name: TokyoNightBlockName;
8+
segments: RenderSegment[];
9+
}
10+
11+
interface BlockColors {
12+
fg: string;
13+
bg: string;
14+
}
15+
16+
const TOKYO_NIGHT_COLORS = {
17+
lead: "#a3aed2",
18+
header: { fg: "#090c0c", bg: "#a3aed2" },
19+
directory: { fg: "#e3e5e5", bg: "#769ff0" },
20+
git: { fg: "#769ff0", bg: "#394260" },
21+
runtime: { fg: "#769ff0", bg: "#212736" },
22+
meter: { fg: "#a0a9cb", bg: "#1d2230" },
23+
extensionSeparator: "#394260",
24+
} as const satisfies Record<string, string | BlockColors>;
25+
26+
const TOKYO_NIGHT_BLOCK_ORDER: TokyoNightBlockName[] = [
27+
"header",
28+
"directory",
29+
"git",
30+
"runtime",
31+
"meter",
32+
];
33+
34+
export function renderTokyoNightStatusline(width: number, segments: RenderSegment[]): string {
35+
return truncateToWidth(joinTokyoNightSegments(segments), width, "");
36+
}
37+
38+
export function tokyoNightExtensionSeparator(_theme: Theme): string {
39+
return ansiFg(TOKYO_NIGHT_COLORS.extensionSeparator, "  ");
40+
}
41+
42+
function joinTokyoNightSegments(segments: RenderSegment[]): string {
43+
const blocks = groupTokyoNightBlocks(segments);
44+
let line = ansiFg(TOKYO_NIGHT_COLORS.lead, "░▒▓");
45+
46+
for (const [index, block] of blocks.entries()) {
47+
const colors = getTokyoNightBlockColors(block.name);
48+
const previous =
49+
index === 0 ? undefined : getTokyoNightBlockColors(blocks[index - 1]?.name ?? "header");
50+
if (previous) line += ansiStyle("", { fg: previous.bg, bg: colors.bg });
51+
line += ansiStyle(formatTokyoNightBlockText(block), colors);
52+
}
53+
54+
const lastBlock = blocks.at(-1);
55+
if (lastBlock) line += ansiFg(getTokyoNightBlockColors(lastBlock.name).bg, "");
56+
57+
return line;
58+
}
59+
60+
function groupTokyoNightBlocks(segments: RenderSegment[]): TokyoNightBlock[] {
61+
const blocksByName = new Map<TokyoNightBlockName, RenderSegment[]>();
62+
for (const segment of segments) {
63+
const blockSegments = blocksByName.get(segment.block) ?? [];
64+
blockSegments.push(segment);
65+
blocksByName.set(segment.block, blockSegments);
66+
}
67+
68+
return TOKYO_NIGHT_BLOCK_ORDER.flatMap((name) => {
69+
const blockSegments = blocksByName.get(name);
70+
return blockSegments ? [{ name, segments: blockSegments }] : [];
71+
});
72+
}
73+
74+
function formatTokyoNightBlockText(block: TokyoNightBlock): string {
75+
return ` ${block.segments.map(formatTokyoNightSegmentText).join(" ")}`;
76+
}
77+
78+
function formatTokyoNightSegmentText(segment: RenderSegment): string {
79+
return segment.emphasis ? `\u001b[1m${segment.text}\u001b[22m` : segment.text;
80+
}
81+
82+
function getTokyoNightBlockColors(block: TokyoNightBlockName): BlockColors {
83+
return TOKYO_NIGHT_COLORS[block];
84+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ThemeColor } from "@mariozechner/pi-coding-agent";
2+
3+
export type SegmentName =
4+
| "brand"
5+
| "model"
6+
| "thinking"
7+
| "cwd"
8+
| "branch"
9+
| "tools"
10+
| "context"
11+
| "tokens"
12+
| "cost"
13+
| "time"
14+
| "turn";
15+
16+
export type StatuslinePresetName = "classic" | "tokyo-night";
17+
export type TokyoNightBlockName = "header" | "directory" | "git" | "runtime" | "meter";
18+
export type PaletteName = "ocean" | "sunset" | "forest" | "candy" | "neon" | "mono";
19+
export type Density = "compact" | "cozy";
20+
export type SeparatorName = "dot" | "bar" | "powerline" | "round" | "none";
21+
22+
export interface StatuslineConfig {
23+
preset: StatuslinePresetName;
24+
palette: PaletteName;
25+
density: Density;
26+
separator: SeparatorName;
27+
showLabels: boolean;
28+
segments: SegmentName[];
29+
}
30+
31+
export interface RenderSegment {
32+
name: SegmentName;
33+
text: string;
34+
color: ThemeColor;
35+
block: TokyoNightBlockName;
36+
emphasis?: boolean;
37+
}

0 commit comments

Comments
 (0)