pi-zentui 0.25.0 → 0.25.1

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.
@@ -149,20 +149,36 @@ export class AgentDurationClock {
149
149
  }
150
150
  }
151
151
 
152
- function segmentGraphemes(value: string): Iterable<string> {
152
+ // Creating an `Intl.Segmenter` per call is very expensive (ICU init). The working
153
+ // line segments the same row for every animation frame, so reuse one instance.
154
+ let sharedGraphemeSegmenter: Intl.Segmenter | undefined;
155
+ let sharedGraphemeSegmenterResolved = false;
156
+
157
+ function getSharedGraphemeSegmenter(): Intl.Segmenter | undefined {
158
+ if (sharedGraphemeSegmenterResolved) return sharedGraphemeSegmenter;
159
+ sharedGraphemeSegmenterResolved = true;
153
160
  try {
154
161
  const Segmenter = Intl.Segmenter;
155
162
  if (typeof Segmenter === "function") {
156
- const segments = new Segmenter(undefined, { granularity: "grapheme" }).segment(value);
157
- return {
158
- *[Symbol.iterator]() {
159
- for (const part of segments) yield part.segment;
160
- },
161
- };
163
+ sharedGraphemeSegmenter = new Segmenter(undefined, { granularity: "grapheme" });
162
164
  }
163
165
  } catch {
164
- // Without Intl.Segmenter, treat the complete value as one conservative grapheme.
166
+ sharedGraphemeSegmenter = undefined;
167
+ }
168
+ return sharedGraphemeSegmenter;
169
+ }
170
+
171
+ function segmentGraphemes(value: string): Iterable<string> {
172
+ const segmenter = getSharedGraphemeSegmenter();
173
+ if (segmenter !== undefined) {
174
+ const segments = segmenter.segment(value);
175
+ return {
176
+ *[Symbol.iterator]() {
177
+ for (const part of segments) yield part.segment;
178
+ },
179
+ };
165
180
  }
181
+ // Without Intl.Segmenter, treat the complete value as one conservative grapheme.
166
182
  return [value];
167
183
  }
168
184
 
@@ -775,6 +791,41 @@ function renderWorkingLineSchedule(
775
791
  const frameStates: WorkingLineFrameState[] = [];
776
792
  let codeUnits = 0;
777
793
  const scheduleOrigin = definition.stateAt(scheduleStartFrame);
794
+ // `row` is invariant across the whole schedule; segment it once instead of per frame.
795
+ const rowCells = graphemeCells(row).cells;
796
+ // Many frames of the schedule share the same textTick/spinnerTick. Render each
797
+ // distinct value once and reuse — this dominates on slow CPUs (e.g. a Pi).
798
+ const textRenderCache = new Map<number, string>();
799
+ const renderTextForTick = (tick: number): string => {
800
+ let cachedText = textRenderCache.get(tick);
801
+ if (cachedText === undefined) {
802
+ cachedText = renderAnimatedText(
803
+ theme,
804
+ config,
805
+ colors,
806
+ rowCells,
807
+ width,
808
+ tick,
809
+ config.textAnimation,
810
+ );
811
+ textRenderCache.set(tick, cachedText);
812
+ }
813
+ return cachedText;
814
+ };
815
+ const spinnerRenderCache = new Map<number, string>();
816
+ const renderSpinnerForTick = (tick: number): string => {
817
+ // Key by the glyph index, not the raw tick: the glyph only repeats every
818
+ // `spinner.frames.length` ticks, so keying raw under-caches badly.
819
+ const frameCount = spinner.frames.length;
820
+ const index = ((tick % frameCount) + frameCount) % frameCount;
821
+ let cachedSpinner = spinnerRenderCache.get(index);
822
+ if (cachedSpinner === undefined) {
823
+ const glyph = spinner.frames[index] ?? spinner.frames[0];
824
+ cachedSpinner = renderTier(theme, config, colors, "high", glyph);
825
+ spinnerRenderCache.set(index, cachedSpinner);
826
+ }
827
+ return cachedSpinner;
828
+ };
778
829
  for (let index = 0; index < definition.frameCount; index += 1) {
779
830
  const scheduled = definition.stateAt(scheduleStartFrame + index);
780
831
  const state = {
@@ -796,15 +847,7 @@ function renderWorkingLineSchedule(
796
847
  state.textTick,
797
848
  config.textAnimation,
798
849
  )}${SGR_RESET}`
799
- : `${renderTier(theme, config, colors, "high", spinnerGlyph)} ${renderAnimatedText(
800
- theme,
801
- config,
802
- colors,
803
- graphemeCells(row).cells,
804
- width,
805
- state.textTick,
806
- config.textAnimation,
807
- )}${SGR_RESET}`;
850
+ : `${renderSpinnerForTick(state.spinnerTick)} ${renderTextForTick(state.textTick)}${SGR_RESET}`;
808
851
  codeUnits += frame.length;
809
852
  if (codeUnits > MAX_WORKING_LINE_FRAME_CODE_UNITS) return undefined;
810
853
  frames.push(frame);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",