@narumitw/pi-statusline 0.49.3 → 0.49.5

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.
@@ -0,0 +1,170 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
+ import { ansiStyle } from "./ansi.js";
4
+ import { resolvePreset } from "./presets/index.js";
5
+ import type { BlockColors, PowerlinePreset } from "./presets/types.js";
6
+ import {
7
+ LINE_BREAK_SEGMENT_NAME,
8
+ type PalettePreset,
9
+ type PowerlineBlockName,
10
+ type RenderItem,
11
+ type RenderSegment,
12
+ type SegmentPalette,
13
+ type SeparatorName,
14
+ type StatuslineConfig,
15
+ } from "./types.js";
16
+
17
+ interface PowerlineBlock {
18
+ baseBlock: PowerlineBlockName;
19
+ colors: BlockColors;
20
+ segments: RenderSegment[];
21
+ }
22
+
23
+ export function renderPowerlineStatusline(
24
+ width: number,
25
+ items: RenderItem[],
26
+ config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
27
+ ): string {
28
+ if (items.length === 0 || width <= 0) return "";
29
+ return splitLines(items)
30
+ .map((segments) => fitPowerlineSegments(segments, width, config))
31
+ .join("\n");
32
+ }
33
+
34
+ function splitLines(items: RenderItem[]): RenderSegment[][] {
35
+ const lines: RenderSegment[][] = [[]];
36
+ for (const item of items) {
37
+ if (item.name === LINE_BREAK_SEGMENT_NAME) lines.push([]);
38
+ else lines.at(-1)?.push(item);
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ const SEGMENT_RETENTION_PRIORITY: Readonly<Record<RenderSegment["name"], number>> = {
44
+ context: 120,
45
+ model: 110,
46
+ branch: 100,
47
+ tools: 90,
48
+ cwd: 80,
49
+ thinking: 70,
50
+ cost: 60,
51
+ provider: 50,
52
+ cache: 45,
53
+ tokens: 40,
54
+ time: 30,
55
+ turn: 20,
56
+ brand: 10,
57
+ };
58
+
59
+ function fitPowerlineSegments(
60
+ segments: readonly RenderSegment[],
61
+ width: number,
62
+ config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
63
+ ): string {
64
+ if (segments.length === 0) return "";
65
+ const fitted = [...segments];
66
+ while (fitted.length > 1) {
67
+ const rendered = joinPowerlineSegments(fitted, config);
68
+ if (visibleWidth(rendered) <= width) return rendered;
69
+ let removalIndex = 0;
70
+ for (let index = 1; index < fitted.length; index += 1) {
71
+ const candidate = fitted[index];
72
+ const current = fitted[removalIndex];
73
+ if (
74
+ candidate &&
75
+ current &&
76
+ SEGMENT_RETENTION_PRIORITY[candidate.name] < SEGMENT_RETENTION_PRIORITY[current.name]
77
+ ) {
78
+ removalIndex = index;
79
+ }
80
+ }
81
+ fitted.splice(removalIndex, 1);
82
+ }
83
+ const rendered = joinPowerlineSegments(fitted, config);
84
+ return visibleWidth(rendered) <= width ? rendered : "";
85
+ }
86
+
87
+ export function powerlineExtensionSeparator(
88
+ _theme: Theme,
89
+ palettePreset: PalettePreset = "tokyo-night",
90
+ ): string {
91
+ return ansiStyle(" • ", { fg: resolvePreset(palettePreset).extensionSeparator });
92
+ }
93
+
94
+ function joinPowerlineSegments(
95
+ segments: RenderSegment[],
96
+ config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
97
+ ): string {
98
+ const preset = resolvePreset(config.palettePreset);
99
+ const blocks = contiguousBlocks(segments, preset, config.palettePreset, config.palette);
100
+ let line = ansiStyle("░▒▓", { fg: preset.lead });
101
+
102
+ for (const [index, block] of blocks.entries()) {
103
+ const previous = index === 0 ? undefined : blocks[index - 1]?.colors;
104
+ if (previous) line += ansiStyle("", { fg: previous.bg, bg: block.colors.bg });
105
+ line += ansiStyle(formatBlockText(block, config), block.colors);
106
+ }
107
+
108
+ const lastBlock = blocks.at(-1);
109
+ if (lastBlock) line += ansiStyle("", { fg: lastBlock.colors.bg });
110
+ return line;
111
+ }
112
+
113
+ function contiguousBlocks(
114
+ segments: RenderSegment[],
115
+ preset: PowerlinePreset,
116
+ palettePreset: PalettePreset,
117
+ configuredPalette: SegmentPalette,
118
+ ): PowerlineBlock[] {
119
+ const blocks: PowerlineBlock[] = [];
120
+ const usesConfiguredColors = palettePreset === "custom";
121
+ for (const segment of segments) {
122
+ const colors = usesConfiguredColors
123
+ ? (configuredPalette[segment.name] ?? {})
124
+ : preset.blocks[segment.block];
125
+ const previous = blocks.at(-1);
126
+ const matchesPrevious =
127
+ previous !== undefined &&
128
+ (usesConfiguredColors
129
+ ? colorsEqual(previous.colors, colors)
130
+ : previous.baseBlock === segment.block);
131
+ if (matchesPrevious) previous.segments.push(segment);
132
+ else blocks.push({ baseBlock: segment.block, colors, segments: [segment] });
133
+ }
134
+ return blocks;
135
+ }
136
+
137
+ function colorsEqual(left: BlockColors, right: BlockColors): boolean {
138
+ return left.fg === right.fg && left.bg === right.bg;
139
+ }
140
+
141
+ function formatBlockText(
142
+ block: PowerlineBlock,
143
+ config: Pick<StatuslineConfig, "density" | "separator">,
144
+ ): string {
145
+ const texts = block.segments.map(formatSegmentText);
146
+ const separator = separatorText(config.separator, config.density === "cozy");
147
+ const leading = config.density === "cozy" ? " " : " ";
148
+ const trailing = config.density === "cozy" ? " " : "";
149
+ return `${leading}${texts.join(separator)}${trailing}`;
150
+ }
151
+
152
+ function formatSegmentText(segment: RenderSegment): string {
153
+ return segment.emphasis ? `\u001b[1m${segment.text}\u001b[22m` : segment.text;
154
+ }
155
+
156
+ function separatorText(separator: SeparatorName, cozy: boolean): string {
157
+ const padding = cozy ? " " : " ";
158
+ switch (separator) {
159
+ case "dot":
160
+ return `${padding}•${padding}`;
161
+ case "bar":
162
+ return `${padding}│${padding}`;
163
+ case "powerline":
164
+ return `${padding}${padding}`;
165
+ case "round":
166
+ return `${padding}❯${padding}`;
167
+ case "none":
168
+ return padding;
169
+ }
170
+ }
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const CANDY_PRESET = createRampPreset([
4
+ "#f5c2e7",
5
+ "#cba6f7",
6
+ "#89b4fa",
7
+ "#745f9a",
8
+ "#403a5c",
9
+ ]);
@@ -0,0 +1,41 @@
1
+ import type { PowerlineBlockName } from "../types.js";
2
+ import type { PowerlinePreset } from "./types.js";
3
+
4
+ const BLOCK_NAMES: PowerlineBlockName[] = ["header", "directory", "git", "runtime", "meter"];
5
+
6
+ type PaletteRamp = readonly [string, string, string, string, string];
7
+
8
+ export function createRampPreset(backgrounds: PaletteRamp): PowerlinePreset {
9
+ return {
10
+ lead: backgrounds[0],
11
+ blocks: Object.fromEntries(
12
+ BLOCK_NAMES.map((block, index) => {
13
+ const background = backgrounds[index];
14
+ return [block, { fg: contrastColor(background), bg: background }];
15
+ }),
16
+ ) as Record<PowerlineBlockName, { fg: string; bg: string }>,
17
+ extensionSeparator: backgrounds[2],
18
+ };
19
+ }
20
+
21
+ function contrastColor(hex: string): string {
22
+ const background = relativeLuminance(hex);
23
+ const dark = relativeLuminance("#090c0c");
24
+ const light = relativeLuminance("#f0f0f0");
25
+ return contrastRatio(background, dark) >= contrastRatio(background, light)
26
+ ? "#090c0c"
27
+ : "#f0f0f0";
28
+ }
29
+
30
+ function contrastRatio(left: number, right: number): number {
31
+ return (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);
32
+ }
33
+
34
+ function relativeLuminance(hex: string): number {
35
+ const normalized = hex.slice(1);
36
+ const channels = [0, 2, 4].map((offset) => {
37
+ const value = Number.parseInt(normalized.slice(offset, offset + 2), 16) / 255;
38
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
39
+ });
40
+ return 0.2126 * (channels[0] ?? 0) + 0.7152 * (channels[1] ?? 0) + 0.0722 * (channels[2] ?? 0);
41
+ }
@@ -0,0 +1,11 @@
1
+ import type { PowerlinePreset } from "./types.js";
2
+
3
+ export const CUSTOM_PRESET: PowerlinePreset = {
4
+ blocks: {
5
+ header: {},
6
+ directory: {},
7
+ git: {},
8
+ runtime: {},
9
+ meter: {},
10
+ },
11
+ };
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const FOREST_PRESET = createRampPreset([
4
+ "#a7c080",
5
+ "#83c092",
6
+ "#5f9f75",
7
+ "#3f6f55",
8
+ "#293f35",
9
+ ]);
@@ -0,0 +1,55 @@
1
+ import {
2
+ type PaletteName,
3
+ type PalettePreset,
4
+ type PowerlineBlockName,
5
+ SEGMENT_NAMES,
6
+ type SegmentName,
7
+ type SegmentPalette,
8
+ } from "../types.js";
9
+ import { CANDY_PRESET } from "./candy.js";
10
+ import { CUSTOM_PRESET } from "./custom.js";
11
+ import { FOREST_PRESET } from "./forest.js";
12
+ import { MONO_PRESET } from "./mono.js";
13
+ import { NEON_PRESET } from "./neon.js";
14
+ import { OCEAN_PRESET } from "./ocean.js";
15
+ import { SUNSET_PRESET } from "./sunset.js";
16
+ import { TOKYO_NIGHT_PRESET } from "./tokyo-night.js";
17
+ import type { PowerlinePreset } from "./types.js";
18
+
19
+ const PRESETS = {
20
+ "tokyo-night": TOKYO_NIGHT_PRESET,
21
+ ocean: OCEAN_PRESET,
22
+ sunset: SUNSET_PRESET,
23
+ forest: FOREST_PRESET,
24
+ candy: CANDY_PRESET,
25
+ neon: NEON_PRESET,
26
+ mono: MONO_PRESET,
27
+ custom: CUSTOM_PRESET,
28
+ } satisfies Record<PalettePreset, PowerlinePreset>;
29
+
30
+ const SEGMENT_BLOCKS: Record<SegmentName, PowerlineBlockName> = {
31
+ brand: "header",
32
+ provider: "header",
33
+ model: "header",
34
+ thinking: "header",
35
+ cwd: "directory",
36
+ branch: "git",
37
+ tools: "runtime",
38
+ context: "runtime",
39
+ tokens: "runtime",
40
+ cache: "runtime",
41
+ cost: "meter",
42
+ time: "meter",
43
+ turn: "meter",
44
+ };
45
+
46
+ export function resolvePreset(preset: PalettePreset): PowerlinePreset {
47
+ return PRESETS[preset];
48
+ }
49
+
50
+ export function segmentPaletteForPreset(preset: PaletteName): SegmentPalette {
51
+ const blocks = PRESETS[preset].blocks;
52
+ return Object.fromEntries(
53
+ SEGMENT_NAMES.map((name) => [name, { ...blocks[SEGMENT_BLOCKS[name]] }]),
54
+ ) as SegmentPalette;
55
+ }
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const MONO_PRESET = createRampPreset([
4
+ "#d4d4d4",
5
+ "#a3a3a3",
6
+ "#686868",
7
+ "#404040",
8
+ "#262626",
9
+ ]);
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const NEON_PRESET = createRampPreset([
4
+ "#39ff14",
5
+ "#00f5ff",
6
+ "#ff4fd8",
7
+ "#7a2cf3",
8
+ "#29134f",
9
+ ]);
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const OCEAN_PRESET = createRampPreset([
4
+ "#7dcfff",
5
+ "#4f9fba",
6
+ "#2d6f88",
7
+ "#23475b",
8
+ "#182b3a",
9
+ ]);
@@ -0,0 +1,9 @@
1
+ import { createRampPreset } from "./create-ramp.js";
2
+
3
+ export const SUNSET_PRESET = createRampPreset([
4
+ "#ffcf70",
5
+ "#f59e6f",
6
+ "#dc718a",
7
+ "#8f5b78",
8
+ "#493447",
9
+ ]);
@@ -0,0 +1,13 @@
1
+ import type { PowerlinePreset } from "./types.js";
2
+
3
+ export const TOKYO_NIGHT_PRESET: PowerlinePreset = {
4
+ lead: "#a3aed2",
5
+ blocks: {
6
+ header: { fg: "#090c0c", bg: "#a3aed2" },
7
+ directory: { fg: "#e3e5e5", bg: "#769ff0" },
8
+ git: { fg: "#769ff0", bg: "#394260" },
9
+ runtime: { fg: "#769ff0", bg: "#212736" },
10
+ meter: { fg: "#a0a9cb", bg: "#1d2230" },
11
+ },
12
+ extensionSeparator: "#394260",
13
+ };
@@ -0,0 +1,12 @@
1
+ import type { PowerlineBlockName } from "../types.js";
2
+
3
+ export interface BlockColors {
4
+ fg?: string;
5
+ bg?: string;
6
+ }
7
+
8
+ export interface PowerlinePreset {
9
+ lead?: string;
10
+ blocks: Record<PowerlineBlockName, BlockColors>;
11
+ extensionSeparator?: string;
12
+ }