@narumitw/pi-statusline 0.20.0 → 0.23.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 +143 -95
- package/package.json +3 -4
- package/src/commands.ts +118 -0
- package/src/extension-status.ts +29 -32
- package/src/git-status.ts +11 -3
- package/src/render.ts +104 -64
- package/src/settings.ts +492 -109
- package/src/statusline.ts +51 -19
- package/src/tokyo-night.ts +180 -0
- package/src/types.ts +67 -0
- package/presets/classic.ts +0 -55
- package/presets/tokyo-night.ts +0 -84
- package/presets/types.ts +0 -38
- /package/{presets → src}/ansi.ts +0 -0
package/src/statusline.ts
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
getAgentDir,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { registerStatuslineCommand } from "./commands.js";
|
|
2
7
|
import {
|
|
3
8
|
buildExtensionStatusIconAliases,
|
|
4
9
|
type ExtensionStatusIconAliasMap,
|
|
5
10
|
findDuplicateExtensions,
|
|
6
11
|
readInstalledExtensionPackages,
|
|
7
12
|
} from "./extension-status.js";
|
|
13
|
+
import { type GitStatusSummary, gitStatusSummaryEqual, readGitStatus } from "./git-status.js";
|
|
14
|
+
import { type RuntimeState, renderExtensionStatusline, renderStatusline } from "./render.js";
|
|
8
15
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
renderStatusline,
|
|
16
|
-
type RuntimeState,
|
|
17
|
-
} from "./render.js";
|
|
18
|
-
import { consumeStatuslineSettingsNotice, createDefaultConfig } from "./settings.js";
|
|
16
|
+
consumeStatuslineSettingsNotice,
|
|
17
|
+
type LoadedStatuslineSettings,
|
|
18
|
+
loadOrCreateStatuslineSettings,
|
|
19
|
+
loadStatuslineSettings,
|
|
20
|
+
settingsFilePath,
|
|
21
|
+
} from "./settings.js";
|
|
19
22
|
|
|
20
23
|
const STATUSLINE_KEY = "statusline";
|
|
21
24
|
const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
|
|
@@ -23,7 +26,7 @@ const GIT_STATUS_EVENT_DEBOUNCE_MS = 250;
|
|
|
23
26
|
const EMPTY_EXTENSION_STATUS_ICON_ALIASES: ExtensionStatusIconAliasMap = new Map();
|
|
24
27
|
|
|
25
28
|
export default function statusline(pi: ExtensionAPI) {
|
|
26
|
-
|
|
29
|
+
let loaded: LoadedStatuslineSettings | undefined;
|
|
27
30
|
const runtime: RuntimeState = {
|
|
28
31
|
turnCount: 0,
|
|
29
32
|
activeTools: new Map(),
|
|
@@ -38,9 +41,7 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
38
41
|
let activeGitStatusTarget: { cwd: string; generation: number } | undefined;
|
|
39
42
|
let gitStatusRefreshInFlight = false;
|
|
40
43
|
let gitStatusDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
41
|
-
let pendingGitStatusRefresh:
|
|
42
|
-
| { cwd: string; generation: number; requestId: number }
|
|
43
|
-
| undefined;
|
|
44
|
+
let pendingGitStatusRefresh: { cwd: string; generation: number; requestId: number } | undefined;
|
|
44
45
|
|
|
45
46
|
const refresh = () => runtime.requestRender?.();
|
|
46
47
|
|
|
@@ -116,7 +117,7 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
116
117
|
runtime.duplicateExtensions = [];
|
|
117
118
|
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES;
|
|
118
119
|
ctx.ui.setStatus(STATUSLINE_KEY, undefined);
|
|
119
|
-
if (!activeGitStatusTarget) return;
|
|
120
|
+
if (!activeGitStatusTarget || !loaded) return;
|
|
120
121
|
const installedPackages = readInstalledExtensionPackages(cwd);
|
|
121
122
|
runtime.duplicateExtensions = findDuplicateExtensions(installedPackages);
|
|
122
123
|
runtime.extensionStatusIconAliases = buildExtensionStatusIconAliases(installedPackages);
|
|
@@ -152,8 +153,13 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
152
153
|
},
|
|
153
154
|
invalidate() {},
|
|
154
155
|
render(width: number): string[] {
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
if (!loaded) return [];
|
|
157
|
+
const config = loaded.config;
|
|
158
|
+
const mainLine = renderStatusline(width, ctx, footerData, theme, config, runtime);
|
|
159
|
+
const lines = mainLine ? mainLine.split("\n") : [];
|
|
160
|
+
lines.push(
|
|
161
|
+
...renderExtensionStatusline(width, footerData, theme, config, runtime, mainLine),
|
|
162
|
+
);
|
|
157
163
|
return lines;
|
|
158
164
|
},
|
|
159
165
|
};
|
|
@@ -161,9 +167,24 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
161
167
|
refreshGitStatus(cwd, generation);
|
|
162
168
|
};
|
|
163
169
|
|
|
170
|
+
const agentDir = getAgentDir();
|
|
171
|
+
const configPath = settingsFilePath(agentDir);
|
|
172
|
+
registerStatuslineCommand(pi, {
|
|
173
|
+
settingsPath: configPath,
|
|
174
|
+
getLoaded: () => loaded ?? loadStatuslineSettings(configPath),
|
|
175
|
+
apply(next) {
|
|
176
|
+
loaded = next;
|
|
177
|
+
refresh();
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
164
181
|
pi.on("session_start", (_event, ctx) => {
|
|
182
|
+
loaded = loadOrCreateStatuslineSettings(agentDir);
|
|
165
183
|
const settingsNotice = consumeStatuslineSettingsNotice();
|
|
166
184
|
if (settingsNotice) ctx.ui.notify(settingsNotice, "warning");
|
|
185
|
+
if (loaded.diagnostics.length > 0) {
|
|
186
|
+
ctx.ui.notify(formatSettingsDiagnostics(loaded), "warning");
|
|
187
|
+
}
|
|
167
188
|
runtime.thinkingLevel = pi.getThinkingLevel();
|
|
168
189
|
installFooter(ctx);
|
|
169
190
|
});
|
|
@@ -233,6 +254,15 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
233
254
|
});
|
|
234
255
|
}
|
|
235
256
|
|
|
257
|
+
function formatSettingsDiagnostics(loaded: LoadedStatuslineSettings): string {
|
|
258
|
+
const details = loaded.diagnostics.slice(0, 5).map((item) => item.message);
|
|
259
|
+
const remaining = loaded.diagnostics.length - details.length;
|
|
260
|
+
return [
|
|
261
|
+
`pi-statusline settings: ${details.join("; ")}`,
|
|
262
|
+
...(remaining > 0 ? [`+${remaining} more`] : []),
|
|
263
|
+
].join(" ");
|
|
264
|
+
}
|
|
265
|
+
|
|
236
266
|
export {
|
|
237
267
|
buildExtensionStatusIconAliases,
|
|
238
268
|
type ExtensionStatusIconAliasMap,
|
|
@@ -246,14 +276,16 @@ export {
|
|
|
246
276
|
} from "./extension-status.js";
|
|
247
277
|
export {
|
|
248
278
|
formatGitBranchText,
|
|
279
|
+
formatGitBranchValue,
|
|
249
280
|
formatGitStatusSummary,
|
|
250
|
-
parseGitStatusPorcelain,
|
|
251
281
|
type GitStatusSummary,
|
|
282
|
+
parseGitStatusPorcelain,
|
|
252
283
|
} from "./git-status.js";
|
|
253
284
|
export {
|
|
254
285
|
contextColor,
|
|
255
286
|
formatCount,
|
|
256
287
|
formatToolActivity,
|
|
288
|
+
prContextFromStatuses,
|
|
257
289
|
prLinkFromStatuses,
|
|
258
290
|
shortenModel,
|
|
259
291
|
} from "./render.js";
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { ansiFg, ansiStyle } from "./ansi.js";
|
|
4
|
+
import {
|
|
5
|
+
LINE_BREAK_SEGMENT_NAME,
|
|
6
|
+
type PaletteName,
|
|
7
|
+
type RenderItem,
|
|
8
|
+
type RenderSegment,
|
|
9
|
+
type SeparatorName,
|
|
10
|
+
type StatuslineConfig,
|
|
11
|
+
type TokyoNightBlockName,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
interface TokyoNightBlock {
|
|
15
|
+
name: TokyoNightBlockName;
|
|
16
|
+
segments: RenderSegment[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface BlockColors {
|
|
20
|
+
fg: string;
|
|
21
|
+
bg: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PowerlinePalette {
|
|
25
|
+
lead: string;
|
|
26
|
+
blocks: Record<TokyoNightBlockName, BlockColors>;
|
|
27
|
+
extensionSeparator: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const BLOCK_NAMES: TokyoNightBlockName[] = ["header", "directory", "git", "runtime", "meter"];
|
|
31
|
+
|
|
32
|
+
const TOKYO_NIGHT_PALETTE: PowerlinePalette = {
|
|
33
|
+
lead: "#a3aed2",
|
|
34
|
+
blocks: {
|
|
35
|
+
header: { fg: "#090c0c", bg: "#a3aed2" },
|
|
36
|
+
directory: { fg: "#e3e5e5", bg: "#769ff0" },
|
|
37
|
+
git: { fg: "#769ff0", bg: "#394260" },
|
|
38
|
+
runtime: { fg: "#769ff0", bg: "#212736" },
|
|
39
|
+
meter: { fg: "#a0a9cb", bg: "#1d2230" },
|
|
40
|
+
},
|
|
41
|
+
extensionSeparator: "#394260",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const SEMANTIC_COLORS = {
|
|
45
|
+
accent: "#7aa2f7",
|
|
46
|
+
muted: "#565f89",
|
|
47
|
+
success: "#9ece6a",
|
|
48
|
+
warning: "#e0af68",
|
|
49
|
+
error: "#f7768e",
|
|
50
|
+
dim: "#414868",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
type SemanticColor = keyof typeof SEMANTIC_COLORS;
|
|
54
|
+
|
|
55
|
+
const PALETTE_SEQUENCES: Record<Exclude<PaletteName, "tokyo-night">, SemanticColor[]> = {
|
|
56
|
+
ocean: ["accent", "muted", "success", "warning"],
|
|
57
|
+
sunset: ["warning", "accent", "success", "muted"],
|
|
58
|
+
forest: ["success", "accent", "muted", "warning"],
|
|
59
|
+
candy: ["accent", "warning", "success", "muted"],
|
|
60
|
+
neon: ["accent", "success", "warning", "error"],
|
|
61
|
+
mono: ["muted", "dim"],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function renderTokyoNightStatusline(
|
|
65
|
+
width: number,
|
|
66
|
+
items: RenderItem[],
|
|
67
|
+
config: Pick<StatuslineConfig, "palette" | "density" | "separator">,
|
|
68
|
+
): string {
|
|
69
|
+
if (items.length === 0 || width <= 0) return "";
|
|
70
|
+
return splitLines(items)
|
|
71
|
+
.map((segments) =>
|
|
72
|
+
segments.length === 0
|
|
73
|
+
? ""
|
|
74
|
+
: truncateToWidth(joinTokyoNightSegments(segments, config), width, ""),
|
|
75
|
+
)
|
|
76
|
+
.join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function splitLines(items: RenderItem[]): RenderSegment[][] {
|
|
80
|
+
const lines: RenderSegment[][] = [[]];
|
|
81
|
+
for (const item of items) {
|
|
82
|
+
if (item.name === LINE_BREAK_SEGMENT_NAME) lines.push([]);
|
|
83
|
+
else lines.at(-1)?.push(item);
|
|
84
|
+
}
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function tokyoNightExtensionSeparator(
|
|
89
|
+
_theme: Theme,
|
|
90
|
+
paletteName: PaletteName = "tokyo-night",
|
|
91
|
+
): string {
|
|
92
|
+
return ansiFg(resolvePalette(paletteName).extensionSeparator, " • ");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function joinTokyoNightSegments(
|
|
96
|
+
segments: RenderSegment[],
|
|
97
|
+
config: Pick<StatuslineConfig, "palette" | "density" | "separator">,
|
|
98
|
+
): string {
|
|
99
|
+
const palette = resolvePalette(config.palette);
|
|
100
|
+
const blocks = contiguousBlocks(segments);
|
|
101
|
+
let line = ansiFg(palette.lead, "░▒▓");
|
|
102
|
+
|
|
103
|
+
for (const [index, block] of blocks.entries()) {
|
|
104
|
+
const colors = palette.blocks[block.name];
|
|
105
|
+
const previous = index === 0 ? undefined : palette.blocks[blocks[index - 1]?.name ?? "header"];
|
|
106
|
+
if (previous) line += ansiStyle("", { fg: previous.bg, bg: colors.bg });
|
|
107
|
+
line += ansiStyle(formatBlockText(block, config), colors);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lastBlock = blocks.at(-1);
|
|
111
|
+
if (lastBlock) line += ansiFg(palette.blocks[lastBlock.name].bg, "");
|
|
112
|
+
return line;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function contiguousBlocks(segments: RenderSegment[]): TokyoNightBlock[] {
|
|
116
|
+
const blocks: TokyoNightBlock[] = [];
|
|
117
|
+
for (const segment of segments) {
|
|
118
|
+
const previous = blocks.at(-1);
|
|
119
|
+
if (previous?.name === segment.block) previous.segments.push(segment);
|
|
120
|
+
else blocks.push({ name: segment.block, segments: [segment] });
|
|
121
|
+
}
|
|
122
|
+
return blocks;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatBlockText(
|
|
126
|
+
block: TokyoNightBlock,
|
|
127
|
+
config: Pick<StatuslineConfig, "density" | "separator">,
|
|
128
|
+
): string {
|
|
129
|
+
const texts = block.segments.map(formatSegmentText);
|
|
130
|
+
const separator = separatorText(config.separator, config.density === "cozy");
|
|
131
|
+
const leading = config.density === "cozy" ? " " : " ";
|
|
132
|
+
const trailing = config.density === "cozy" ? " " : "";
|
|
133
|
+
return `${leading}${texts.join(separator)}${trailing}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatSegmentText(segment: RenderSegment): string {
|
|
137
|
+
return segment.emphasis ? `\u001b[1m${segment.text}\u001b[22m` : segment.text;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function separatorText(separator: SeparatorName, cozy: boolean): string {
|
|
141
|
+
const padding = cozy ? " " : " ";
|
|
142
|
+
switch (separator) {
|
|
143
|
+
case "dot":
|
|
144
|
+
return `${padding}•${padding}`;
|
|
145
|
+
case "bar":
|
|
146
|
+
return `${padding}│${padding}`;
|
|
147
|
+
case "powerline":
|
|
148
|
+
return `${padding}${padding}`;
|
|
149
|
+
case "round":
|
|
150
|
+
return `${padding}❯${padding}`;
|
|
151
|
+
case "none":
|
|
152
|
+
return padding;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolvePalette(name: PaletteName): PowerlinePalette {
|
|
157
|
+
if (name === "tokyo-night") return TOKYO_NIGHT_PALETTE;
|
|
158
|
+
const sequence = PALETTE_SEQUENCES[name];
|
|
159
|
+
const backgrounds = BLOCK_NAMES.map(
|
|
160
|
+
(_block, index) => SEMANTIC_COLORS[sequence[index % sequence.length] ?? "muted"],
|
|
161
|
+
);
|
|
162
|
+
return {
|
|
163
|
+
lead: backgrounds[0] ?? SEMANTIC_COLORS.accent,
|
|
164
|
+
blocks: Object.fromEntries(
|
|
165
|
+
BLOCK_NAMES.map((block, index) => {
|
|
166
|
+
const background = backgrounds[index] ?? SEMANTIC_COLORS.muted;
|
|
167
|
+
return [block, { fg: contrastColor(background), bg: background }];
|
|
168
|
+
}),
|
|
169
|
+
) as Record<TokyoNightBlockName, BlockColors>,
|
|
170
|
+
extensionSeparator: backgrounds[2] ?? SEMANTIC_COLORS.muted,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function contrastColor(hex: string): string {
|
|
175
|
+
const normalized = hex.slice(1);
|
|
176
|
+
const red = Number.parseInt(normalized.slice(0, 2), 16);
|
|
177
|
+
const green = Number.parseInt(normalized.slice(2, 4), 16);
|
|
178
|
+
const blue = Number.parseInt(normalized.slice(4, 6), 16);
|
|
179
|
+
return red * 0.299 + green * 0.587 + blue * 0.114 > 150 ? "#090c0c" : "#f0f0f0";
|
|
180
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const SEGMENT_NAMES = [
|
|
4
|
+
"brand",
|
|
5
|
+
"provider",
|
|
6
|
+
"model",
|
|
7
|
+
"thinking",
|
|
8
|
+
"cwd",
|
|
9
|
+
"branch",
|
|
10
|
+
"tools",
|
|
11
|
+
"context",
|
|
12
|
+
"tokens",
|
|
13
|
+
"cost",
|
|
14
|
+
"time",
|
|
15
|
+
"turn",
|
|
16
|
+
] as const;
|
|
17
|
+
export type SegmentName = (typeof SEGMENT_NAMES)[number];
|
|
18
|
+
|
|
19
|
+
export const LINE_BREAK_SEGMENT_NAME = "line_break" as const;
|
|
20
|
+
export type ConfigSegmentName = SegmentName | typeof LINE_BREAK_SEGMENT_NAME;
|
|
21
|
+
|
|
22
|
+
export const PALETTE_NAMES = [
|
|
23
|
+
"tokyo-night",
|
|
24
|
+
"ocean",
|
|
25
|
+
"sunset",
|
|
26
|
+
"forest",
|
|
27
|
+
"candy",
|
|
28
|
+
"neon",
|
|
29
|
+
"mono",
|
|
30
|
+
] as const;
|
|
31
|
+
export type PaletteName = (typeof PALETTE_NAMES)[number];
|
|
32
|
+
|
|
33
|
+
export const DENSITIES = ["compact", "cozy"] as const;
|
|
34
|
+
export type Density = (typeof DENSITIES)[number];
|
|
35
|
+
|
|
36
|
+
export const SEPARATOR_NAMES = ["none", "dot", "bar", "powerline", "round"] as const;
|
|
37
|
+
export type SeparatorName = (typeof SEPARATOR_NAMES)[number];
|
|
38
|
+
|
|
39
|
+
export type TokyoNightBlockName = "header" | "directory" | "git" | "runtime" | "meter";
|
|
40
|
+
|
|
41
|
+
export interface SegmentTextConfig {
|
|
42
|
+
prefix: string;
|
|
43
|
+
suffix: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StatuslineConfig {
|
|
47
|
+
palette: PaletteName;
|
|
48
|
+
density: Density;
|
|
49
|
+
separator: SeparatorName;
|
|
50
|
+
segments: ConfigSegmentName[];
|
|
51
|
+
segmentText: Record<SegmentName, SegmentTextConfig>;
|
|
52
|
+
extensionStatusIcons: Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RenderSegment {
|
|
56
|
+
name: SegmentName;
|
|
57
|
+
text: string;
|
|
58
|
+
color: ThemeColor;
|
|
59
|
+
block: TokyoNightBlockName;
|
|
60
|
+
emphasis?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RenderLineBreak {
|
|
64
|
+
name: typeof LINE_BREAK_SEGMENT_NAME;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type RenderItem = RenderSegment | RenderLineBreak;
|
package/presets/classic.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth } from "@earendil-works/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
|
-
}
|
package/presets/tokyo-night.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth } from "@earendil-works/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
|
-
}
|
package/presets/types.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { ThemeColor } from "@earendil-works/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
|
-
extensionStatusIcons: Record<string, string>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface RenderSegment {
|
|
33
|
-
name: SegmentName;
|
|
34
|
-
text: string;
|
|
35
|
-
color: ThemeColor;
|
|
36
|
-
block: TokyoNightBlockName;
|
|
37
|
-
emphasis?: boolean;
|
|
38
|
-
}
|
/package/{presets → src}/ansi.ts
RENAMED
|
File without changes
|