@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/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
- export const DEFAULT_EXTENSION_STATUS_ICONS: Record<string, string> = {
28
- "chrome-devtools": "🌐",
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
- pisync: "🔄",
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 DEFAULT_SEGMENTS: SegmentName[] = [
42
- "brand",
43
- "provider",
44
- "model",
45
- "thinking",
46
- "cwd",
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
- palette: "tokyo-night",
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
- 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`;
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
- 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
+ }
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 "palette" | "density" | "separator",
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 = 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
- }