@narumitw/pi-statusline 0.23.0 → 0.28.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 +217 -119
- package/package.json +2 -2
- package/src/commands.ts +775 -9
- package/src/extension-status.ts +28 -2
- package/src/index.ts +1 -0
- package/src/information-profiles.ts +39 -0
- package/src/powerline.ts +168 -0
- package/src/presets/candy.ts +9 -0
- package/src/presets/create-ramp.ts +41 -0
- package/src/presets/custom.ts +11 -0
- package/src/presets/forest.ts +9 -0
- package/src/presets/index.ts +54 -0
- package/src/presets/mono.ts +9 -0
- package/src/presets/neon.ts +9 -0
- package/src/presets/ocean.ts +9 -0
- package/src/presets/sunset.ts +9 -0
- package/src/presets/tokyo-night.ts +13 -0
- package/src/presets/types.ts +12 -0
- package/src/render.ts +34 -23
- package/src/settings.ts +135 -22
- package/src/statusline.ts +42 -8
- package/src/types.ts +14 -3
- package/src/tokyo-night.ts +0 -180
package/src/settings.ts
CHANGED
|
@@ -10,50 +10,58 @@ import {
|
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { INFORMATION_PROFILES } from "./information-profiles.js";
|
|
14
|
+
import { segmentPaletteForPreset } from "./presets/index.js";
|
|
13
15
|
import {
|
|
14
16
|
type ConfigSegmentName,
|
|
15
17
|
DENSITIES,
|
|
16
18
|
LINE_BREAK_SEGMENT_NAME,
|
|
17
19
|
PALETTE_NAMES,
|
|
20
|
+
PALETTE_PRESET_NAMES,
|
|
21
|
+
type PaletteName,
|
|
18
22
|
SEGMENT_NAMES,
|
|
19
23
|
SEPARATOR_NAMES,
|
|
20
24
|
type SegmentName,
|
|
25
|
+
type SegmentPalette,
|
|
21
26
|
type StatuslineConfig,
|
|
22
27
|
} from "./types.js";
|
|
23
28
|
|
|
24
29
|
export const SETTINGS_FILE_NAME = "pi-statusline.json";
|
|
25
30
|
const LEGACY_SETTINGS_FILE_NAME = "pi-statusline-settings.json";
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"codex-usage": "📊",
|
|
32
|
+
const DEFAULT_DOCUMENT_EXTENSION_STATUS_ICONS: Record<string, string> = {
|
|
33
|
+
accounts: "👤",
|
|
30
34
|
caffeinate: "💊",
|
|
35
|
+
"chrome-devtools": "🌐",
|
|
31
36
|
firecrawl: "🔥",
|
|
32
37
|
"github-pr": "🔎",
|
|
33
38
|
goal: "🎯",
|
|
39
|
+
"google-genai": "✨",
|
|
34
40
|
lsp: "🧰",
|
|
35
41
|
"plan-mode": "📝",
|
|
36
|
-
|
|
42
|
+
retry: "🔁",
|
|
37
43
|
subagents: "🧑🤝🧑",
|
|
44
|
+
sync: "🔄",
|
|
45
|
+
usage: "📊",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_EXTENSION_STATUS_ICONS: Record<string, string> = {
|
|
49
|
+
...DEFAULT_DOCUMENT_EXTENSION_STATUS_ICONS,
|
|
50
|
+
"codex-usage": "📊",
|
|
51
|
+
pisync: "🔄",
|
|
38
52
|
"unknown-error-retry": "🔁",
|
|
39
53
|
};
|
|
40
54
|
|
|
41
|
-
const
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"branch",
|
|
48
|
-
"tools",
|
|
49
|
-
"context",
|
|
50
|
-
"tokens",
|
|
51
|
-
"cost",
|
|
52
|
-
"time",
|
|
53
|
-
];
|
|
55
|
+
const LEGACY_STATUS_ICON_KEYS = {
|
|
56
|
+
pisync: "sync",
|
|
57
|
+
"unknown-error-retry": "retry",
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
const DEFAULT_SEGMENTS: SegmentName[] = [...INFORMATION_PROFILES.balanced];
|
|
54
61
|
|
|
55
62
|
export const DEFAULT_STATUSLINE_CONFIG: StatuslineConfig = {
|
|
56
|
-
|
|
63
|
+
palettePreset: "tokyo-night",
|
|
64
|
+
palette: segmentPaletteForPreset("tokyo-night"),
|
|
57
65
|
density: "compact",
|
|
58
66
|
separator: "none",
|
|
59
67
|
segments: DEFAULT_SEGMENTS,
|
|
@@ -74,7 +82,20 @@ export const DEFAULT_STATUSLINE_CONFIG: StatuslineConfig = {
|
|
|
74
82
|
extensionStatusIcons: DEFAULT_EXTENSION_STATUS_ICONS,
|
|
75
83
|
};
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
const DEFAULT_STATUSLINE_DOCUMENT_CONFIG = {
|
|
86
|
+
palettePreset: DEFAULT_STATUSLINE_CONFIG.palettePreset,
|
|
87
|
+
density: DEFAULT_STATUSLINE_CONFIG.density,
|
|
88
|
+
separator: DEFAULT_STATUSLINE_CONFIG.separator,
|
|
89
|
+
segments: DEFAULT_SEGMENTS,
|
|
90
|
+
segmentText: DEFAULT_STATUSLINE_CONFIG.segmentText,
|
|
91
|
+
extensionStatusIcons: DEFAULT_DOCUMENT_EXTENSION_STATUS_ICONS,
|
|
92
|
+
} satisfies Omit<StatuslineConfig, "palette">;
|
|
93
|
+
|
|
94
|
+
export const DEFAULT_STATUSLINE_DOCUMENT = `${JSON.stringify(
|
|
95
|
+
DEFAULT_STATUSLINE_DOCUMENT_CONFIG,
|
|
96
|
+
null,
|
|
97
|
+
"\t",
|
|
98
|
+
)}\n`;
|
|
78
99
|
|
|
79
100
|
export interface StatuslineConfigDiagnostic {
|
|
80
101
|
severity: "warning" | "error";
|
|
@@ -127,8 +148,8 @@ export function normalizeStatuslineConfig(value: unknown): {
|
|
|
127
148
|
diagnostics: [invalidDiagnostic("", "Settings must contain a JSON object", "error")],
|
|
128
149
|
};
|
|
129
150
|
}
|
|
130
|
-
|
|
131
151
|
const knownRoot = new Set([
|
|
152
|
+
"palettePreset",
|
|
132
153
|
"palette",
|
|
133
154
|
"density",
|
|
134
155
|
"separator",
|
|
@@ -140,7 +161,11 @@ export function normalizeStatuslineConfig(value: unknown): {
|
|
|
140
161
|
if (!knownRoot.has(key)) diagnostics.push(unknownDiagnostic(key));
|
|
141
162
|
}
|
|
142
163
|
|
|
143
|
-
|
|
164
|
+
normalizePalette(value.palette, config, diagnostics);
|
|
165
|
+
normalizeEnum(value, "palettePreset", PALETTE_PRESET_NAMES, config, diagnostics);
|
|
166
|
+
if (!isRecord(value.palette) && isPaletteName(config.palettePreset)) {
|
|
167
|
+
config.palette = segmentPaletteForPreset(config.palettePreset);
|
|
168
|
+
}
|
|
144
169
|
normalizeEnum(value, "density", DENSITIES, config, diagnostics);
|
|
145
170
|
normalizeEnum(value, "separator", SEPARATOR_NAMES, config, diagnostics);
|
|
146
171
|
|
|
@@ -237,6 +262,28 @@ export function normalizeStatuslineConfig(value: unknown): {
|
|
|
237
262
|
writable: true,
|
|
238
263
|
});
|
|
239
264
|
}
|
|
265
|
+
for (const [legacyKey, canonicalKey] of Object.entries(LEGACY_STATUS_ICON_KEYS)) {
|
|
266
|
+
const legacyIcon = Object.hasOwn(value.extensionStatusIcons, legacyKey)
|
|
267
|
+
? value.extensionStatusIcons[legacyKey]
|
|
268
|
+
: undefined;
|
|
269
|
+
const canonicalIcon = Object.hasOwn(value.extensionStatusIcons, canonicalKey)
|
|
270
|
+
? value.extensionStatusIcons[canonicalKey]
|
|
271
|
+
: undefined;
|
|
272
|
+
const targetKey = typeof canonicalIcon === "string" ? legacyKey : canonicalKey;
|
|
273
|
+
const inheritedIcon =
|
|
274
|
+
typeof canonicalIcon === "string"
|
|
275
|
+
? canonicalIcon
|
|
276
|
+
: typeof legacyIcon === "string"
|
|
277
|
+
? legacyIcon
|
|
278
|
+
: undefined;
|
|
279
|
+
if (inheritedIcon === undefined) continue;
|
|
280
|
+
Object.defineProperty(config.extensionStatusIcons, targetKey, {
|
|
281
|
+
value: inheritedIcon,
|
|
282
|
+
enumerable: true,
|
|
283
|
+
configurable: true,
|
|
284
|
+
writable: true,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
240
287
|
}
|
|
241
288
|
}
|
|
242
289
|
|
|
@@ -412,8 +459,63 @@ export function normalizeStatuslineSettings(value: unknown): StatuslineConfig {
|
|
|
412
459
|
return normalizeStatuslineConfig(value).config;
|
|
413
460
|
}
|
|
414
461
|
|
|
462
|
+
function normalizePalette(
|
|
463
|
+
value: unknown,
|
|
464
|
+
config: StatuslineConfig,
|
|
465
|
+
diagnostics: StatuslineConfigDiagnostic[],
|
|
466
|
+
) {
|
|
467
|
+
if (value === undefined) return;
|
|
468
|
+
if (typeof value === "string") {
|
|
469
|
+
if (!(PALETTE_NAMES as readonly string[]).includes(value)) {
|
|
470
|
+
diagnostics.push(
|
|
471
|
+
invalidDiagnostic(
|
|
472
|
+
"palette",
|
|
473
|
+
`Expected a palette object or one of: ${PALETTE_NAMES.join(", ")}`,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
config.palettePreset = value as (typeof PALETTE_NAMES)[number];
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!isRecord(value)) {
|
|
482
|
+
diagnostics.push(invalidDiagnostic("palette", "Expected a palette object"));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const palette: SegmentPalette = {};
|
|
487
|
+
config.palette = palette;
|
|
488
|
+
for (const [name, colors] of Object.entries(value)) {
|
|
489
|
+
const path = `palette.${name}`;
|
|
490
|
+
if (!isSegmentName(name)) {
|
|
491
|
+
diagnostics.push(unknownDiagnostic(path));
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (!isRecord(colors)) {
|
|
495
|
+
diagnostics.push(invalidDiagnostic(path, "Expected an object"));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const normalizedColors: NonNullable<SegmentPalette[SegmentName]> = {};
|
|
499
|
+
palette[name] = normalizedColors;
|
|
500
|
+
for (const [field, color] of Object.entries(colors)) {
|
|
501
|
+
const colorPath = `${path}.${field}`;
|
|
502
|
+
if (field !== "fg" && field !== "bg") {
|
|
503
|
+
diagnostics.push(unknownDiagnostic(colorPath));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (typeof color !== "string" || !/^#[0-9a-f]{6}$/iu.test(color)) {
|
|
507
|
+
diagnostics.push(invalidDiagnostic(colorPath, "Expected a full #RRGGBB hexadecimal color"));
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
normalizedColors[field] = color.toLowerCase();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
config.palette = palette;
|
|
514
|
+
config.palettePreset = "custom";
|
|
515
|
+
}
|
|
516
|
+
|
|
415
517
|
function normalizeEnum<
|
|
416
|
-
K extends "
|
|
518
|
+
K extends "palettePreset" | "density" | "separator",
|
|
417
519
|
T extends StatuslineConfig[K],
|
|
418
520
|
>(
|
|
419
521
|
value: Record<string, unknown>,
|
|
@@ -433,9 +535,16 @@ function normalizeEnum<
|
|
|
433
535
|
config[field] = candidate as StatuslineConfig[K];
|
|
434
536
|
}
|
|
435
537
|
|
|
538
|
+
function cloneSegmentPalette(palette: SegmentPalette): SegmentPalette {
|
|
539
|
+
return Object.fromEntries(
|
|
540
|
+
Object.entries(palette).map(([name, colors]) => [name, { ...colors }]),
|
|
541
|
+
) as SegmentPalette;
|
|
542
|
+
}
|
|
543
|
+
|
|
436
544
|
function cloneConfig(config: StatuslineConfig): StatuslineConfig {
|
|
437
545
|
return {
|
|
438
546
|
...config,
|
|
547
|
+
palette: cloneSegmentPalette(config.palette),
|
|
439
548
|
segments: [...config.segments],
|
|
440
549
|
segmentText: Object.fromEntries(
|
|
441
550
|
SEGMENT_NAMES.map((name) => [name, { ...config.segmentText[name] }]),
|
|
@@ -472,6 +581,10 @@ function isConfigSegmentName(value: string): value is ConfigSegmentName {
|
|
|
472
581
|
return value === LINE_BREAK_SEGMENT_NAME || isSegmentName(value);
|
|
473
582
|
}
|
|
474
583
|
|
|
584
|
+
function isPaletteName(value: StatuslineConfig["palettePreset"]): value is PaletteName {
|
|
585
|
+
return (PALETTE_NAMES as readonly StatuslineConfig["palettePreset"][]).includes(value);
|
|
586
|
+
}
|
|
587
|
+
|
|
475
588
|
function isSegmentName(value: string): value is SegmentName {
|
|
476
589
|
return (SEGMENT_NAMES as readonly string[]).includes(value);
|
|
477
590
|
}
|
package/src/statusline.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
loadStatuslineSettings,
|
|
20
20
|
settingsFilePath,
|
|
21
21
|
} from "./settings.js";
|
|
22
|
+
import type { PalettePreset } from "./types.js";
|
|
22
23
|
|
|
23
24
|
const STATUSLINE_KEY = "statusline";
|
|
24
25
|
const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
|
|
@@ -27,6 +28,8 @@ const EMPTY_EXTENSION_STATUS_ICON_ALIASES: ExtensionStatusIconAliasMap = new Map
|
|
|
27
28
|
|
|
28
29
|
export default function statusline(pi: ExtensionAPI) {
|
|
29
30
|
let loaded: LoadedStatuslineSettings | undefined;
|
|
31
|
+
let previewPalettePreset: PalettePreset | undefined;
|
|
32
|
+
let activeSessionManager: ExtensionContext["sessionManager"] | undefined;
|
|
30
33
|
const runtime: RuntimeState = {
|
|
31
34
|
turnCount: 0,
|
|
32
35
|
activeTools: new Map(),
|
|
@@ -44,6 +47,7 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
44
47
|
let pendingGitStatusRefresh: { cwd: string; generation: number; requestId: number } | undefined;
|
|
45
48
|
|
|
46
49
|
const refresh = () => runtime.requestRender?.();
|
|
50
|
+
const ownsRuntime = (ctx: ExtensionContext) => ctx.sessionManager === activeSessionManager;
|
|
47
51
|
|
|
48
52
|
const setGitStatus = (summary: GitStatusSummary | undefined) => {
|
|
49
53
|
if (gitStatusSummaryEqual(runtime.gitStatus, summary)) return;
|
|
@@ -111,6 +115,8 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
111
115
|
const installFooter = (ctx: ExtensionContext) => {
|
|
112
116
|
const generation = ++sessionGeneration;
|
|
113
117
|
const cwd = ctx.cwd;
|
|
118
|
+
activeSessionManager = ctx.sessionManager;
|
|
119
|
+
previewPalettePreset = undefined;
|
|
114
120
|
clearGitStatusDebounce();
|
|
115
121
|
activeGitStatusTarget = ctx.mode === "tui" ? { cwd, generation } : undefined;
|
|
116
122
|
runtime.gitStatus = undefined;
|
|
@@ -154,7 +160,9 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
154
160
|
invalidate() {},
|
|
155
161
|
render(width: number): string[] {
|
|
156
162
|
if (!loaded) return [];
|
|
157
|
-
const config =
|
|
163
|
+
const config = previewPalettePreset
|
|
164
|
+
? { ...loaded.config, palettePreset: previewPalettePreset }
|
|
165
|
+
: loaded.config;
|
|
158
166
|
const mainLine = renderStatusline(width, ctx, footerData, theme, config, runtime);
|
|
159
167
|
const lines = mainLine ? mainLine.split("\n") : [];
|
|
160
168
|
lines.push(
|
|
@@ -172,13 +180,23 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
172
180
|
registerStatuslineCommand(pi, {
|
|
173
181
|
settingsPath: configPath,
|
|
174
182
|
getLoaded: () => loaded ?? loadStatuslineSettings(configPath),
|
|
175
|
-
apply(next) {
|
|
183
|
+
apply(next, ctx) {
|
|
184
|
+
if (ctx.sessionManager !== activeSessionManager) return;
|
|
185
|
+
previewPalettePreset = undefined;
|
|
176
186
|
loaded = next;
|
|
177
187
|
refresh();
|
|
178
188
|
},
|
|
189
|
+
preview(palettePreset, ctx) {
|
|
190
|
+
if (ctx.sessionManager !== activeSessionManager) return;
|
|
191
|
+
previewPalettePreset = palettePreset;
|
|
192
|
+
refresh();
|
|
193
|
+
},
|
|
179
194
|
});
|
|
180
195
|
|
|
181
196
|
pi.on("session_start", (_event, ctx) => {
|
|
197
|
+
runtime.turnCount = 0;
|
|
198
|
+
runtime.activeTools.clear();
|
|
199
|
+
runtime.isStreaming = false;
|
|
182
200
|
loaded = loadOrCreateStatuslineSettings(agentDir);
|
|
183
201
|
const settingsNotice = consumeStatuslineSettingsNotice();
|
|
184
202
|
if (settingsNotice) ctx.ui.notify(settingsNotice, "warning");
|
|
@@ -195,11 +213,16 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
195
213
|
});
|
|
196
214
|
|
|
197
215
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
216
|
+
if (!ownsRuntime(ctx)) return;
|
|
198
217
|
sessionGeneration += 1;
|
|
218
|
+
activeSessionManager = undefined;
|
|
219
|
+
previewPalettePreset = undefined;
|
|
199
220
|
activeGitStatusTarget = undefined;
|
|
200
221
|
clearGitStatusDebounce();
|
|
201
222
|
pendingGitStatusRefresh = undefined;
|
|
202
223
|
runtime.gitStatus = undefined;
|
|
224
|
+
runtime.activeTools.clear();
|
|
225
|
+
runtime.isStreaming = false;
|
|
203
226
|
runtime.duplicateExtensions = [];
|
|
204
227
|
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES;
|
|
205
228
|
ctx.ui.setFooter(undefined);
|
|
@@ -214,41 +237,52 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
214
237
|
refresh();
|
|
215
238
|
});
|
|
216
239
|
|
|
217
|
-
pi.on("agent_start", () => {
|
|
240
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
241
|
+
if (!ownsRuntime(ctx)) return;
|
|
218
242
|
runtime.isStreaming = true;
|
|
219
243
|
refresh();
|
|
220
244
|
});
|
|
221
245
|
|
|
222
246
|
pi.on("agent_end", (_event, ctx) => {
|
|
223
|
-
|
|
247
|
+
if (!ownsRuntime(ctx)) return;
|
|
248
|
+
runtime.activeTools.clear();
|
|
224
249
|
scheduleGitStatusRefreshForContext(ctx);
|
|
225
250
|
refresh();
|
|
226
251
|
});
|
|
227
252
|
|
|
228
|
-
pi.on("
|
|
253
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
254
|
+
if (!ownsRuntime(ctx)) return;
|
|
255
|
+
runtime.isStreaming = false;
|
|
256
|
+
runtime.activeTools.clear();
|
|
257
|
+
refresh();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
261
|
+
if (!ownsRuntime(ctx)) return;
|
|
229
262
|
runtime.turnCount += 1;
|
|
230
263
|
runtime.isStreaming = true;
|
|
231
264
|
refresh();
|
|
232
265
|
});
|
|
233
266
|
|
|
234
267
|
pi.on("turn_end", (_event, ctx) => {
|
|
268
|
+
if (!ownsRuntime(ctx)) return;
|
|
235
269
|
scheduleGitStatusRefreshForContext(ctx);
|
|
236
270
|
refresh();
|
|
237
271
|
});
|
|
238
272
|
|
|
239
|
-
pi.on("tool_execution_start", (event) => {
|
|
273
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
274
|
+
if (!ownsRuntime(ctx)) return;
|
|
240
275
|
const currentCount = runtime.activeTools.get(event.toolName) ?? 0;
|
|
241
276
|
runtime.activeTools.set(event.toolName, currentCount + 1);
|
|
242
|
-
runtime.lastTool = event.toolName;
|
|
243
277
|
refresh();
|
|
244
278
|
});
|
|
245
279
|
|
|
246
280
|
pi.on("tool_execution_end", (event, ctx) => {
|
|
281
|
+
if (!ownsRuntime(ctx)) return;
|
|
247
282
|
const currentCount = runtime.activeTools.get(event.toolName) ?? 0;
|
|
248
283
|
if (currentCount <= 1) runtime.activeTools.delete(event.toolName);
|
|
249
284
|
else runtime.activeTools.set(event.toolName, currentCount - 1);
|
|
250
285
|
|
|
251
|
-
runtime.lastCompletedTool = event.toolName;
|
|
252
286
|
scheduleGitStatusRefreshForContext(ctx);
|
|
253
287
|
refresh();
|
|
254
288
|
});
|
package/src/types.ts
CHANGED
|
@@ -30,21 +30,32 @@ export const PALETTE_NAMES = [
|
|
|
30
30
|
] as const;
|
|
31
31
|
export type PaletteName = (typeof PALETTE_NAMES)[number];
|
|
32
32
|
|
|
33
|
+
export const PALETTE_PRESET_NAMES = [...PALETTE_NAMES, "custom"] as const;
|
|
34
|
+
export type PalettePreset = (typeof PALETTE_PRESET_NAMES)[number];
|
|
35
|
+
|
|
33
36
|
export const DENSITIES = ["compact", "cozy"] as const;
|
|
34
37
|
export type Density = (typeof DENSITIES)[number];
|
|
35
38
|
|
|
36
39
|
export const SEPARATOR_NAMES = ["none", "dot", "bar", "powerline", "round"] as const;
|
|
37
40
|
export type SeparatorName = (typeof SEPARATOR_NAMES)[number];
|
|
38
41
|
|
|
39
|
-
export type
|
|
42
|
+
export type PowerlineBlockName = "header" | "directory" | "git" | "runtime" | "meter";
|
|
40
43
|
|
|
41
44
|
export interface SegmentTextConfig {
|
|
42
45
|
prefix: string;
|
|
43
46
|
suffix: string;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
export interface SegmentPaletteColor {
|
|
50
|
+
fg?: string;
|
|
51
|
+
bg?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SegmentPalette = Partial<Record<SegmentName, SegmentPaletteColor>>;
|
|
55
|
+
|
|
46
56
|
export interface StatuslineConfig {
|
|
47
|
-
|
|
57
|
+
palettePreset: PalettePreset;
|
|
58
|
+
palette: SegmentPalette;
|
|
48
59
|
density: Density;
|
|
49
60
|
separator: SeparatorName;
|
|
50
61
|
segments: ConfigSegmentName[];
|
|
@@ -56,7 +67,7 @@ export interface RenderSegment {
|
|
|
56
67
|
name: SegmentName;
|
|
57
68
|
text: string;
|
|
58
69
|
color: ThemeColor;
|
|
59
|
-
block:
|
|
70
|
+
block: PowerlineBlockName;
|
|
60
71
|
emphasis?: boolean;
|
|
61
72
|
}
|
|
62
73
|
|
package/src/tokyo-night.ts
DELETED
|
@@ -1,180 +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 {
|
|
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
|
-
}
|