@underactive/pi-topping-statusline 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -41,6 +41,7 @@ and apply live.
41
41
  ║ Symbols ‹ nerdfont › ║
42
42
  ║ Border style ‹ rounded › ║
43
43
  ║ [■] Rainbow border on max thinking ON ║
44
+ ║ [■] Animate rainbow border ON ║
44
45
  ║ ║
45
46
  ╟─ Top Left Segment Group ───────────────────────────────────────╢
46
47
  ║ [■] Pi symbol ON ║
@@ -65,26 +66,26 @@ and apply live.
65
66
  ║ + add feed ║
66
67
  ║ ║
67
68
  ║ ↑↓ move ←→ cycle ␣ toggle ⏎ apply/edit esc cancel ║
68
- ╚════════════════════════════════════════════════════════[ 1/28 ]╝
69
+ ╚════════════════════════════════════════════════════════[ 1/29 ]╝
69
70
  ```
70
71
 
71
72
  | Section | Settings |
72
73
  | --- | --- |
73
- | Global | Transparent Segments · Separator (`powerline` `powerline-thin` `slash` `pipe` `ascii`) · Symbols (`nerdfont` `unicode` `ascii` — stored in settings.json as `nerd`/`unicode`/`ascii`) · Border style (`rounded` `heavy` `double` `single`) · Rainbow border on max thinking |
74
+ | Global | Transparent Segments · Separator (`powerline` `powerline-thin` `slash` `pipe` `ascii`) · Symbols (`nerdfont` `unicode` `ascii` — stored in settings.json as `nerd`/`unicode`/`ascii`) · Border style (`rounded` `heavy` `double` `single`) · Rainbow border on max thinking · Animate rainbow border |
74
75
  | Top Left Segment Group | Pi symbol · Model · Provider · Thinking level · Path · Git · PR |
75
76
  | Top Right Segment Group | Token rate · Session name |
76
77
  | Bottom Right Segment Group | Feeds · Token rate · Pi stats · Context bar · Context stats |
77
78
  | Bottom Left Segment Group | Scroll hint · Feeds · Token rate |
78
79
  | Feeds | One subscription per row: type · field · prefix · format, plus add/remove |
79
- | Defaults | Transparent on · Separator `powerline-thin` · Symbols `nerdfont` · Border style `rounded` · Rainbow border on |
80
+ | Defaults | Transparent on · Separator `powerline-thin` · Symbols `nerdfont` · Border style `rounded` · Rainbow border on · Animate rainbow border on |
80
81
 
81
82
  With **Rainbow border on max thinking** on (the default), cycling the thinking level to `max`
82
- replaces the border's fixed theme color with an animated rainbow: a full hue cycle distributed
83
- around the box perimeter that slowly flows around the border (~14s per rotation), like Apple
84
- Intelligence's screen border. Any other thinking level keeps the normal theme border color.
85
- The settings preview animates while the toggle is on; the effect repaints the border about
86
- 20 times a second, so switch it off over slow SSH links or in terminals with expensive
87
- redraws.
83
+ replaces the border's fixed theme color with a rainbow: a full hue cycle distributed around the
84
+ box perimeter. **Animate rainbow border** is also on by default, so the hues flow around the
85
+ border (~14s per rotation), like Apple Intelligence's screen border. Turn animation off to keep
86
+ the rainbow at a fixed color phase without its repaint timer; the settings preview uses a stable
87
+ phase too. Any other thinking level keeps the normal theme border color. Disable animation over
88
+ slow SSH links or in terminals with expensive redraws while retaining the rainbow border.
88
89
 
89
90
  ## Segments
90
91
 
@@ -130,9 +131,10 @@ A feed contributes nothing when its publisher is absent, has published nothing t
130
131
  reports a value its format suppresses; the segment disappears entirely once every feed is
131
132
  quiet, so it costs nothing to leave switched on. Publishers typically reset per process run,
132
133
  so entries predating the current session start (`--resume`, `/reload`, `/new`, fork) are
133
- ignored rather than shown as a stale total. The session is re-read at most once every two
134
- seconds, because pi copies the whole entry list on each read while the bar re-renders on
135
- every keystroke.
134
+ ignored rather than shown as a stale total. During steady-state rendering, the session is
135
+ re-read at most once every two seconds, because pi copies the whole entry list on each read
136
+ while the bar re-renders on every keystroke; changing subscriptions or starting a new
137
+ session forces an immediate scan.
136
138
 
137
139
  ### Publishing a feed
138
140
 
package/index.ts CHANGED
@@ -55,17 +55,18 @@ export default function (pi: ExtensionAPI) {
55
55
  rateMonitor.attach(pi);
56
56
  builder.setTokenRateProvider(() => rateMonitor.getDisplay());
57
57
 
58
- // Animated rainbow border while the thinking level is max: a timer steps
59
- // the hue phase and re-renders; renderBoxed paints the box glyphs from it.
58
+ // The rainbow is visible at max thinking independently of whether its hue
59
+ // phase is animated. Stopping the timer preserves the current live phase.
60
60
  const rainbow = new RainbowBorder();
61
61
  let rainbowTimer: ReturnType<typeof setInterval> | undefined;
62
62
  const rainbowActive = (): boolean =>
63
63
  state.effective.rainbowBorder && activeCtx !== undefined && pi.getThinkingLevel() === "max";
64
+ const rainbowAnimationActive = (): boolean => rainbowActive() && state.effective.rainbowAnimation;
64
65
  const syncRainbow = (): void => {
65
- if (rainbowActive()) {
66
+ if (rainbowAnimationActive()) {
66
67
  if (!rainbowTimer) {
67
68
  rainbowTimer = setInterval(() => {
68
- if (!rainbowActive()) {
69
+ if (!rainbowAnimationActive()) {
69
70
  syncRainbow();
70
71
  return;
71
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@underactive/pi-topping-statusline",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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
@@ -191,6 +191,9 @@ export class SegmentContextBuilder {
191
191
  this.#requestRender();
192
192
  } finally {
193
193
  this.#repoResolving = false;
194
+ if (this.#repoCwd && this.#repoCwd !== cwd) {
195
+ void this.#resolveRepo(this.#repoCwd);
196
+ }
194
197
  }
195
198
  }
196
199
 
@@ -278,7 +281,8 @@ export class SegmentContextBuilder {
278
281
  // first: matching on customType alone lets one shadow a real entry.
279
282
  if (entry?.type !== "custom" || !pending.has(entry.customType)) continue;
280
283
  const ts = Date.parse(entry.timestamp);
281
- if (!Number.isFinite(ts) || ts < this.#runStartedAt) break;
284
+ if (!Number.isFinite(ts)) continue;
285
+ if (ts < this.#runStartedAt) break;
282
286
  pending.delete(entry.customType);
283
287
  if (entry.data !== undefined) this.#feedData[entry.customType] = entry.data;
284
288
  }
@@ -307,7 +311,7 @@ export class SegmentContextBuilder {
307
311
  const u = URL.parse(parsed.url);
308
312
  if (
309
313
  u &&
310
- (u.protocol === "https:" || u.protocol === "http:") &&
314
+ u.protocol === "https:" &&
311
315
  u.hostname === "github.com" &&
312
316
  !/[\u0000-\u001f\u007f]/.test(parsed.url)
313
317
  ) {
@@ -135,6 +135,11 @@ function buildSections(settings: StatusLineSettings): MenuSection[] {
135
135
  label: "Rainbow border on max thinking",
136
136
  value: settings.rainbowBorder ?? true,
137
137
  },
138
+ {
139
+ id: "rainbowAnimation",
140
+ label: "Animate rainbow border",
141
+ value: settings.rainbowAnimation ?? true,
142
+ },
138
143
  ],
139
144
  };
140
145
  return [
@@ -164,6 +169,7 @@ function valuesToSettings(values: Record<string, MenuValue>): StatusLineSettings
164
169
  segments,
165
170
  feeds: sanitizeFeeds(feedsFromValues(values)),
166
171
  rainbowBorder: values.rainbowBorder === true,
172
+ rainbowAnimation: values.rainbowAnimation === true,
167
173
  };
168
174
  }
169
175
 
@@ -195,12 +201,10 @@ function cannedFeedData(
195
201
  }
196
202
 
197
203
  class StatusLinePreview {
198
- readonly #state: SettingsState;
199
204
  readonly #builder: SegmentContextBuilder;
200
205
  readonly #uiTheme: Theme;
201
206
 
202
- constructor(state: SettingsState, builder: SegmentContextBuilder, uiTheme: Theme) {
203
- this.#state = state;
207
+ constructor(builder: SegmentContextBuilder, uiTheme: Theme) {
204
208
  this.#builder = builder;
205
209
  this.#uiTheme = uiTheme;
206
210
  }
@@ -217,6 +221,7 @@ class StatusLinePreview {
217
221
  const boxWidth = Math.max(24, innerWidth - 1);
218
222
  const barWidth = boxWidth - 6;
219
223
  const rainbowOn = effective.rainbowBorder;
224
+ const rainbowAnimationOn = rainbowOn && effective.rainbowAnimation;
220
225
 
221
226
  // The theme singleton drives glyph lookups everywhere, so swap the
222
227
  // symbol preset for this synchronous render only; the live bar behind
@@ -255,7 +260,7 @@ class StatusLinePreview {
255
260
  const bottomIdx = 2;
256
261
  const painters = makeBoxPainters({
257
262
  rainbowOn,
258
- rainbow: new RainbowBorder((elapsedMs * 360) / RAINBOW_CYCLE_MS),
263
+ rainbow: new RainbowBorder(rainbowAnimationOn ? (elapsedMs * 360) / RAINBOW_CYCLE_MS : 0),
259
264
  box,
260
265
  width: boxWidth,
261
266
  bottomIdx,
@@ -291,9 +296,8 @@ class StatusLinePreview {
291
296
  painters.paint(1, boxWidth - 1, box.vertical),
292
297
  renderBoxRow(painters, bottom, bottomIdx, boxWidth, box.bottomLeft, box.bottomRight),
293
298
  ],
294
- // Keep the preview animated only while the rainbow is enabled;
295
- // a static preview refreshes once and stops.
296
- nextRefreshInMs: rainbowOn ? RAINBOW_FRAME_MS : undefined,
299
+ // A static rainbow stays at phase zero and needs no repaint timer.
300
+ nextRefreshInMs: rainbowAnimationOn ? RAINBOW_FRAME_MS : undefined,
297
301
  };
298
302
  } finally {
299
303
  theme.setSymbolPreset(applied);
@@ -308,13 +312,13 @@ export function registerSettingsCommand(
308
312
  onChange: () => void,
309
313
  ): void {
310
314
  pi.registerCommand("topping-statusline-settings", {
311
- description: "Configure transparent segments, statusline segments, separator, symbols, border style, feeds, and rainbow border",
315
+ description: "Configure transparent segments, statusline segments, separator, symbols, border style, feeds, and rainbow border animation",
312
316
  handler: async (_args, ctx: ExtensionCommandContext) => {
313
317
  if (ctx.mode !== "tui") {
314
318
  ctx.ui.notify("/topping-statusline-settings requires TUI mode", "error");
315
319
  return;
316
320
  }
317
- const preview = new StatusLinePreview(state, builder, ctx.ui.theme);
321
+ const preview = new StatusLinePreview(builder, ctx.ui.theme);
318
322
  const result = await showMenu<Record<string, MenuValue>>(ctx, {
319
323
  title: "Pi Topping Statusline: Settings",
320
324
  sections: buildSections(state.settings),
package/src/settings.ts CHANGED
@@ -92,6 +92,7 @@ function loadSettings(): StatusLineSettings {
92
92
  }
93
93
  if (raw.feeds !== undefined) settings.feeds = sanitizeFeeds(raw.feeds);
94
94
  if (typeof raw.rainbowBorder === "boolean") settings.rainbowBorder = raw.rainbowBorder;
95
+ if (typeof raw.rainbowAnimation === "boolean") settings.rainbowAnimation = raw.rainbowAnimation;
95
96
  return settings;
96
97
  } catch {
97
98
  // Missing or corrupt settings.json falls back to defaults.
@@ -153,6 +154,7 @@ export function resolveEffectiveSettings(settings: StatusLineSettings): Effectiv
153
154
  bottomLeftSegments,
154
155
  bottomRightSegments,
155
156
  rainbowBorder: settings.rainbowBorder ?? true,
157
+ rainbowAnimation: settings.rainbowAnimation ?? true,
156
158
  includes,
157
159
  segmentOptions: {
158
160
  model: { showModel: seg.model, showProvider: seg.provider, showThinking: seg.thinking },
@@ -113,9 +113,11 @@ test("a left group shifts the gap start by its width", () => {
113
113
  assert.ok(calls[0]!.startCol > 3, "gap starts after the pi segment, not at the origin");
114
114
  });
115
115
 
116
- test("rainbowBorder defaults to true and is loadable", () => {
116
+ test("rainbow settings default to true and resolve explicit false", () => {
117
117
  assert.equal(resolveEffectiveSettings({}).rainbowBorder, true);
118
+ assert.equal(resolveEffectiveSettings({}).rainbowAnimation, true);
118
119
  assert.equal(resolveEffectiveSettings({ rainbowBorder: false }).rainbowBorder, false);
120
+ assert.equal(resolveEffectiveSettings({ rainbowAnimation: false }).rainbowAnimation, false);
119
121
  });
120
122
 
121
123
  test("borderStyle defaults to rounded and is loadable", () => {
package/src/types.ts CHANGED
@@ -69,8 +69,10 @@ export interface StatusLineSettings {
69
69
  borderStyle?: BorderStyle;
70
70
  segments?: StatusLineSegmentToggles;
71
71
  feeds?: StatusLineFeed[];
72
- /** Animated rainbow border while the thinking level is max. */
72
+ /** Rainbow border while the thinking level is max. */
73
73
  rainbowBorder?: boolean;
74
+ /** Animate the rainbow border when it is visible. */
75
+ rainbowAnimation?: boolean;
74
76
  }
75
77
 
76
78
  /** Resolved per-segment render options; resolveEffectiveSettings always fills them. */
@@ -102,6 +104,7 @@ export interface EffectiveStatusLineSettings {
102
104
  bottomRightSegments: StatusLineSegmentId[];
103
105
  segmentOptions: StatusLineSegmentOptions;
104
106
  rainbowBorder: boolean;
107
+ rainbowAnimation: boolean;
105
108
  includes: SegmentIncludes;
106
109
  }
107
110