@underactive/pi-topping-statusline 0.1.1 → 0.1.2
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/package.json +1 -1
- package/src/context.ts +6 -3
- package/src/layout.ts +4 -7
- package/src/menu.ts +9 -9
- package/src/rainbow.ts +2 -2
- package/src/settings.ts +1 -0
- package/src/tests/feeds.test.ts +0 -2
- package/src/theme.ts +0 -8
- package/src/utils.ts +10 -23
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -17,6 +17,9 @@ const EXEC_TIMEOUT_MS = 2_000;
|
|
|
17
17
|
/** getEntries() filters and copies the whole session, so rescans are rate-limited. */
|
|
18
18
|
const FEED_TTL_MS = 2_000;
|
|
19
19
|
|
|
20
|
+
/** Prototype-less map — feed keys are publisher-supplied customTypes. */
|
|
21
|
+
const emptyFeedData = (): Record<string, unknown> => Object.create(null);
|
|
22
|
+
|
|
20
23
|
interface GitStatusCounts {
|
|
21
24
|
staged: number;
|
|
22
25
|
unstaged: number;
|
|
@@ -85,7 +88,7 @@ export class SegmentContextBuilder {
|
|
|
85
88
|
|
|
86
89
|
#worktree: { projectName: string; worktreeName: string } | null = null;
|
|
87
90
|
|
|
88
|
-
#feedData: Record<string, unknown> =
|
|
91
|
+
#feedData: Record<string, unknown> = emptyFeedData();
|
|
89
92
|
#feedsScannedAt = 0;
|
|
90
93
|
#feedsScanned = "";
|
|
91
94
|
#runStartedAt = 0;
|
|
@@ -113,7 +116,7 @@ export class SegmentContextBuilder {
|
|
|
113
116
|
// session still carries the previous run's entries. Anything predating this
|
|
114
117
|
// attach belongs to that stale run.
|
|
115
118
|
this.#runStartedAt = Date.now();
|
|
116
|
-
this.#feedData =
|
|
119
|
+
this.#feedData = emptyFeedData();
|
|
117
120
|
this.#feedsScannedAt = 0;
|
|
118
121
|
this.#feedsScanned = "";
|
|
119
122
|
if (this.#repoCwd !== ctx.cwd) {
|
|
@@ -265,7 +268,7 @@ export class SegmentContextBuilder {
|
|
|
265
268
|
*/
|
|
266
269
|
#refreshFeeds(customTypes: readonly string[], now: number): void {
|
|
267
270
|
this.#feedsScannedAt = now;
|
|
268
|
-
this.#feedData =
|
|
271
|
+
this.#feedData = emptyFeedData();
|
|
269
272
|
const entries = this.#ctx?.sessionManager?.getEntries();
|
|
270
273
|
if (!entries) return;
|
|
271
274
|
const pending = new Set(customTypes);
|
package/src/layout.ts
CHANGED
|
@@ -66,27 +66,24 @@ export function buildStatusLine(
|
|
|
66
66
|
const fgAnsi = theme.getFgAnsi("text");
|
|
67
67
|
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
68
68
|
|
|
69
|
-
const
|
|
69
|
+
const left: string[] = [];
|
|
70
70
|
const leftSegIds: StatusLineSegmentId[] = [];
|
|
71
71
|
for (const segId of segmentGroups.left) {
|
|
72
72
|
const rendered = renderSegment(segId, ctx);
|
|
73
73
|
if (rendered.visible && rendered.content) {
|
|
74
|
-
|
|
74
|
+
left.push(rendered.content);
|
|
75
75
|
leftSegIds.push(segId);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const
|
|
79
|
+
const right: string[] = [];
|
|
80
80
|
for (const segId of segmentGroups.right) {
|
|
81
81
|
const rendered = renderSegment(segId, ctx);
|
|
82
82
|
if (rendered.visible && rendered.content) {
|
|
83
|
-
|
|
83
|
+
right.push(rendered.content);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const left = [...leftParts];
|
|
88
|
-
const right = [...rightParts];
|
|
89
|
-
|
|
90
87
|
const leftSepWidth = visibleWidth(separatorDef.left);
|
|
91
88
|
const rightSepWidth = visibleWidth(separatorDef.right);
|
|
92
89
|
// Transparent mode drops the round caps (they need a bg fill to bridge);
|
package/src/menu.ts
CHANGED
|
@@ -112,6 +112,10 @@ export interface MenuResult<T> {
|
|
|
112
112
|
const DEFAULT_HINTS = ["\u2191\u2193 move", "\u2423 toggle", "\u23ce apply", "esc cancel"];
|
|
113
113
|
const EDIT_HINTS = ["type to edit", "\u232b delete", "\u23ce commit", "esc discard"];
|
|
114
114
|
const OVERLAY_WIDTH = "90%";
|
|
115
|
+
/** Width of the " <marker> " prefix shared by rows without a checkbox. */
|
|
116
|
+
const PLAIN_ROW_PREFIX_LEN = 5;
|
|
117
|
+
/** Width of the " <marker> [■] " prefix shared by checkbox rows. */
|
|
118
|
+
const CHECKBOX_ROW_PREFIX_LEN = 8;
|
|
115
119
|
|
|
116
120
|
interface FlatItem {
|
|
117
121
|
id: string;
|
|
@@ -607,10 +611,9 @@ export class MenuComponent implements Component {
|
|
|
607
611
|
if (item.text) {
|
|
608
612
|
const editing = this.editing?.id === item.id ? this.editing : undefined;
|
|
609
613
|
const raw = editing ? editing.buffer : String(value ?? "");
|
|
610
|
-
const
|
|
611
|
-
const labelWidth = Math.min(16, Math.max(0, innerWidth - fixedLeftLen - 4));
|
|
614
|
+
const labelWidth = Math.min(16, Math.max(0, innerWidth - PLAIN_ROW_PREFIX_LEN - 4));
|
|
612
615
|
const label = truncateToWidth(item.label, labelWidth).padEnd(labelWidth);
|
|
613
|
-
const fieldWidth = Math.max(0, innerWidth -
|
|
616
|
+
const fieldWidth = Math.max(0, innerWidth - PLAIN_ROW_PREFIX_LEN - labelWidth - 3);
|
|
614
617
|
// Keep the caret in view by showing the buffer's tail once it overflows.
|
|
615
618
|
const shownRaw =
|
|
616
619
|
editing && raw.length > fieldWidth - 1 ? raw.slice(raw.length - (fieldWidth - 1)) : raw;
|
|
@@ -630,8 +633,7 @@ export class MenuComponent implements Component {
|
|
|
630
633
|
const stateWord = `‹ ${value} ›`;
|
|
631
634
|
// Ungated cycle rows carry no checkbox; gated ones keep it for the space toggle.
|
|
632
635
|
if (!item.cycleEnabledBy) {
|
|
633
|
-
const
|
|
634
|
-
const maxLabelLen = Math.max(0, innerWidth - fixedLeftLen - visibleWidth(stateWord) - 1);
|
|
636
|
+
const maxLabelLen = Math.max(0, innerWidth - PLAIN_ROW_PREFIX_LEN - visibleWidth(stateWord) - 1);
|
|
635
637
|
const label = item.label.length > maxLabelLen ? truncateToWidth(item.label, maxLabelLen) : item.label;
|
|
636
638
|
const leftPlain = ` ${marker} ${label}`;
|
|
637
639
|
const gap = Math.max(1, innerWidth - visibleWidth(leftPlain) - visibleWidth(stateWord) - 2);
|
|
@@ -639,8 +641,7 @@ export class MenuComponent implements Component {
|
|
|
639
641
|
return this.wrap("\u2551", content, "\u2551");
|
|
640
642
|
}
|
|
641
643
|
const enabled = this.values[item.cycleEnabledBy] as boolean;
|
|
642
|
-
const
|
|
643
|
-
const maxLabelLen = Math.max(0, innerWidth - fixedLeftLen - visibleWidth(stateWord) - 1);
|
|
644
|
+
const maxLabelLen = Math.max(0, innerWidth - CHECKBOX_ROW_PREFIX_LEN - visibleWidth(stateWord) - 1);
|
|
644
645
|
const label = item.label.length > maxLabelLen ? truncateToWidth(item.label, maxLabelLen) : item.label;
|
|
645
646
|
const leftPlain = ` ${marker} [${enabled ? "■" : " "}] ${label}`;
|
|
646
647
|
const gap = Math.max(1, innerWidth - visibleWidth(leftPlain) - visibleWidth(stateWord) - 2);
|
|
@@ -652,8 +653,7 @@ export class MenuComponent implements Component {
|
|
|
652
653
|
const box = enabled ? "\u25a0" : " ";
|
|
653
654
|
const stateWord = enabled ? "ON" : "OFF";
|
|
654
655
|
const rightPlain = `${stateWord} `;
|
|
655
|
-
const
|
|
656
|
-
const maxLabelLen = Math.max(0, innerWidth - fixedLeftLen - rightPlain.length - 1);
|
|
656
|
+
const maxLabelLen = Math.max(0, innerWidth - CHECKBOX_ROW_PREFIX_LEN - rightPlain.length - 1);
|
|
657
657
|
const label = item.label.length > maxLabelLen ? truncateToWidth(item.label, maxLabelLen) : item.label;
|
|
658
658
|
const leftPlain = ` ${marker} [${box}] ${label}`;
|
|
659
659
|
const gap = Math.max(1, innerWidth - visibleWidth(leftPlain) - visibleWidth(rightPlain));
|
package/src/rainbow.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function perimeterPosition(row: number, col: number, width: number, botto
|
|
|
46
46
|
return 2 * width + 2 * bottomIdx - 2 - row; // col === 0
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
49
|
+
export function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
50
50
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
51
51
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
52
52
|
const m = l - c / 2;
|
|
@@ -60,7 +60,7 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
|
60
60
|
return [Math.round((rgb[0] + m) * 255), Math.round((rgb[1] + m) * 255), Math.round((rgb[2] + m) * 255)];
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function rgbToHex([r, g, b]: [number, number, number]): string {
|
|
63
|
+
export function rgbToHex([r, g, b]: [number, number, number]): string {
|
|
64
64
|
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`;
|
|
65
65
|
}
|
|
66
66
|
|
package/src/settings.ts
CHANGED
package/src/tests/feeds.test.ts
CHANGED
|
@@ -15,7 +15,6 @@ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-wor
|
|
|
15
15
|
import { SegmentContextBuilder } from "../context.ts";
|
|
16
16
|
import { SEGMENTS } from "../segments.ts";
|
|
17
17
|
import { DEFAULT_FEEDS, DEFAULT_SEGMENTS, resolveEffectiveSettings, sanitizeFeeds } from "../settings.ts";
|
|
18
|
-
import { theme } from "../theme.ts";
|
|
19
18
|
import type { SegmentContext, StatusLineFeed } from "../types.ts";
|
|
20
19
|
|
|
21
20
|
const SAVINGS = "pi-prompt-cache/savings";
|
|
@@ -364,5 +363,4 @@ test("the default subscription matches what pi-prompt-cache publishes", () => {
|
|
|
364
363
|
assert.equal(DEFAULT_FEEDS[0]?.customType, SAVINGS);
|
|
365
364
|
assert.equal(DEFAULT_FEEDS[0]?.field, "savedUsd");
|
|
366
365
|
assert.equal(DEFAULT_FEEDS[0]?.format, "currency");
|
|
367
|
-
void theme;
|
|
368
366
|
});
|
package/src/theme.ts
CHANGED
|
@@ -30,8 +30,6 @@ export interface StatusSymbols {
|
|
|
30
30
|
powerlineThinRight: string;
|
|
31
31
|
halfCircleLeft: string;
|
|
32
32
|
halfCircleRight: string;
|
|
33
|
-
block: string;
|
|
34
|
-
space: string;
|
|
35
33
|
asciiLeft: string;
|
|
36
34
|
asciiRight: string;
|
|
37
35
|
dot: string;
|
|
@@ -65,8 +63,6 @@ const UNICODE_SYMBOLS: StatusSymbols = {
|
|
|
65
63
|
powerlineThinRight: "<",
|
|
66
64
|
halfCircleLeft: "",
|
|
67
65
|
halfCircleRight: "",
|
|
68
|
-
block: "▌",
|
|
69
|
-
space: " ",
|
|
70
66
|
asciiLeft: ">",
|
|
71
67
|
asciiRight: "<",
|
|
72
68
|
dot: " · ",
|
|
@@ -100,8 +96,6 @@ const NERD_SYMBOLS: StatusSymbols = {
|
|
|
100
96
|
powerlineThinRight: "\ue0b3",
|
|
101
97
|
halfCircleLeft: "\ue0b6",
|
|
102
98
|
halfCircleRight: "\ue0b4",
|
|
103
|
-
block: "█",
|
|
104
|
-
space: " ",
|
|
105
99
|
asciiLeft: ">",
|
|
106
100
|
asciiRight: "<",
|
|
107
101
|
dot: " · ",
|
|
@@ -135,8 +129,6 @@ const ASCII_SYMBOLS: StatusSymbols = {
|
|
|
135
129
|
powerlineThinRight: "<",
|
|
136
130
|
halfCircleLeft: "",
|
|
137
131
|
halfCircleRight: "",
|
|
138
|
-
block: "#",
|
|
139
|
-
space: " ",
|
|
140
132
|
asciiLeft: ">",
|
|
141
133
|
asciiRight: "<",
|
|
142
134
|
dot: " - ",
|
package/src/utils.ts
CHANGED
|
@@ -7,13 +7,20 @@
|
|
|
7
7
|
import { stripVTControlCharacters } from "node:util";
|
|
8
8
|
import { sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import { parseHex } from "./theme.js";
|
|
10
|
+
import { hslToRgb, rgbToHex } from "./rainbow.js";
|
|
10
11
|
|
|
11
12
|
export function formatNumber(n: number): string {
|
|
12
13
|
if (n < 1_000) return n.toString();
|
|
13
14
|
if (n < 10_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
14
|
-
if (n < 1_000_000)
|
|
15
|
+
if (n < 1_000_000) {
|
|
16
|
+
const k = Math.round(n / 1_000);
|
|
17
|
+
return k < 1000 ? `${k}K` : `${(n / 1_000_000).toFixed(1)}M`;
|
|
18
|
+
}
|
|
15
19
|
if (n < 10_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
16
|
-
if (n < 1_000_000_000)
|
|
20
|
+
if (n < 1_000_000_000) {
|
|
21
|
+
const m = Math.round(n / 1_000_000);
|
|
22
|
+
return m < 1000 ? `${m}M` : `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
23
|
+
}
|
|
17
24
|
if (n < 10_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
18
25
|
return `${Math.round(n / 1_000_000_000)}B`;
|
|
19
26
|
}
|
|
@@ -122,26 +129,6 @@ function hexToHue(hex: string): number | undefined {
|
|
|
122
129
|
return (h + 360) % 360;
|
|
123
130
|
}
|
|
124
131
|
|
|
125
|
-
function hslToHex(h: number, s: number, l: number): string {
|
|
126
|
-
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
127
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
128
|
-
const m = l - c / 2;
|
|
129
|
-
let r = 0;
|
|
130
|
-
let g = 0;
|
|
131
|
-
let b = 0;
|
|
132
|
-
if (h < 60) [r, g, b] = [c, x, 0];
|
|
133
|
-
else if (h < 120) [r, g, b] = [x, c, 0];
|
|
134
|
-
else if (h < 180) [r, g, b] = [0, c, x];
|
|
135
|
-
else if (h < 240) [r, g, b] = [0, x, c];
|
|
136
|
-
else if (h < 300) [r, g, b] = [x, 0, c];
|
|
137
|
-
else [r, g, b] = [c, 0, x];
|
|
138
|
-
const toHex = (v: number) =>
|
|
139
|
-
Math.round((v + m) * 255)
|
|
140
|
-
.toString(16)
|
|
141
|
-
.padStart(2, "0");
|
|
142
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
132
|
function findSafeHue(target: number, occupied: number[], lo: number, hi: number): number {
|
|
146
133
|
if (occupied.length === 0) return target;
|
|
147
134
|
if (occupied.every(h => hueDistance(target, h) >= MIN_HUE_DISTANCE)) return target;
|
|
@@ -160,5 +147,5 @@ export function getSessionAccentHex(name: string, themeColorHexes: string[]): st
|
|
|
160
147
|
let targetHue = DARK_HUE_START + (nameToHue(name) % range);
|
|
161
148
|
const themeHues = themeColorHexes.map(hexToHue).filter((h): h is number => h !== undefined);
|
|
162
149
|
targetHue = findSafeHue(targetHue, themeHues, DARK_HUE_START, DARK_HUE_END);
|
|
163
|
-
return
|
|
150
|
+
return rgbToHex(hslToRgb(targetHue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS));
|
|
164
151
|
}
|