@ciphore/radiocli 0.2.1 → 0.2.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/CHANGELOG.md +133 -1
- package/README.md +76 -6
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +143 -0
- package/dist/agent/launcher.js +71 -0
- package/dist/agent/mcp-install.js +467 -0
- package/dist/agent/mcp-server.js +139 -0
- package/dist/agent/service.js +347 -0
- package/dist/agent/session.js +248 -0
- package/dist/alarms/active-session.js +183 -0
- package/dist/alarms/cli.js +312 -0
- package/dist/alarms/guard.js +343 -0
- package/dist/alarms/inhibitor.js +48 -0
- package/dist/alarms/power-guard-store.js +169 -0
- package/dist/alarms/runner.js +342 -0
- package/dist/alarms/runtime-health.js +79 -0
- package/dist/alarms/schedule.js +149 -0
- package/dist/alarms/scheduler.js +250 -0
- package/dist/alarms/setup-verification.js +187 -0
- package/dist/alarms/system-volume.js +43 -0
- package/dist/alarms/terminal-launcher.js +181 -0
- package/dist/alarms/tui-presence.js +38 -0
- package/dist/cli.js +113 -5
- package/dist/player/backend-install.js +2 -1
- package/dist/player/command-diagnostics.js +27 -0
- package/dist/player/command.js +123 -62
- package/dist/player/player-controller.js +32 -2
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +36 -6
- package/dist/setup.js +462 -0
- package/dist/storage/store.js +262 -2
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +111 -26
- package/dist/ui/App.js +401 -67
- package/dist/ui/AppContent.js +24 -7
- package/dist/ui/adaptive-explore-layout.js +47 -0
- package/dist/ui/alarm-editor.js +174 -0
- package/dist/ui/alarm-tui-service.js +84 -0
- package/dist/ui/app-state.js +3 -0
- package/dist/ui/ascii.js +8 -0
- package/dist/ui/components/AdaptiveMarquee.js +28 -0
- package/dist/ui/components/StationList.js +10 -5
- package/dist/ui/components/VersionIndicator.js +19 -0
- package/dist/ui/cosmo-world-map.js +5 -2
- package/dist/ui/explore-map-layout.js +18 -6
- package/dist/ui/format.js +21 -0
- package/dist/ui/help-content.js +14 -2
- package/dist/ui/layout.js +1 -1
- package/dist/ui/page-footer.js +120 -2
- package/dist/ui/receiver-animation.js +68 -0
- package/dist/ui/screen-items.js +41 -9
- package/dist/ui/screen-meta.js +4 -0
- package/dist/ui/screens/AlarmsScreen.js +202 -0
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +8 -3
- package/dist/ui/screens/HomeScreen.js +3 -1
- package/dist/ui/screens/NowPlayingScreen.js +6 -2
- package/dist/ui/screens/SettingsScreen.js +70 -53
- package/dist/ui/screens/StationScreen.js +3 -2
- package/dist/ui/selection-state.js +10 -0
- package/dist/ui/terminal-mouse.js +18 -3
- package/dist/ui/use-alarm-tui.js +727 -0
- package/dist/ui/use-app-input.js +107 -46
- package/dist/ui/visualizers/gallop.js +118 -0
- package/dist/ui/visualizers/horse-stride.js +20 -0
- package/dist/ui/visualizers/receiver-style-registry.js +14 -7
- package/dist/ui/visualizers/receiver-visualizers.js +233 -128
- package/dist/ui/visualizers/retro-receivers.js +4 -0
- package/dist/ui/visualizers/terminal-receivers.js +57 -0
- package/dist/update-check.js +26 -7
- package/package.json +6 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { themeAccent, themeContributionColors } from '../theme.js';
|
|
2
|
+
import { retroReceiverBuilders } from './retro-receivers.js';
|
|
2
3
|
import { receiverStyleMetadata } from './receiver-style-registry.js';
|
|
3
4
|
const receiverStyleBuilders = {
|
|
5
|
+
...retroReceiverBuilders,
|
|
4
6
|
ultracode: buildUltracode,
|
|
5
7
|
'motion-contour': buildMotionContour,
|
|
6
8
|
leds: buildLeds,
|
|
@@ -13,7 +15,7 @@ const receiverStyleBuilders = {
|
|
|
13
15
|
starfield: buildAsciiStarfield,
|
|
14
16
|
mesh: buildMesh,
|
|
15
17
|
ribbon: buildRibbon,
|
|
16
|
-
|
|
18
|
+
equalizer: buildEqualizer,
|
|
17
19
|
tunnel: buildTunnel,
|
|
18
20
|
kaleidoscope: buildKaleidoscope,
|
|
19
21
|
constellation: buildConstellation,
|
|
@@ -89,6 +91,7 @@ function buildMicroVisualizer(style, pulse, width, height, theme) {
|
|
|
89
91
|
// the circular braille rings) while fixed-size and minimum-size scenes use the
|
|
90
92
|
// supersampled fit path below.
|
|
91
93
|
const microNativeScaleStyles = new Set([
|
|
94
|
+
...Object.keys(retroReceiverBuilders),
|
|
92
95
|
'pulse-grid',
|
|
93
96
|
'hex-pulse',
|
|
94
97
|
'galaxy',
|
|
@@ -170,9 +173,76 @@ function buildFlatZeroSignal(width, requestedHeight, theme) {
|
|
|
170
173
|
color: rowIndex === height - 1 ? accent : '#767676'
|
|
171
174
|
}));
|
|
172
175
|
}
|
|
176
|
+
function buildEqualizer(pulse, width, height, theme) {
|
|
177
|
+
const accent = themeAccent(theme);
|
|
178
|
+
const bandWidth = 3;
|
|
179
|
+
const bandCount = Math.floor((width - 2) / bandWidth);
|
|
180
|
+
const levels = Array.from({ length: bandCount }, (_, i) => {
|
|
181
|
+
const low = Math.sin(i * 0.18 + pulse * 0.46);
|
|
182
|
+
const mid = Math.cos(i * 0.32 - pulse * 0.28);
|
|
183
|
+
const high = Math.sin(i * 0.45 + pulse * 0.68);
|
|
184
|
+
const normalized = (low * 0.4 + mid * 0.35 + high * 0.25 + 1) / 2;
|
|
185
|
+
const eased = Math.pow(Math.max(0, Math.min(1, normalized)), 1.4);
|
|
186
|
+
return Math.round(eased * height);
|
|
187
|
+
});
|
|
188
|
+
const peaks = Array.from({ length: bandCount }, (_, i) => {
|
|
189
|
+
let maxLvl = 0;
|
|
190
|
+
for (let k = 0; k < 8; k++) {
|
|
191
|
+
const p = (pulse - k + 240) % 240;
|
|
192
|
+
const low = Math.sin(i * 0.18 + p * 0.46);
|
|
193
|
+
const mid = Math.cos(i * 0.32 - p * 0.28);
|
|
194
|
+
const high = Math.sin(i * 0.45 + p * 0.68);
|
|
195
|
+
const normalized = (low * 0.4 + mid * 0.35 + high * 0.25 + 1) / 2;
|
|
196
|
+
const eased = Math.pow(Math.max(0, Math.min(1, normalized)), 1.4);
|
|
197
|
+
const lvl = Math.round(eased * height);
|
|
198
|
+
if (lvl > maxLvl) {
|
|
199
|
+
maxLvl = lvl;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return maxLvl;
|
|
203
|
+
});
|
|
204
|
+
const rows = [];
|
|
205
|
+
const colors = themeContributionColors(theme);
|
|
206
|
+
for (let rowIndex = 0; rowIndex < height; rowIndex++) {
|
|
207
|
+
const threshold = height - rowIndex;
|
|
208
|
+
let rowText = ' ';
|
|
209
|
+
for (let i = 0; i < bandCount; i++) {
|
|
210
|
+
const lvl = levels[i];
|
|
211
|
+
const peak = peaks[i];
|
|
212
|
+
let bandChar = ' ';
|
|
213
|
+
if (lvl >= threshold) {
|
|
214
|
+
bandChar = '██';
|
|
215
|
+
}
|
|
216
|
+
else if (peak === threshold) {
|
|
217
|
+
bandChar = '◆◆';
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
bandChar = '··';
|
|
221
|
+
}
|
|
222
|
+
rowText += bandChar + ' ';
|
|
223
|
+
}
|
|
224
|
+
let color = colors[2] ?? accent;
|
|
225
|
+
if (rowIndex < Math.max(1, height * 0.3)) {
|
|
226
|
+
color = '#ff5f87';
|
|
227
|
+
}
|
|
228
|
+
else if (rowIndex < Math.max(2, height * 0.6)) {
|
|
229
|
+
color = colors[4] ?? accent;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
color = colors[3] ?? accent;
|
|
233
|
+
}
|
|
234
|
+
rows.push({
|
|
235
|
+
text: rowText.padEnd(width).slice(0, width),
|
|
236
|
+
color
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return rows;
|
|
240
|
+
}
|
|
173
241
|
const ultracodeWavelength = 20;
|
|
174
242
|
const ultracodeTravelPerPulse = 80 * 0.03;
|
|
175
243
|
const ultracodeVioletRamp = ['#3e1676', '#491e87', '#542799', '#5f2faa', '#6b37bc', '#763fcd', '#8148df', '#8c50f0'];
|
|
244
|
+
const ultracodeDistanceCache = new Map();
|
|
245
|
+
const ultracodeRampCache = new Map();
|
|
176
246
|
function buildUltracode(pulse, width, height, theme) {
|
|
177
247
|
const renderWidth = Math.max(1, width);
|
|
178
248
|
const renderHeight = Math.max(1, height);
|
|
@@ -180,25 +250,43 @@ function buildUltracode(pulse, width, height, theme) {
|
|
|
180
250
|
const ramp = ultracodeRippleRamp(theme);
|
|
181
251
|
const selectedColor = ramp[ramp.length - 1] ?? accent;
|
|
182
252
|
const travel = pulse * ultracodeTravelPerPulse;
|
|
183
|
-
const
|
|
184
|
-
const
|
|
253
|
+
const distances = ultracodeDistances(renderWidth, renderHeight);
|
|
254
|
+
const blank = ' '.repeat(renderWidth);
|
|
185
255
|
const rows = Array.from({ length: renderHeight }, (_, rowIndex) => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
256
|
+
const segments = [];
|
|
257
|
+
for (let columnIndex = 0; columnIndex < renderWidth; columnIndex += 1) {
|
|
258
|
+
const level = ultracodeRippleLevel(distances[rowIndex * renderWidth + columnIndex] ?? 0, travel, ramp.length);
|
|
259
|
+
const color = level === null ? accent : selectedColor;
|
|
260
|
+
const backgroundColor = level === null ? undefined : ramp[level] ?? selectedColor;
|
|
261
|
+
const previous = segments.at(-1);
|
|
262
|
+
if (previous?.color === color && previous.backgroundColor === backgroundColor && previous.bold === undefined) {
|
|
263
|
+
previous.text += ' ';
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
segments.push({ text: ' ', color, backgroundColor, bold: undefined });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { text: blank, color: accent, segments };
|
|
195
270
|
});
|
|
196
271
|
return rows;
|
|
197
272
|
}
|
|
198
|
-
function
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
273
|
+
function ultracodeDistances(width, height) {
|
|
274
|
+
const key = `${width}x${height}`;
|
|
275
|
+
const cached = ultracodeDistanceCache.get(key);
|
|
276
|
+
if (cached)
|
|
277
|
+
return cached;
|
|
278
|
+
const originColumn = Math.floor(width / 2);
|
|
279
|
+
const originRow = Math.floor(height / 2);
|
|
280
|
+
const distances = new Float64Array(width * height);
|
|
281
|
+
for (let row = 0; row < height; row += 1) {
|
|
282
|
+
for (let column = 0; column < width; column += 1) {
|
|
283
|
+
const dx = column - originColumn;
|
|
284
|
+
const dy = (row - originRow) * 2;
|
|
285
|
+
distances[row * width + column] = Math.sqrt(dx * dx + dy * dy);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
rememberGeometry(ultracodeDistanceCache, key, distances);
|
|
289
|
+
return distances;
|
|
202
290
|
}
|
|
203
291
|
function ultracodeRippleLevel(distance, travel, rampLength) {
|
|
204
292
|
if (distance > travel) {
|
|
@@ -212,10 +300,15 @@ function ultracodeRippleRamp(theme) {
|
|
|
212
300
|
if (theme === 'violet') {
|
|
213
301
|
return ultracodeVioletRamp;
|
|
214
302
|
}
|
|
303
|
+
const cached = ultracodeRampCache.get(theme);
|
|
304
|
+
if (cached)
|
|
305
|
+
return cached;
|
|
215
306
|
const colors = themeContributionColors(theme);
|
|
216
307
|
const start = colors[1] ?? '#1c1c1c';
|
|
217
308
|
const end = themeAccent(theme);
|
|
218
|
-
|
|
309
|
+
const ramp = Array.from({ length: 8 }, (_, index) => interpolateHex(start, end, index / 7));
|
|
310
|
+
ultracodeRampCache.set(theme, ramp);
|
|
311
|
+
return ramp;
|
|
219
312
|
}
|
|
220
313
|
function interpolateHex(startHex, endHex, amount) {
|
|
221
314
|
const start = hexToRgb(startHex);
|
|
@@ -481,6 +574,17 @@ function buildAsciiFire(pulse, width, height, theme) {
|
|
|
481
574
|
// A few additional overlapping plumes keep the center lively without
|
|
482
575
|
// widening the compact hearth silhouette.
|
|
483
576
|
const plumeCount = Math.max(7, Math.min(11, Math.round(width / 13)));
|
|
577
|
+
const plumes = Array.from({ length: plumeCount }, (_, index) => {
|
|
578
|
+
const phase = hashNoise(index, 3, 0) * Math.PI * 2;
|
|
579
|
+
return {
|
|
580
|
+
index,
|
|
581
|
+
phase,
|
|
582
|
+
baseCenter: fireLeft + ((index + 0.5) / plumeCount) * fireSpan,
|
|
583
|
+
flameHeight: 0.46 + hashNoise(index, 4, 0) * 0.39 + bass * 0.045 + Math.sin(pulse * 0.052 + phase) * 0.055,
|
|
584
|
+
swaySpeed: 0.038 + hashNoise(index, 5, 0) * 0.02,
|
|
585
|
+
baseHalfWidth: fireSpan * (0.095 + hashNoise(index, 6, 0) * 0.035)
|
|
586
|
+
};
|
|
587
|
+
});
|
|
484
588
|
const embers = Array.from({ length: Math.max(4, Math.round(width / 18)) }, (_, index) => {
|
|
485
589
|
const cycle = height + 5 + Math.floor(hashNoise(index, 18, 0) * height);
|
|
486
590
|
const travel = (pulse * (0.13 + hashNoise(index, 19, 0) * 0.1) + hashNoise(index, 20, 0) * cycle) % cycle;
|
|
@@ -492,32 +596,33 @@ function buildAsciiFire(pulse, width, height, theme) {
|
|
|
492
596
|
});
|
|
493
597
|
for (let y = 0; y < height; y += 1) {
|
|
494
598
|
const rise = (height - 1 - y) / Math.max(1, height - 1);
|
|
599
|
+
const activePlumes = plumes.flatMap(plume => {
|
|
600
|
+
if (rise > plume.flameHeight)
|
|
601
|
+
return [];
|
|
602
|
+
const vertical = rise / Math.max(0.01, plume.flameHeight);
|
|
603
|
+
const sway = Math.sin(pulse * plume.swaySpeed + plume.phase + vertical * 3.4) *
|
|
604
|
+
(0.005 + vertical * 0.026) +
|
|
605
|
+
Math.sin(vertical * 8.5 - pulse * 0.09 + plume.index) * vertical * 0.008;
|
|
606
|
+
return [{
|
|
607
|
+
...plume,
|
|
608
|
+
vertical,
|
|
609
|
+
center: plume.baseCenter + sway,
|
|
610
|
+
halfWidth: Math.max(0.002, plume.baseHalfWidth * (0.18 + 0.82 * Math.pow(1 - vertical, 0.7)))
|
|
611
|
+
}];
|
|
612
|
+
});
|
|
495
613
|
for (let x = 0; x < width; x += 1) {
|
|
496
614
|
const nx = x / Math.max(1, width - 1);
|
|
497
615
|
let heat = 0;
|
|
498
|
-
for (
|
|
499
|
-
const
|
|
500
|
-
const baseCenter = fireLeft + ((index + 0.5) / plumeCount) * fireSpan;
|
|
501
|
-
const flameHeight = 0.46 + hashNoise(index, 4, 0) * 0.39 + bass * 0.045 + Math.sin(pulse * 0.052 + phase) * 0.055;
|
|
502
|
-
if (rise > flameHeight) {
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
const vertical = rise / Math.max(0.01, flameHeight);
|
|
506
|
-
const sway = Math.sin(pulse * (0.038 + hashNoise(index, 5, 0) * 0.02) + phase + vertical * 3.4) *
|
|
507
|
-
(0.005 + vertical * 0.026) +
|
|
508
|
-
Math.sin(vertical * 8.5 - pulse * 0.09 + index) * vertical * 0.008;
|
|
509
|
-
const center = baseCenter + sway;
|
|
510
|
-
const baseHalfWidth = fireSpan * (0.095 + hashNoise(index, 6, 0) * 0.035);
|
|
511
|
-
const halfWidth = Math.max(0.002, baseHalfWidth * (0.18 + 0.82 * Math.pow(1 - vertical, 0.7)));
|
|
512
|
-
const lateral = Math.abs(nx - center) / halfWidth;
|
|
616
|
+
for (const plume of activePlumes) {
|
|
617
|
+
const lateral = Math.abs(nx - plume.center) / plume.halfWidth;
|
|
513
618
|
if (lateral >= 1.15) {
|
|
514
619
|
continue;
|
|
515
620
|
}
|
|
516
|
-
const body = Math.exp(-lateral * lateral * 1.7) * (1 - vertical * 0.3);
|
|
517
|
-
const upwardCurl = Math.sin(x * 0.31 + rise * 13 - pulse * 0.17 + phase) * 0.1 +
|
|
621
|
+
const body = Math.exp(-lateral * lateral * 1.7) * (1 - plume.vertical * 0.3);
|
|
622
|
+
const upwardCurl = Math.sin(x * 0.31 + rise * 13 - pulse * 0.17 + plume.phase) * 0.1 +
|
|
518
623
|
Math.sin(x * 0.67 - rise * 19 - pulse * 0.11) * 0.055;
|
|
519
|
-
const breakup = hashNoise(Math.floor(x / 2), Math.floor(y / 2), index) * 0.07;
|
|
520
|
-
const contribution = Math.max(0, body + upwardCurl + breakup - vertical * 0.1);
|
|
624
|
+
const breakup = hashNoise(Math.floor(x / 2), Math.floor(y / 2), plume.index) * 0.07;
|
|
625
|
+
const contribution = Math.max(0, body + upwardCurl + breakup - plume.vertical * 0.1);
|
|
521
626
|
heat += contribution * 0.68;
|
|
522
627
|
}
|
|
523
628
|
const sideEnvelope = Math.sin(clampNumber((nx - fireLeft) / fireSpan, 0, 1) * Math.PI);
|
|
@@ -724,16 +829,13 @@ function buildAsciiDonut(pulse, width, height, theme) {
|
|
|
724
829
|
function buildAsciiStarfield(pulse, width, height, theme) {
|
|
725
830
|
const accent = themeAccent(theme);
|
|
726
831
|
const ramp = themeContributionColors(theme);
|
|
727
|
-
const
|
|
728
|
-
const cy = (height - 1) / 2;
|
|
832
|
+
const geometry = starfieldGeometry(width, height);
|
|
729
833
|
// Dense multi-layer warp field: dust nebula + mid stars + near streaks + flares
|
|
730
834
|
return Array.from({ length: height }, (_, y) => {
|
|
731
835
|
const cells = Array.from({ length: width }, (_, x) => {
|
|
732
|
-
const
|
|
733
|
-
const
|
|
734
|
-
const
|
|
735
|
-
const dist = Math.sqrt(dx * dx + dy * dy) + 0.001;
|
|
736
|
-
const maxDist = Math.sqrt(cx * cx + (cy * 2.05) ** 2);
|
|
836
|
+
const cellIndex = y * width + x;
|
|
837
|
+
const angle = geometry.angle[cellIndex] ?? 0;
|
|
838
|
+
const dist = geometry.distance[cellIndex] ?? 0.001;
|
|
737
839
|
// Soft nebula dust so the void isn't empty
|
|
738
840
|
const nebula = 0.22 * Math.sin(angle * 3 + pulse * 0.03) +
|
|
739
841
|
0.18 * Math.sin(dist * 0.12 - pulse * 0.04 + angle * 2) +
|
|
@@ -747,17 +849,14 @@ function buildAsciiStarfield(pulse, width, height, theme) {
|
|
|
747
849
|
for (let layer = 0; layer < 4; layer += 1) {
|
|
748
850
|
const layerSpeed = 0.35 + layer * 0.55;
|
|
749
851
|
// Quantize angle into star "lanes" so stars feel discrete
|
|
750
|
-
const
|
|
751
|
-
const lane = Math.floor(((angle + Math.PI) / (Math.PI * 2)) * laneCount);
|
|
752
|
-
const laneAngle = (lane / laneCount) * Math.PI * 2 - Math.PI;
|
|
753
|
-
const angDiff = Math.abs(Math.atan2(Math.sin(angle - laneAngle), Math.cos(angle - laneAngle)));
|
|
852
|
+
const angDiff = geometry.angularDifference[layer]?.[cellIndex] ?? Math.PI;
|
|
754
853
|
if (angDiff > 0.04 + layer * 0.01) {
|
|
755
854
|
continue;
|
|
756
855
|
}
|
|
757
|
-
const seed =
|
|
758
|
-
const phase =
|
|
856
|
+
const seed = geometry.seed[layer]?.[cellIndex] ?? 0;
|
|
857
|
+
const phase = geometry.phase[layer]?.[cellIndex] ?? 0;
|
|
759
858
|
const depth = ((pulse * layerSpeed * 0.45 + phase * 40 + seed) % 100) / 100;
|
|
760
|
-
const starDist = Math.pow(depth, 1.55) *
|
|
859
|
+
const starDist = Math.pow(depth, 1.55) * geometry.maxDistance * (0.15 + phase * 0.85);
|
|
761
860
|
const along = Math.abs(dist - starDist);
|
|
762
861
|
// Near stars elongate into streaks
|
|
763
862
|
const streak = layer >= 2 ? 0.8 + depth * 4.5 : 0.35 + depth * 1.2;
|
|
@@ -861,48 +960,6 @@ function buildRibbon(pulse, width, height, theme) {
|
|
|
861
960
|
}
|
|
862
961
|
return grid.map(row => lineFromCells(row, accent));
|
|
863
962
|
}
|
|
864
|
-
function buildMirror(pulse, width, height, theme) {
|
|
865
|
-
const grid = emptyMotionGrid(width, height, '#0a0e16');
|
|
866
|
-
const colors = themeContributionColors(theme);
|
|
867
|
-
const accent = themeAccent(theme);
|
|
868
|
-
const baseY = Math.round((height - 1) * 0.66);
|
|
869
|
-
const bandWidth = 2;
|
|
870
|
-
const bands = Math.ceil((width + 1) / bandWidth);
|
|
871
|
-
const sample = (band, p) => clampNumber((Math.sin(band * 0.4 + p * 0.3) * 0.42 +
|
|
872
|
-
Math.sin(band * 0.13 - p * 0.18) * 0.34 +
|
|
873
|
-
Math.sin(band * 0.66 + p * 0.52) * 0.24 +
|
|
874
|
-
1) /
|
|
875
|
-
2, 0, 1);
|
|
876
|
-
for (let band = 0; band < bands; band += 1) {
|
|
877
|
-
const x = band * bandWidth;
|
|
878
|
-
const up = Math.round(sample(band, pulse) * baseY);
|
|
879
|
-
let peak = 0;
|
|
880
|
-
for (let k = 0; k < 10; k += 1) {
|
|
881
|
-
peak = Math.max(peak, Math.round(sample(band, pulse - k) * baseY));
|
|
882
|
-
}
|
|
883
|
-
for (let d = 0; d <= up; d += 1) {
|
|
884
|
-
const frac = d / Math.max(1, baseY);
|
|
885
|
-
const colorIndex = Math.min(colors.length - 1, 1 + Math.round(frac * (colors.length - 2)));
|
|
886
|
-
paintCell(grid, x, baseY - d, '█', colors[colorIndex] ?? accent);
|
|
887
|
-
}
|
|
888
|
-
const reflection = Math.round(up * 0.45);
|
|
889
|
-
for (let d = 1; d <= reflection; d += 1) {
|
|
890
|
-
paintCell(grid, x, baseY + d, d > reflection - 1 ? '░' : '▒', '#27313d');
|
|
891
|
-
}
|
|
892
|
-
if (peak > 0) {
|
|
893
|
-
paintCell(grid, x, baseY - peak, '▀', '#ffffff');
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
const baseline = grid[baseY];
|
|
897
|
-
if (baseline) {
|
|
898
|
-
for (let x = 0; x < width; x += 1) {
|
|
899
|
-
if (baseline[x]?.text === ' ') {
|
|
900
|
-
paintCell(grid, x, baseY, '─', '#1d2530');
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
return grid.map(row => lineFromCells(row, accent));
|
|
905
|
-
}
|
|
906
963
|
function buildTunnel(pulse, width, height, theme) {
|
|
907
964
|
const grid = emptyMotionGrid(width, height, '#04060a');
|
|
908
965
|
const colors = themeContributionColors(theme);
|
|
@@ -995,30 +1052,28 @@ function buildConstellation(pulse, width, height, theme) {
|
|
|
995
1052
|
return grid.map(row => lineFromCells(row, accent));
|
|
996
1053
|
}
|
|
997
1054
|
function buildPulseGrid(pulse, width, height, theme) {
|
|
998
|
-
const
|
|
1055
|
+
const grid = emptyMotionGrid(width, height, '#0a0e16');
|
|
999
1056
|
const accent = themeAccent(theme);
|
|
1000
1057
|
const colors = themeContributionColors(theme);
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const stroke = Math.max(0.65, ringGap * 0.1);
|
|
1008
|
-
for (let y = 0; y < dotsHigh; y += 1) {
|
|
1009
|
-
for (let x = 0; x < dotsWide; x += 1) {
|
|
1010
|
-
const radius = Math.hypot(x - cx, y - cy);
|
|
1011
|
-
const phase = ((radius - travel) % ringGap + ringGap) % ringGap;
|
|
1012
|
-
const distanceToRing = Math.min(phase, ringGap - phase);
|
|
1013
|
-
if (distanceToRing > stroke)
|
|
1058
|
+
const cx = (width - 1) / 2;
|
|
1059
|
+
const cy = (height - 1) / 2;
|
|
1060
|
+
const levels = ['·', '∘', '•', '●', '◉'];
|
|
1061
|
+
for (let y = 0; y < height; y += 1) {
|
|
1062
|
+
for (let x = 0; x < width; x += 1) {
|
|
1063
|
+
if (x % 2 === 1) {
|
|
1014
1064
|
continue;
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1065
|
+
}
|
|
1066
|
+
const dx = (x - cx) / 2.0;
|
|
1067
|
+
const dy = y - cy;
|
|
1068
|
+
const r = Math.sqrt(dx * dx + dy * dy);
|
|
1069
|
+
const wave = Math.sin(r * 0.7 - pulse * 0.3) * 0.5 + 0.5;
|
|
1070
|
+
const value = clampNumber(wave, 0, 1);
|
|
1071
|
+
const levelIndex = Math.min(levels.length - 1, Math.floor(value * levels.length));
|
|
1072
|
+
const colorIndex = Math.min(colors.length - 1, Math.max(1, Math.round(value * (colors.length - 1))));
|
|
1073
|
+
paintCell(grid, x, y, levels[levelIndex] ?? '·', value > 0.16 ? colors[colorIndex] ?? accent : '#1a2029');
|
|
1019
1074
|
}
|
|
1020
1075
|
}
|
|
1021
|
-
return
|
|
1076
|
+
return grid.map(row => lineFromCells(row, accent));
|
|
1022
1077
|
}
|
|
1023
1078
|
function buildLissajous(pulse, width, height, theme) {
|
|
1024
1079
|
const grid = emptyMotionGrid(width, height, '#04070d');
|
|
@@ -1986,29 +2041,31 @@ function buildAurora(pulse, width, height, theme) {
|
|
|
1986
2041
|
const curtains = 6;
|
|
1987
2042
|
return Array.from({ length: height }, (_, y) => {
|
|
1988
2043
|
const ny = y / Math.max(1, height - 1);
|
|
2044
|
+
const curtainRows = Array.from({ length: curtains }, (_, c) => {
|
|
2045
|
+
const fold = 0.08 +
|
|
2046
|
+
c * 0.15 +
|
|
2047
|
+
0.06 * Math.sin(pulse * 0.035 + c * 1.9) +
|
|
2048
|
+
0.03 * Math.sin(y * 0.28 + pulse * 0.05 + c) +
|
|
2049
|
+
0.02 * Math.sin(y * 0.55 - pulse * 0.04 + c * 0.7);
|
|
2050
|
+
const halfWidth = 0.045 + 0.02 * Math.sin(pulse * 0.03 + c) + 0.015 * Math.sin(y * 0.4 + c);
|
|
2051
|
+
const vertical = 0.35 +
|
|
2052
|
+
0.65 * Math.sin(Math.PI * clampNumber(ny * 1.15 - 0.05, 0, 1)) *
|
|
2053
|
+
(0.7 + 0.3 * Math.sin(pulse * 0.06 + c * 1.1 + y * 0.15));
|
|
2054
|
+
return { c, fold, halfWidth, vertical };
|
|
2055
|
+
});
|
|
1989
2056
|
const cells = Array.from({ length: width }, (_, x) => {
|
|
1990
2057
|
const nx = x / Math.max(1, width - 1);
|
|
1991
2058
|
let field = 0;
|
|
1992
2059
|
let filament = 0;
|
|
1993
|
-
for (
|
|
1994
|
-
// Curtain folds drift slowly; each sheet has different phase & width
|
|
1995
|
-
const
|
|
1996
|
-
c * 0.15 +
|
|
1997
|
-
0.06 * Math.sin(pulse * 0.035 + c * 1.9) +
|
|
1998
|
-
0.03 * Math.sin(y * 0.28 + pulse * 0.05 + c) +
|
|
1999
|
-
0.02 * Math.sin(y * 0.55 - pulse * 0.04 + c * 0.7);
|
|
2000
|
-
const halfWidth = 0.045 + 0.02 * Math.sin(pulse * 0.03 + c) + 0.015 * Math.sin(y * 0.4 + c);
|
|
2001
|
-
const dist = Math.abs(nx - fold);
|
|
2060
|
+
for (const curtain of curtainRows) {
|
|
2061
|
+
// Curtain folds drift slowly; each sheet has different phase & width.
|
|
2062
|
+
const dist = Math.abs(nx - curtain.fold);
|
|
2002
2063
|
// Soft body of the curtain (filled, not holey)
|
|
2003
|
-
const body = Math.exp(-Math.pow(dist / halfWidth, 2));
|
|
2004
|
-
|
|
2005
|
-
const vertical = 0.35 +
|
|
2006
|
-
0.65 * Math.sin(Math.PI * clampNumber(ny * 1.15 - 0.05, 0, 1)) *
|
|
2007
|
-
(0.7 + 0.3 * Math.sin(pulse * 0.06 + c * 1.1 + y * 0.15));
|
|
2008
|
-
field += body * vertical * (0.85 + 0.15 * Math.sin(pulse * 0.08 + c));
|
|
2064
|
+
const body = Math.exp(-Math.pow(dist / curtain.halfWidth, 2));
|
|
2065
|
+
field += body * curtain.vertical * (0.85 + 0.15 * Math.sin(pulse * 0.08 + curtain.c));
|
|
2009
2066
|
// Fine vertical filaments inside the curtain
|
|
2010
2067
|
if (body > 0.15) {
|
|
2011
|
-
const micro = Math.sin(x * (2.8 + c * 0.4) + pulse * 0.25 + y * 0.1);
|
|
2068
|
+
const micro = Math.sin(x * (2.8 + curtain.c * 0.4) + pulse * 0.25 + y * 0.1);
|
|
2012
2069
|
filament += body * Math.max(0, micro) * 0.35;
|
|
2013
2070
|
}
|
|
2014
2071
|
}
|
|
@@ -4140,9 +4197,10 @@ function rgbColor(red, green, blue) {
|
|
|
4140
4197
|
return `#${channel(red)}${channel(green)}${channel(blue)}`;
|
|
4141
4198
|
}
|
|
4142
4199
|
function lineFromCells(cells, fallbackColor) {
|
|
4143
|
-
|
|
4200
|
+
let text = '';
|
|
4144
4201
|
const segments = [];
|
|
4145
4202
|
for (const cell of cells) {
|
|
4203
|
+
text += cell.text;
|
|
4146
4204
|
const invisibleForeground = cell.text.trim() === '' && cell.backgroundColor === undefined;
|
|
4147
4205
|
const color = invisibleForeground ? fallbackColor : cell.color;
|
|
4148
4206
|
const bold = invisibleForeground ? undefined : cell.bold;
|
|
@@ -4310,6 +4368,53 @@ function hashNoise(x, y, pulse) {
|
|
|
4310
4368
|
const value = Math.sin(x * 12.9898 + y * 78.233 + pulse * 0.037) * 43758.5453;
|
|
4311
4369
|
return value - Math.floor(value);
|
|
4312
4370
|
}
|
|
4371
|
+
const STARFIELD_GEOMETRY_CACHE_LIMIT = 4;
|
|
4372
|
+
const starfieldGeometryCache = new Map();
|
|
4373
|
+
function starfieldGeometry(width, height) {
|
|
4374
|
+
const key = `${width}x${height}`;
|
|
4375
|
+
const cached = starfieldGeometryCache.get(key);
|
|
4376
|
+
if (cached)
|
|
4377
|
+
return cached;
|
|
4378
|
+
const size = Math.max(0, width * height);
|
|
4379
|
+
const angle = new Float64Array(size);
|
|
4380
|
+
const distance = new Float64Array(size);
|
|
4381
|
+
const angularDifference = Array.from({ length: 4 }, () => new Float64Array(size));
|
|
4382
|
+
const seed = Array.from({ length: 4 }, () => new Int32Array(size));
|
|
4383
|
+
const phase = Array.from({ length: 4 }, () => new Float64Array(size));
|
|
4384
|
+
const cx = (width - 1) / 2;
|
|
4385
|
+
const cy = (height - 1) / 2;
|
|
4386
|
+
const maxDistance = Math.sqrt(cx * cx + (cy * 2.05) ** 2);
|
|
4387
|
+
for (let y = 0; y < height; y += 1) {
|
|
4388
|
+
for (let x = 0; x < width; x += 1) {
|
|
4389
|
+
const index = y * width + x;
|
|
4390
|
+
const dx = x - cx;
|
|
4391
|
+
const dy = (y - cy) * 2.05;
|
|
4392
|
+
const cellAngle = Math.atan2(dy, dx);
|
|
4393
|
+
angle[index] = cellAngle;
|
|
4394
|
+
distance[index] = Math.sqrt(dx * dx + dy * dy) + 0.001;
|
|
4395
|
+
for (let layer = 0; layer < 4; layer += 1) {
|
|
4396
|
+
const laneCount = 48 + layer * 36;
|
|
4397
|
+
const lane = Math.floor(((cellAngle + Math.PI) / (Math.PI * 2)) * laneCount);
|
|
4398
|
+
const laneAngle = (lane / laneCount) * Math.PI * 2 - Math.PI;
|
|
4399
|
+
angularDifference[layer][index] = Math.abs(Math.atan2(Math.sin(cellAngle - laneAngle), Math.cos(cellAngle - laneAngle)));
|
|
4400
|
+
const cellSeed = lane * 17 + layer * 91;
|
|
4401
|
+
seed[layer][index] = cellSeed;
|
|
4402
|
+
phase[layer][index] = hashNoise(cellSeed, layer, 0);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
const geometry = { angle, distance, maxDistance, angularDifference, seed, phase };
|
|
4407
|
+
rememberGeometry(starfieldGeometryCache, key, geometry);
|
|
4408
|
+
return geometry;
|
|
4409
|
+
}
|
|
4410
|
+
function rememberGeometry(cache, key, value) {
|
|
4411
|
+
if (cache.size >= STARFIELD_GEOMETRY_CACHE_LIMIT) {
|
|
4412
|
+
const oldest = cache.keys().next().value;
|
|
4413
|
+
if (oldest !== undefined)
|
|
4414
|
+
cache.delete(oldest);
|
|
4415
|
+
}
|
|
4416
|
+
cache.set(key, value);
|
|
4417
|
+
}
|
|
4313
4418
|
function winterPrecipitationSpeed(index) {
|
|
4314
4419
|
return 0.5 + hashNoise(index, 51, 0) * 0.8;
|
|
4315
4420
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { buildGallopFine } from './gallop.js';
|
|
2
|
+
import { panelBackground, themeAccent, themeContributionColors } from '../theme.js';
|
|
3
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
4
|
+
function blend(a, b, t) {
|
|
5
|
+
return '#' + [1, 3, 5].map(offset => Math.round(parseInt(a.slice(offset, offset + 2), 16) * (1 - t) + parseInt(b.slice(offset, offset + 2), 16) * t).toString(16).padStart(2, '0')).join('');
|
|
6
|
+
}
|
|
7
|
+
/** A cell canvas, not a bitmap: glyphs, whole-cell color and intentional empty space. */
|
|
8
|
+
function canvas(width, height, theme) {
|
|
9
|
+
const w = Math.max(1, Math.floor(width));
|
|
10
|
+
const h = Math.max(1, Math.floor(height));
|
|
11
|
+
const accent = themeAccent(theme);
|
|
12
|
+
const contributions = themeContributionColors(theme);
|
|
13
|
+
const ramp = [panelBackground, blend(panelBackground, contributions[1], 0.55),
|
|
14
|
+
...contributions.slice(1), blend(accent, '#ffffff', 0.36), '#ffffff'];
|
|
15
|
+
const cells = Array.from({ length: h }, () => Array.from({ length: w }, () => ({ text: ' ', color: accent })));
|
|
16
|
+
const paint = (x, y, text, level, background = false, color) => {
|
|
17
|
+
const row = Math.round(y);
|
|
18
|
+
const col = Math.round(x);
|
|
19
|
+
if (row < 0 || row >= h || col < 0 || col >= w)
|
|
20
|
+
return;
|
|
21
|
+
const ink = color ?? ramp[Math.max(0, Math.min(ramp.length - 1, Math.round(level)))];
|
|
22
|
+
cells[row][col] = background ? { text: ' ', color: ink, backgroundColor: ink } : { text, color: ink };
|
|
23
|
+
};
|
|
24
|
+
const finish = () => cells.map(row => {
|
|
25
|
+
const segments = [];
|
|
26
|
+
for (const cell of row) {
|
|
27
|
+
const last = segments.at(-1);
|
|
28
|
+
if (last && last.color === cell.color && last.backgroundColor === cell.backgroundColor)
|
|
29
|
+
last.text += cell.text;
|
|
30
|
+
else
|
|
31
|
+
segments.push({ ...cell });
|
|
32
|
+
}
|
|
33
|
+
return { text: row.map(c => c.text).join(''), color: accent, segments };
|
|
34
|
+
});
|
|
35
|
+
return { w, h, ramp, paint, finish };
|
|
36
|
+
}
|
|
37
|
+
// Interleaved wavefronts, folded through each other. Entire cells are the light source.
|
|
38
|
+
const crossfade = (pulse, width, height, theme) => {
|
|
39
|
+
const c = canvas(width, height, theme);
|
|
40
|
+
const t = pulse * 0.08;
|
|
41
|
+
const shades = Array.from({ length: 18 }, (_, i) => blend(c.ramp[2], c.ramp[5], i / 17));
|
|
42
|
+
for (let y = 0; y < c.h; y++) {
|
|
43
|
+
for (let x = 0; x < c.w; x++) {
|
|
44
|
+
const u = x / c.w * 2 - 1;
|
|
45
|
+
const v = y / c.h * 2 - 1;
|
|
46
|
+
const a = u * 8.5 + v * 3 + Math.sin(v * 3.4 - t * 0.7) * 2 - t * 1.1;
|
|
47
|
+
const b = v * 5 - u * 2 + Math.sin(u * 3 + t * 0.5) * 1.5 + t * 0.75;
|
|
48
|
+
const value = clamp(0.5 + 0.28 * Math.cos(a) + 0.24 * Math.cos(b));
|
|
49
|
+
c.paint(x, y, ' ', 0, true, shades[Math.round(value * 17)]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return c.finish();
|
|
53
|
+
};
|
|
54
|
+
export const terminalReceiverBuilders = {
|
|
55
|
+
crossfade,
|
|
56
|
+
'gallop-fine': buildGallopFine
|
|
57
|
+
};
|
package/dist/update-check.js
CHANGED
|
@@ -40,17 +40,26 @@ export function shouldCheckForUpdate(updateCheck, now = Date.now()) {
|
|
|
40
40
|
const checkedAt = Date.parse(updateCheck.checkedAt);
|
|
41
41
|
return !Number.isFinite(checkedAt) || now - checkedAt >= UPDATE_CHECK_INTERVAL_MS;
|
|
42
42
|
}
|
|
43
|
-
export function
|
|
43
|
+
export function automaticUpdateChecksAllowed(enabled = true) {
|
|
44
|
+
return enabled && process.env.RADIOCLI_DISABLE_UPDATE_CHECK !== '1' && process.env.CI !== 'true';
|
|
45
|
+
}
|
|
46
|
+
export function updateAvailableForVersion(updateCheck, currentVersion = updateCheck?.currentVersion) {
|
|
47
|
+
return Boolean(updateCheck?.updateAvailable &&
|
|
48
|
+
updateCheck.latestVersion &&
|
|
49
|
+
currentVersion &&
|
|
50
|
+
compareSemver(updateCheck.latestVersion, currentVersion) > 0);
|
|
51
|
+
}
|
|
52
|
+
export function updateStatusText(updateCheck, currentVersion) {
|
|
44
53
|
if (!updateCheck) {
|
|
45
54
|
return 'not checked yet';
|
|
46
55
|
}
|
|
47
|
-
if (updateCheck
|
|
56
|
+
if (updateAvailableForVersion(updateCheck, currentVersion) && updateCheck.latestVersion) {
|
|
48
57
|
return `v${updateCheck.latestVersion} available`;
|
|
49
58
|
}
|
|
50
59
|
if (updateCheck.error) {
|
|
51
60
|
return `check failed: ${updateCheck.error}`;
|
|
52
61
|
}
|
|
53
|
-
return updateCheck.latestVersion ? `current at v${updateCheck.latestVersion}` : 'not checked yet';
|
|
62
|
+
return updateCheck.latestVersion ? `current at v${currentVersion ?? updateCheck.latestVersion}` : 'not checked yet';
|
|
54
63
|
}
|
|
55
64
|
export function updateCommandForInstall(entryPath = process.argv[1]) {
|
|
56
65
|
const resolved = resolvePath(entryPath);
|
|
@@ -58,16 +67,21 @@ export function updateCommandForInstall(entryPath = process.argv[1]) {
|
|
|
58
67
|
if (/\/(?:opt\/homebrew|usr\/local)\/(?:Cellar|Homebrew)\//.test(haystack) || /\/\.linuxbrew\/(?:Cellar|Homebrew)\//.test(haystack)) {
|
|
59
68
|
return { method: 'homebrew', command: 'brew update && brew upgrade radiocli' };
|
|
60
69
|
}
|
|
61
|
-
if (
|
|
70
|
+
if (/[\\/](?:pnpm|\.pnpm-global)[\\/]/.test(haystack)) {
|
|
71
|
+
return { method: 'pnpm', command: 'pnpm add -g @ciphore/radiocli@latest' };
|
|
72
|
+
}
|
|
73
|
+
if (/[\\/]\.bun[\\/]install[\\/]global[\\/]/.test(haystack)) {
|
|
74
|
+
return { method: 'bun', command: 'bun add -g @ciphore/radiocli@latest' };
|
|
75
|
+
}
|
|
76
|
+
if (/[\\/]node_modules[\\/]@ciphore[\\/]radiocli[\\/]/.test(haystack)) {
|
|
62
77
|
return { method: 'npm', command: 'npm install -g @ciphore/radiocli@latest' };
|
|
63
78
|
}
|
|
64
79
|
return { method: 'unknown', command: 'npm install -g @ciphore/radiocli@latest' };
|
|
65
80
|
}
|
|
66
81
|
export function installUpdate(command = updateCommandForInstall().command) {
|
|
67
82
|
return new Promise(resolve => {
|
|
68
|
-
const shell = process.platform
|
|
69
|
-
const
|
|
70
|
-
const child = spawn(shell, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
83
|
+
const shell = updateShellForPlatform(process.platform, command);
|
|
84
|
+
const child = spawn(shell.command, shell.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
71
85
|
const chunks = [];
|
|
72
86
|
child.stdout.on('data', chunk => chunks.push(Buffer.from(chunk)));
|
|
73
87
|
child.stderr.on('data', chunk => chunks.push(Buffer.from(chunk)));
|
|
@@ -80,6 +94,11 @@ export function installUpdate(command = updateCommandForInstall().command) {
|
|
|
80
94
|
});
|
|
81
95
|
});
|
|
82
96
|
}
|
|
97
|
+
export function updateShellForPlatform(platform, command) {
|
|
98
|
+
return platform === 'win32'
|
|
99
|
+
? { command: 'cmd.exe', args: ['/d', '/s', '/c', command] }
|
|
100
|
+
: { command: 'sh', args: ['-lc', command] };
|
|
101
|
+
}
|
|
83
102
|
export function compareSemver(left, right) {
|
|
84
103
|
const leftParts = semverParts(left);
|
|
85
104
|
const rightParts = semverParts(right);
|