@narumitw/pi-statusline 0.25.0 → 0.31.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/src/settings.ts CHANGED
@@ -10,51 +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
- export const DEFAULT_EXTENSION_STATUS_ICONS: Record<string, string> = {
28
- "chrome-devtools": "🌐",
29
- "codex-usage": "📊",
30
- usage: "📊",
32
+ const DEFAULT_DOCUMENT_EXTENSION_STATUS_ICONS: Record<string, string> = {
33
+ accounts: "👤",
31
34
  caffeinate: "💊",
35
+ "chrome-devtools": "🌐",
32
36
  firecrawl: "🔥",
33
37
  "github-pr": "🔎",
34
38
  goal: "🎯",
39
+ "google-genai": "✨",
35
40
  lsp: "🧰",
36
41
  "plan-mode": "📝",
37
- pisync: "🔄",
42
+ retry: "🔁",
38
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: "🔄",
39
52
  "unknown-error-retry": "🔁",
40
53
  };
41
54
 
42
- const DEFAULT_SEGMENTS: SegmentName[] = [
43
- "brand",
44
- "provider",
45
- "model",
46
- "thinking",
47
- "cwd",
48
- "branch",
49
- "tools",
50
- "context",
51
- "tokens",
52
- "cost",
53
- "time",
54
- ];
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];
55
61
 
56
62
  export const DEFAULT_STATUSLINE_CONFIG: StatuslineConfig = {
57
- palette: "tokyo-night",
63
+ palettePreset: "tokyo-night",
64
+ palette: segmentPaletteForPreset("tokyo-night"),
58
65
  density: "compact",
59
66
  separator: "none",
60
67
  segments: DEFAULT_SEGMENTS,
@@ -75,7 +82,20 @@ export const DEFAULT_STATUSLINE_CONFIG: StatuslineConfig = {
75
82
  extensionStatusIcons: DEFAULT_EXTENSION_STATUS_ICONS,
76
83
  };
77
84
 
78
- export const DEFAULT_STATUSLINE_DOCUMENT = `${JSON.stringify(DEFAULT_STATUSLINE_CONFIG, null, "\t")}\n`;
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`;
79
99
 
80
100
  export interface StatuslineConfigDiagnostic {
81
101
  severity: "warning" | "error";
@@ -128,8 +148,8 @@ export function normalizeStatuslineConfig(value: unknown): {
128
148
  diagnostics: [invalidDiagnostic("", "Settings must contain a JSON object", "error")],
129
149
  };
130
150
  }
131
-
132
151
  const knownRoot = new Set([
152
+ "palettePreset",
133
153
  "palette",
134
154
  "density",
135
155
  "separator",
@@ -141,7 +161,11 @@ export function normalizeStatuslineConfig(value: unknown): {
141
161
  if (!knownRoot.has(key)) diagnostics.push(unknownDiagnostic(key));
142
162
  }
143
163
 
144
- normalizeEnum(value, "palette", PALETTE_NAMES, config, diagnostics);
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
+ }
145
169
  normalizeEnum(value, "density", DENSITIES, config, diagnostics);
146
170
  normalizeEnum(value, "separator", SEPARATOR_NAMES, config, diagnostics);
147
171
 
@@ -238,6 +262,28 @@ export function normalizeStatuslineConfig(value: unknown): {
238
262
  writable: true,
239
263
  });
240
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
+ }
241
287
  }
242
288
  }
243
289
 
@@ -413,8 +459,63 @@ export function normalizeStatuslineSettings(value: unknown): StatuslineConfig {
413
459
  return normalizeStatuslineConfig(value).config;
414
460
  }
415
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
+
416
517
  function normalizeEnum<
417
- K extends "palette" | "density" | "separator",
518
+ K extends "palettePreset" | "density" | "separator",
418
519
  T extends StatuslineConfig[K],
419
520
  >(
420
521
  value: Record<string, unknown>,
@@ -434,9 +535,16 @@ function normalizeEnum<
434
535
  config[field] = candidate as StatuslineConfig[K];
435
536
  }
436
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
+
437
544
  function cloneConfig(config: StatuslineConfig): StatuslineConfig {
438
545
  return {
439
546
  ...config,
547
+ palette: cloneSegmentPalette(config.palette),
440
548
  segments: [...config.segments],
441
549
  segmentText: Object.fromEntries(
442
550
  SEGMENT_NAMES.map((name) => [name, { ...config.segmentText[name] }]),
@@ -473,6 +581,10 @@ function isConfigSegmentName(value: string): value is ConfigSegmentName {
473
581
  return value === LINE_BREAK_SEGMENT_NAME || isSegmentName(value);
474
582
  }
475
583
 
584
+ function isPaletteName(value: StatuslineConfig["palettePreset"]): value is PaletteName {
585
+ return (PALETTE_NAMES as readonly StatuslineConfig["palettePreset"][]).includes(value);
586
+ }
587
+
476
588
  function isSegmentName(value: string): value is SegmentName {
477
589
  return (SEGMENT_NAMES as readonly string[]).includes(value);
478
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 = loaded.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
- runtime.isStreaming = false;
247
+ if (!ownsRuntime(ctx)) return;
248
+ runtime.activeTools.clear();
224
249
  scheduleGitStatusRefreshForContext(ctx);
225
250
  refresh();
226
251
  });
227
252
 
228
- pi.on("turn_start", () => {
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 TokyoNightBlockName = "header" | "directory" | "git" | "runtime" | "meter";
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
- palette: PaletteName;
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: TokyoNightBlockName;
70
+ block: PowerlineBlockName;
60
71
  emphasis?: boolean;
61
72
  }
62
73
 
@@ -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
- }