@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@underactive/pi-topping-statusline",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "oh-my-pi's powerline statusline as a pi extension with some customizations",
5
5
  "keywords": [
6
6
  "pi-package",
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> = Object.create(null) as 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 = Object.create(null) as Record<string, unknown>;
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 = Object.create(null) as Record<string, unknown>;
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 leftParts: string[] = [];
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
- leftParts.push(rendered.content);
74
+ left.push(rendered.content);
75
75
  leftSegIds.push(segId);
76
76
  }
77
77
  }
78
78
 
79
- const rightParts: string[] = [];
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
- rightParts.push(rendered.content);
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 fixedLeftLen = 5;
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 - fixedLeftLen - labelWidth - 3);
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 fixedLeftLen = 5; // " " + marker(1) + " " + margin
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 fixedLeftLen = 8;
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 fixedLeftLen = 8; // " " + marker(1) + " " + "[" + box(1) + "]" + " "
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
@@ -94,6 +94,7 @@ function loadSettings(): StatusLineSettings {
94
94
  if (typeof raw.rainbowBorder === "boolean") settings.rainbowBorder = raw.rainbowBorder;
95
95
  return settings;
96
96
  } catch {
97
+ // Missing or corrupt settings.json falls back to defaults.
97
98
  return {};
98
99
  }
99
100
  }
@@ -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) return `${Math.round(n / 1_000)}K`;
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) return `${Math.round(n / 1_000_000)}M`;
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 hslToHex(targetHue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
150
+ return rgbToHex(hslToRgb(targetHue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS));
164
151
  }