@groeponline/pi-wishcraft 1.3.0 → 1.4.0

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.
@@ -20,7 +20,8 @@ const CORE_MOTIONS: readonly MotionDef[] = [
20
20
  colorRole: "motionHot",
21
21
  fallbackGlyph: "*",
22
22
  loop: "while-active",
23
- generator: { geometry: "ember", trail: 3, direction: "forward", intervalMs: 90, ease: "pulse" },
23
+ // ponytail: vivid — longer trail + faster pulse, travels the full Signal rail
24
+ generator: { geometry: "ember", trail: 6, direction: "forward", intervalMs: 62, ease: "pulse" },
24
25
  description: "Lanternwake signature. A warm ember travels the connected segments, only while work happens.",
25
26
  },
26
27
  {
@@ -29,10 +30,11 @@ const CORE_MOTIONS: readonly MotionDef[] = [
29
30
  category: "wishcraft",
30
31
  kind: "frames",
31
32
  channels: ["workingGlyph", "ambient"],
32
- colorRole: "motionDim",
33
+ colorRole: "motionHot",
33
34
  fallbackGlyph: "o",
34
35
  loop: "ambient",
35
- frames: ["◌", "○", "◈", "○"],
36
+ // ponytail: vivid ambient — higher contrast, tighter loop so idle actually breathes
37
+ frames: ["◌", "◎", "◈", "⬡", "◈", "◎"],
36
38
  description: "Soft phase drift for minimal presets.",
37
39
  },
38
40
  {
@@ -56,7 +58,7 @@ const CORE_MOTIONS: readonly MotionDef[] = [
56
58
  colorRole: "motionHot",
57
59
  fallbackGlyph: "#",
58
60
  loop: "while-active",
59
- generator: { geometry: "heat", trail: 4, direction: "forward", intervalMs: 80, ease: "linear" },
61
+ generator: { geometry: "heat", trail: 7, direction: "forward", intervalMs: 55, ease: "linear" },
60
62
  description: "Heat propagates block by block toward the active tool.",
61
63
  },
62
64
  {
@@ -125,10 +127,10 @@ const CORE_MOTIONS: readonly MotionDef[] = [
125
127
  category: "matrix",
126
128
  kind: "generator",
127
129
  channels: ["workingGlyph", "signal", "ambient"],
128
- colorRole: "secondary",
130
+ colorRole: "motionHot",
129
131
  fallbackGlyph: "O",
130
132
  loop: "ambient",
131
- generator: { geometry: "orbit", radius: 2, trail: 3, intervalMs: 280, ease: "breathe" },
133
+ generator: { geometry: "orbit", radius: 3, trail: 5, intervalMs: 150, ease: "breathe" },
132
134
  description: "Orbit and phase motion, slow enough for night work.",
133
135
  },
134
136
  {
@@ -276,13 +278,14 @@ export const CHANNEL_MATRIX: Record<MotionEvent, readonly MotionChannel[]> = {
276
278
  };
277
279
 
278
280
  /** Documented cadence bands per channel. */
281
+ // ponytail: tightened for vivid motion — signal/glyph sweep at ~60-80ms, ambient breathes at ~180ms
279
282
  export const CADENCE_MS: Record<MotionChannel, { min: number; max: number }> = {
280
- workingGlyph: { min: 80, max: 120 },
281
- signal: { min: 80, max: 120 },
283
+ workingGlyph: { min: 55, max: 85 },
284
+ signal: { min: 55, max: 85 },
282
285
  deckTransient: { min: 50, max: 100 },
283
- panelIndicator: { min: 120, max: 250 },
284
- borderEmphasis: { min: 200, max: 400 },
285
- ambient: { min: 250, max: 750 },
286
+ panelIndicator: { min: 100, max: 200 },
287
+ borderEmphasis: { min: 180, max: 350 },
288
+ ambient: { min: 180, max: 400 },
286
289
  };
287
290
 
288
291
  /** Gallery previews are allowed to run hotter than production channels. */
@@ -80,9 +80,10 @@ export function targetFps(
80
80
  ? allowed.filter((channel) => consumers.includes(channel))
81
81
  : allowed;
82
82
  if (live.length === 0) return 0;
83
- if (live.length === 1 && live[0] === "ambient") return 3;
83
+ // ponytail: vivid idle breathes visibly; active sweeps at 16 FPS
84
+ if (live.length === 1 && live[0] === "ambient") return 8;
84
85
  if (effectiveLevel(policy) === "reduced") return 3;
85
- return 10;
86
+ return 16;
86
87
  }
87
88
 
88
89
  /** Color transitions are suppressed without color; glyph motion may stay. */
@@ -18,8 +18,9 @@ import { mergeSegmentsWithCustomItems } from "../config/powerline-config.ts";
18
18
  import { renderSegment } from "../segments/index.ts";
19
19
  import { ansi, colorEnabled, getFgAnsiCode } from "../theme/colors.ts";
20
20
  import { getSeparator } from "../theme/separators.ts";
21
- import { frameAt, getMotion } from "../motion/index.ts";
21
+ import { frameAt, getMotion, sweepPosition, trailGlyph } from "../motion/index.ts";
22
22
  import type { SignalRuntime } from "./controller.ts";
23
+ import { getContributedSignalSources } from "../extension/contrib/registry.ts";
23
24
 
24
25
  export interface SignalRenderOptions {
25
26
  separatorStyle: StatusLineSeparatorStyle;
@@ -50,7 +51,10 @@ export function renderSignal(
50
51
  },
51
52
  );
52
53
  const left = renderModules(merged.leftSegments, ctx);
53
- const right = renderModules(merged.rightSegments, ctx);
54
+ const right = [
55
+ ...renderModules(merged.rightSegments, ctx),
56
+ ...renderContributedSources(ctx),
57
+ ];
54
58
  const secondary = renderModules(merged.secondarySegments, ctx);
55
59
  const separator = getSeparator(options.separatorStyle).left;
56
60
 
@@ -91,11 +95,27 @@ export function renderActivity(
91
95
  const open = spec.caps.leftOpen ?? "";
92
96
  const close = spec.caps.leftClose ?? "";
93
97
  const dim = getFgAnsiCode("sep");
98
+ const hot = getFgAnsiCode("accent");
94
99
  const reset = colorEnabled() ? ansi.reset : "";
95
- const rail = runtime.active
96
- ? `${spec.separators.left}${glyph}${spec.separators.right}`
97
- : `${spec.separators.left}${spec.separators.right}`;
98
- return `${dim}${open}${rail}${close}${reset} ${label}`;
100
+ // ponytail: vivid sweep — 12-char rail with travelling head + 6-char trail, not a single glyph
101
+ const RAIL_WIDTH = 12;
102
+ let railInner: string;
103
+ if (!runtime.active) {
104
+ railInner = "━".repeat(RAIL_WIDTH);
105
+ } else {
106
+ const pos = sweepPosition(runtime.tick, RAIL_WIDTH, true);
107
+ let built = "";
108
+ for (let i = 0; i < RAIL_WIDTH; i++) {
109
+ const dist = Math.abs(i - pos);
110
+ if (dist === 0) built += glyph;
111
+ else if (dist <= 6) built += trailGlyph(dist, ascii);
112
+ else built += "━";
113
+ }
114
+ railInner = built;
115
+ }
116
+ const railColor = runtime.active ? hot : dim;
117
+ const rail = `${spec.separators.left}${railInner}${spec.separators.right}`;
118
+ return `${railColor}${open}${rail}${close}${reset} ${label}`;
99
119
  }
100
120
 
101
121
  function fitLanes(
@@ -164,6 +184,29 @@ function joinModules(modules: Module[], separator: string, width: number): strin
164
184
  return fitted.length ? ` ${fitted.map((module) => module.content).join(sep)} ` : "";
165
185
  }
166
186
 
187
+ const ANSI_CONTROL_RE = /\x1B(?:\][^\x07]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])|\x9B[0-?]*[ -/]*[@-~]|\x9D[^\x07]*(?:\x07|\x1B\\)/g;
188
+
189
+ function stripAnsiControls(text: string): string {
190
+ return text.replace(ANSI_CONTROL_RE, "");
191
+ }
192
+
193
+ function renderContributedSources(ctx: SegmentContext): Module[] {
194
+ const out: Module[] = [];
195
+ for (const src of getContributedSignalSources()) {
196
+ try {
197
+ const raw = src.render?.(ctx);
198
+ if (!raw || typeof raw !== "string") continue;
199
+ const content = raw.trim();
200
+ if (!stripAnsiControls(content).trim()) continue;
201
+ // ponytail: fault-isolated — a throwing or empty source never takes down Signal
202
+ out.push({ content, width: visibleWidth(content) });
203
+ } catch {
204
+ // skip failing contribution
205
+ }
206
+ }
207
+ return out;
208
+ }
209
+
167
210
  function styledSeparator(separator: string): string {
168
211
  const color = getFgAnsiCode("sep");
169
212
  const reset = colorEnabled() ? ansi.reset : "";