@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +133 -1
  2. package/README.md +76 -6
  3. package/dist/agent/alarm-service.js +210 -0
  4. package/dist/agent/cli.js +193 -0
  5. package/dist/agent/headless-host.js +143 -0
  6. package/dist/agent/launcher.js +71 -0
  7. package/dist/agent/mcp-install.js +467 -0
  8. package/dist/agent/mcp-server.js +139 -0
  9. package/dist/agent/service.js +347 -0
  10. package/dist/agent/session.js +248 -0
  11. package/dist/alarms/active-session.js +183 -0
  12. package/dist/alarms/cli.js +312 -0
  13. package/dist/alarms/guard.js +343 -0
  14. package/dist/alarms/inhibitor.js +48 -0
  15. package/dist/alarms/power-guard-store.js +169 -0
  16. package/dist/alarms/runner.js +342 -0
  17. package/dist/alarms/runtime-health.js +79 -0
  18. package/dist/alarms/schedule.js +149 -0
  19. package/dist/alarms/scheduler.js +250 -0
  20. package/dist/alarms/setup-verification.js +187 -0
  21. package/dist/alarms/system-volume.js +43 -0
  22. package/dist/alarms/terminal-launcher.js +181 -0
  23. package/dist/alarms/tui-presence.js +38 -0
  24. package/dist/cli.js +113 -5
  25. package/dist/player/backend-install.js +2 -1
  26. package/dist/player/command-diagnostics.js +27 -0
  27. package/dist/player/command.js +123 -62
  28. package/dist/player/player-controller.js +32 -2
  29. package/dist/providers/provider-manager.js +5 -0
  30. package/dist/providers/radio-browser.js +36 -6
  31. package/dist/setup.js +462 -0
  32. package/dist/storage/store.js +262 -2
  33. package/dist/types.js +6 -0
  34. package/dist/ui/AdaptiveContent.js +111 -26
  35. package/dist/ui/App.js +401 -67
  36. package/dist/ui/AppContent.js +24 -7
  37. package/dist/ui/adaptive-explore-layout.js +47 -0
  38. package/dist/ui/alarm-editor.js +174 -0
  39. package/dist/ui/alarm-tui-service.js +84 -0
  40. package/dist/ui/app-state.js +3 -0
  41. package/dist/ui/ascii.js +8 -0
  42. package/dist/ui/components/AdaptiveMarquee.js +28 -0
  43. package/dist/ui/components/StationList.js +10 -5
  44. package/dist/ui/components/VersionIndicator.js +19 -0
  45. package/dist/ui/cosmo-world-map.js +5 -2
  46. package/dist/ui/explore-map-layout.js +18 -6
  47. package/dist/ui/format.js +21 -0
  48. package/dist/ui/help-content.js +14 -2
  49. package/dist/ui/layout.js +1 -1
  50. package/dist/ui/page-footer.js +120 -2
  51. package/dist/ui/receiver-animation.js +68 -0
  52. package/dist/ui/screen-items.js +41 -9
  53. package/dist/ui/screen-meta.js +4 -0
  54. package/dist/ui/screens/AlarmsScreen.js +202 -0
  55. package/dist/ui/screens/CountriesScreen.js +8 -5
  56. package/dist/ui/screens/ExploreScreen.js +8 -3
  57. package/dist/ui/screens/HomeScreen.js +3 -1
  58. package/dist/ui/screens/NowPlayingScreen.js +6 -2
  59. package/dist/ui/screens/SettingsScreen.js +70 -53
  60. package/dist/ui/screens/StationScreen.js +3 -2
  61. package/dist/ui/selection-state.js +10 -0
  62. package/dist/ui/terminal-mouse.js +18 -3
  63. package/dist/ui/use-alarm-tui.js +727 -0
  64. package/dist/ui/use-app-input.js +107 -46
  65. package/dist/ui/visualizers/gallop.js +118 -0
  66. package/dist/ui/visualizers/horse-stride.js +20 -0
  67. package/dist/ui/visualizers/receiver-style-registry.js +14 -7
  68. package/dist/ui/visualizers/receiver-visualizers.js +233 -128
  69. package/dist/ui/visualizers/retro-receivers.js +4 -0
  70. package/dist/ui/visualizers/terminal-receivers.js +57 -0
  71. package/dist/update-check.js +26 -7
  72. package/package.json +6 -1
@@ -1,23 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
2
3
  import { Box, Text } from 'ink';
3
4
  import { computeListeningStats } from '../activity/stats.js';
4
5
  import { playbackBackendLabel } from '../player/backend-install.js';
5
6
  import { visibleWindow } from './list-window.js';
6
7
  import { displayWidth, padDisplayEnd, stationLocation, stationTags, stationTech, truncate } from './format.js';
7
- import { homeItems, settingsItems, settingsSectionFor } from './screen-items.js';
8
+ import { homeItems, settingsGroup, settingsGroups, settingsItemsForPage } from './screen-items.js';
8
9
  import { screenTitle } from './screen-meta.js';
9
10
  import { keyHelpSections, commandHelp } from './help-content.js';
10
11
  import { settingLabel, settingValue } from './screens/SettingsScreen.js';
11
- import { panelBorder, textDim, textMuted, themeAccent } from './theme.js';
12
+ import { exploreMapLand, mapMarker, panelBorder, textDim, textMuted, themeAccent } from './theme.js';
12
13
  import { panelBorderStyle, useDisplay } from './display-context.js';
13
14
  import { toAsciiSafe } from './ascii.js';
14
15
  import { buildVisualizer, visualizerHeight } from './visualizers/receiver-visualizers.js';
15
16
  import { Logo } from './components/Logo.js';
17
+ import { useReceiverPulse } from './receiver-animation.js';
18
+ import { formatExploreCursor } from './app-state.js';
19
+ import { buildCosmoWorldMap } from './cosmo-world-map.js';
20
+ import { adaptiveExploreFrameMetrics, computeAdaptiveExploreLayout } from './adaptive-explore-layout.js';
21
+ import { AdaptiveMarquee } from './components/AdaptiveMarquee.js';
16
22
  export function AdaptiveContent(props) {
17
23
  return _jsx(AdaptiveContentBody, { ...props });
18
24
  }
19
25
  function AdaptiveContentBody(props) {
20
- const { mode, screen, selected, height, width, theme, playback, playingStation, nowPlaying, stations, countries, airPlayDevices, airPlayCode, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, library, diagnostics, backends, updateCheck, favoriteKeys, stationTitle, pulse, } = props;
26
+ const { mode, screen, settingsPage = 'root', selected, height, width, theme, playback, playingStation, nowPlaying, stations, countries, airPlayDevices, airPlayCode, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, exploreCursor, library, diagnostics, backends, updateCheck, appVersion, favoriteKeys, stationTitle, } = props;
21
27
  const accent = themeAccent(theme);
22
28
  const { ascii } = useDisplay();
23
29
  const { bodyRows } = adaptiveFrameMetrics(mode, height);
@@ -27,11 +33,15 @@ function AdaptiveContentBody(props) {
27
33
  return (_jsx(AdaptiveHome, { mode: mode, selected: selected, height: height, width: width, theme: theme, library: library }));
28
34
  }
29
35
  if (screen === 'now-playing') {
30
- return (_jsx(AdaptiveNowPlaying, { mode: mode, station: playingStation, playback: playback, metadata: nowPlaying, pulse: pulse, width: width, height: height, ascii: ascii, theme: theme, receiverStyle: library.settings.receiverStyle }));
36
+ return (_jsx(AdaptiveNowPlaying, { mode: mode, station: playingStation, playback: playback, metadata: nowPlaying, width: width, height: height, ascii: ascii, theme: theme, receiverStyle: library.settings.receiverStyle, reduceMotion: Boolean(library.settings.reduceMotion) }));
37
+ }
38
+ if (screen === 'explore') {
39
+ return (_jsx(AdaptiveExplore, { mode: mode, width: width, height: height, theme: theme, stations: stations, selected: selected, favorites: favoriteKeys, cursor: exploreCursor, loading: loadingStations, error: props.stationError, ascii: ascii, reduceMotion: Boolean(library.settings.reduceMotion) }));
31
40
  }
32
41
  if (screen === 'search') {
33
42
  const rows = adaptiveRows({
34
43
  screen,
44
+ settingsPage,
35
45
  stations,
36
46
  countries,
37
47
  airPlayDevices,
@@ -39,6 +49,7 @@ function AdaptiveContentBody(props) {
39
49
  diagnostics,
40
50
  backends,
41
51
  updateCheck,
52
+ appVersion,
42
53
  favoriteKeys,
43
54
  selected,
44
55
  width,
@@ -53,9 +64,10 @@ function AdaptiveContentBody(props) {
53
64
  loadingCountries,
54
65
  loadingStations,
55
66
  nearbyEnabled: library.settings.enableNearbyLocation,
56
- stationTitle
67
+ stationTitle,
68
+ stationError: props.stationError
57
69
  });
58
- return (_jsx(AdaptiveSearch, { mode: mode, query: searchQuery, editing: editingSearch, loading: loadingStations, rows: rows, empty: empty, selected: selected, height: height, width: width, theme: theme, ascii: ascii }));
70
+ return (_jsx(AdaptiveSearch, { mode: mode, query: searchQuery, editing: editingSearch, loading: loadingStations, rows: rows, empty: empty, selected: selected, height: height, width: width, theme: theme, ascii: ascii, reduceMotion: Boolean(library.settings.reduceMotion) }));
59
71
  }
60
72
  if (screen === 'stats') {
61
73
  const stats = computeListeningStats(library.activity.sessions);
@@ -78,6 +90,7 @@ function AdaptiveContentBody(props) {
78
90
  }
79
91
  const rows = adaptiveRows({
80
92
  screen,
93
+ settingsPage,
81
94
  stations,
82
95
  countries,
83
96
  airPlayDevices,
@@ -85,6 +98,7 @@ function AdaptiveContentBody(props) {
85
98
  diagnostics,
86
99
  backends,
87
100
  updateCheck,
101
+ appVersion,
88
102
  favoriteKeys,
89
103
  selected,
90
104
  width,
@@ -99,9 +113,55 @@ function AdaptiveContentBody(props) {
99
113
  loadingCountries,
100
114
  loadingStations,
101
115
  nearbyEnabled: library.settings.enableNearbyLocation,
102
- stationTitle
116
+ stationTitle,
117
+ stationError: props.stationError
103
118
  });
104
- return (_jsx(AdaptiveFrame, { title: title, status: status, mode: mode, height: height, width: width, theme: theme, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: bodyRows, width: width, theme: theme })) : (_jsx(StaticRows, { rows: empty.slice(0, bodyRows), width: width, theme: theme })) }));
119
+ return (_jsx(AdaptiveFrame, { title: title, status: status, mode: mode, height: height, width: width, theme: theme, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: bodyRows, width: width, theme: theme, reduceMotion: Boolean(library.settings.reduceMotion) })) : (_jsx(StaticRows, { rows: empty.slice(0, bodyRows), width: width, theme: theme })) }));
120
+ }
121
+ function AdaptiveExplore({ mode, width, height, theme, stations, selected, favorites, cursor, loading, error, ascii, reduceMotion }) {
122
+ const accent = themeAccent(theme);
123
+ const { headerRows, headerGap, bodyRows } = adaptiveExploreFrameMetrics(mode, height);
124
+ const layout = computeAdaptiveExploreLayout(mode, width, Math.max(1, bodyRows));
125
+ const marker = React.useMemo(() => [{ lat: cursor.latitude, lon: cursor.longitude, selected: true }], [cursor.latitude, cursor.longitude]);
126
+ const map = React.useMemo(() => buildCosmoWorldMap(layout.mapColumns, layout.mapRows, marker), [layout.mapColumns, layout.mapRows, marker]);
127
+ const coordinate = formatExploreCursor(cursor);
128
+ const status = `${coordinate} · ${loading ? 'loading' : `${stations.length.toLocaleString()} stations`}`;
129
+ const title = mode === 'micro' ? `RC / Explore · ${coordinate}` : 'RADIOCLI Explore';
130
+ const stationRows = stationAdaptiveRows(stations, favorites, selected, layout.listWidth, ascii);
131
+ const emptyRows = loading
132
+ ? [{ key: 'loading', label: 'Loading stations…' }]
133
+ : error
134
+ ? [{ key: 'error', label: error }]
135
+ : [{ key: 'empty', label: 'No stations near this point.' }];
136
+ const list = (_jsx(AdaptiveList, { rows: stationRows.length > 0 ? stationRows : emptyRows, selected: stationRows.length > 0 ? selected : 0, pageSize: layout.listRows, width: layout.listWidth, theme: theme, reduceMotion: reduceMotion }));
137
+ return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: truncate(title, width) }), headerRows > 1 ? _jsx(Text, { color: textMuted, children: truncate(status, width) }) : null, headerGap ? _jsx(Box, { height: headerGap, flexShrink: 0 }) : null, bodyRows > 0 ? layout.split ? (_jsxs(Box, { flexDirection: "row", height: bodyRows, width: width, overflow: "hidden", children: [_jsx(AdaptiveCosmoMap, { rows: map, width: layout.mapAreaWidth, height: layout.mapRows, offsetX: layout.mapOffsetX, theme: theme, ascii: ascii }), layout.gap ? _jsx(Box, { width: layout.gap, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", width: layout.listWidth, height: layout.listRows, overflow: "hidden", children: list })] })) : (_jsxs(Box, { flexDirection: "column", height: bodyRows, width: width, overflow: "hidden", children: [_jsx(AdaptiveCosmoMap, { rows: map, width: layout.mapAreaWidth, height: layout.mapRows, offsetX: layout.mapOffsetX, theme: theme, ascii: ascii }), layout.gap ? _jsx(Box, { height: layout.gap, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: layout.listRows, width: layout.listWidth, overflow: "hidden", children: list })] })) : null] }));
138
+ }
139
+ function AdaptiveCosmoMap({ rows, width, height, offsetX, theme, ascii }) {
140
+ return (_jsx(Box, { flexDirection: "column", width: width, height: height, overflow: "hidden", flexShrink: 0, children: rows.map((row, rowIndex) => {
141
+ const chunks = [];
142
+ for (const cell of row.cells) {
143
+ const previous = chunks.at(-1);
144
+ if (previous?.kind === cell.kind)
145
+ previous.text += cell.char;
146
+ else
147
+ chunks.push({ kind: cell.kind, text: cell.char });
148
+ }
149
+ let cellOffset = 0;
150
+ return (_jsx(Box, { marginLeft: offsetX, children: chunks.map(chunk => {
151
+ const key = `${cellOffset}-${chunk.kind}`;
152
+ cellOffset += chunk.text.length;
153
+ return (_jsx(Text, { color: adaptiveMapColor(chunk.kind, theme), children: ascii ? toAsciiSafe(chunk.text) : chunk.text }, key));
154
+ }) }, rowIndex));
155
+ }) }));
156
+ }
157
+ function adaptiveMapColor(kind, theme) {
158
+ if (kind === 'selected')
159
+ return themeAccent(theme);
160
+ if (kind === 'marker')
161
+ return mapMarker;
162
+ if (kind === 'land')
163
+ return exploreMapLand;
164
+ return undefined;
105
165
  }
106
166
  function AdaptiveFrame({ title, status, mode, height, width, theme, children }) {
107
167
  const accent = themeAccent(theme);
@@ -134,7 +194,7 @@ function AdaptiveHome({ mode, selected, height, width, theme, library }) {
134
194
  return (_jsxs(Text, { color: active ? accent : undefined, bold: active, children: [active ? '> ' : ' ', absoluteIndex + 1, " ", truncate(`${item.label}${detail}`, Math.max(1, width - 4))] }, item.screen));
135
195
  }) }), showSummary ? (_jsx(Text, { color: textMuted, children: truncate(`${library.recent.length} recent · ${library.favorites.length} favorites · ${library.imported.length} imported`, width) })) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
136
196
  }
137
- function AdaptiveList({ rows, selected, pageSize, width, theme }) {
197
+ function AdaptiveList({ rows, selected, pageSize, width, theme, reduceMotion }) {
138
198
  const selectedIndex = Math.min(Math.max(selected, 0), Math.max(0, rows.length - 1));
139
199
  const window = visibleWindow(rows, selectedIndex, Math.max(1, pageSize));
140
200
  const accent = themeAccent(theme);
@@ -143,14 +203,17 @@ function AdaptiveList({ rows, selected, pageSize, width, theme }) {
143
203
  const active = absoluteIndex === selectedIndex && !row.heading;
144
204
  const prefix = row.heading ? ' ' : active ? '> ' : ' ';
145
205
  const detail = row.detail ? `${row.separator ?? ' · '}${row.detail}` : '';
146
- return (_jsxs(Text, { color: active ? accent : row.heading ? textMuted : undefined, bold: active || row.heading, children: [prefix, truncate(`${row.label}${detail}`, Math.max(0, width - 2))] }, row.key));
206
+ const text = `${row.label}${detail}`;
207
+ const favoriteWidth = row.favoriteGlyph ? 2 : 0;
208
+ return (_jsxs(Text, { color: active ? accent : row.heading ? textMuted : undefined, bold: active || row.heading, children: [prefix, _jsx(AdaptiveMarquee, { text: text, width: Math.max(0, width - 2 - favoriteWidth), active: active && Boolean(row.marquee), reduceMotion: reduceMotion }), row.favoriteGlyph ? _jsxs(Text, { color: "yellow", children: [" ", row.favoriteGlyph] }) : null] }, row.key));
147
209
  }) }));
148
210
  }
149
211
  function StaticRows({ rows, width, theme }) {
150
212
  const accent = themeAccent(theme);
151
213
  return (_jsx(Box, { flexDirection: "column", children: rows.map(row => (_jsxs(Text, { color: row.heading ? textMuted : undefined, bold: row.heading, children: [truncate(row.detail ? `${row.label} · ${row.detail}` : row.label, width), row.key === 'activity' ? _jsx(Text, { color: accent }) : null] }, row.key))) }));
152
214
  }
153
- function AdaptiveNowPlaying({ mode, station, playback, metadata, pulse, width, height, ascii, theme, receiverStyle }) {
215
+ function AdaptiveNowPlaying({ mode, station, playback, metadata, width, height, ascii, theme, receiverStyle, reduceMotion }) {
216
+ const pulse = useReceiverPulse();
154
217
  const accent = themeAccent(theme);
155
218
  const stationName = station?.name ?? 'No station tuned';
156
219
  const showMetadata = mode === 'compact' && height >= 9 && Boolean(metadata?.title);
@@ -164,21 +227,23 @@ function AdaptiveNowPlaying({ mode, station, playback, metadata, pulse, width, h
164
227
  ? `${stationName} · ${playback.state}`
165
228
  : 'RADIOCLI Now playing';
166
229
  const status = `${stationName} ${playback.state}${playback.muted ? ' · muted' : ` · vol ${playback.volume}`}`;
167
- return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: truncate(header, width) }), mode === 'compact' ? _jsx(Text, { color: textMuted, children: truncate(status, width) }) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: availableVisualRows, overflow: "hidden", children: visualRows.map((row, index) => (_jsx(Text, { color: row.segments ? undefined : row.color, children: row.segments
230
+ return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: mode === 'micro'
231
+ ? _jsx(AdaptiveMarquee, { text: header, width: width, active: true, reduceMotion: reduceMotion })
232
+ : truncate(header, width) }), mode === 'compact' ? _jsx(Text, { color: textMuted, children: truncate(status, width) }) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: availableVisualRows, overflow: "hidden", children: visualRows.map((row, index) => (_jsx(Text, { color: row.segments ? undefined : row.color, children: row.segments
168
233
  ? renderAdaptiveSegments(row.segments, ascii)
169
234
  : ascii
170
235
  ? toAsciiSafe(row.text)
171
- : row.text }, index))) }), showMetadata ? _jsx(Text, { color: accent, children: truncate(metadata?.title ?? '', width) }) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
236
+ : row.text }, index))) }), showMetadata ? (_jsx(Text, { color: accent, children: _jsx(AdaptiveMarquee, { text: metadata?.title ?? '', width: width, active: true, reduceMotion: reduceMotion }) })) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
172
237
  }
173
238
  function renderAdaptiveSegments(segments, ascii) {
174
239
  let offset = 0;
175
240
  return segments.map(segment => {
176
- const key = `${offset}-${segment.color}-${segment.backgroundColor ?? ''}`;
241
+ const key = offset;
177
242
  offset += segment.text.length;
178
243
  return (_jsx(Text, { color: segment.color, backgroundColor: segment.backgroundColor, bold: segment.bold, children: ascii ? toAsciiSafe(segment.text) : segment.text }, key));
179
244
  });
180
245
  }
181
- function AdaptiveSearch({ mode, query, editing, loading, rows, empty, selected, height, width, theme, ascii }) {
246
+ function AdaptiveSearch({ mode, query, editing, loading, rows, empty, selected, height, width, theme, ascii, reduceMotion }) {
182
247
  const accent = themeAccent(theme);
183
248
  const { panel: panelBackground } = useDisplay();
184
249
  const fieldText = query || 'station, genre, or place';
@@ -186,7 +251,7 @@ function AdaptiveSearch({ mode, query, editing, loading, rows, empty, selected,
186
251
  const fieldRows = height >= 4 ? 3 : 1;
187
252
  const gapRows = height >= 6 ? 1 : 0;
188
253
  const listRows = Math.max(0, height - 1 - fieldRows - gapRows * 2);
189
- return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: mode === 'micro' ? 'SEARCH' : 'RADIOCLI Search' }), fieldRows === 3 ? (_jsxs(Box, { borderStyle: panelBorderStyle(ascii, 'single'), borderColor: editing || loading ? accent : panelBorder, borderBackgroundColor: panelBackground, backgroundColor: panelBackground, width: width, height: 3, flexShrink: 0, children: [_jsx(Text, { color: editing || loading ? accent : textMuted, children: prefix }), _jsx(Text, { color: query ? accent : textMuted, children: truncate(fieldText, Math.max(1, width - 5)) })] })) : (_jsx(Text, { color: query ? accent : textMuted, children: truncate(`[${prefix}${fieldText}]`, width) })), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: listRows, overflow: "hidden", flexShrink: 0, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: listRows, width: width, theme: theme })) : (_jsx(StaticRows, { rows: empty.slice(0, listRows), width: width, theme: theme })) }), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
254
+ return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: mode === 'micro' ? 'SEARCH' : 'RADIOCLI Search' }), fieldRows === 3 ? (_jsxs(Box, { borderStyle: panelBorderStyle(ascii, 'single'), borderColor: editing || loading ? accent : panelBorder, borderBackgroundColor: panelBackground, backgroundColor: panelBackground, width: width, height: 3, flexShrink: 0, children: [_jsx(Text, { color: editing || loading ? accent : textMuted, children: prefix }), _jsx(Text, { color: query ? accent : textMuted, children: truncate(fieldText, Math.max(1, width - 5)) })] })) : (_jsx(Text, { color: query ? accent : textMuted, children: truncate(`[${prefix}${fieldText}]`, width) })), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: listRows, overflow: "hidden", flexShrink: 0, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: listRows, width: width, theme: theme, reduceMotion: reduceMotion })) : (_jsx(StaticRows, { rows: empty.slice(0, listRows), width: width, theme: theme })) }), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
190
255
  }
191
256
  function adaptiveRows(input) {
192
257
  const { screen, stations, countries, airPlayDevices, library, diagnostics, backends, updateCheck, favoriteKeys, selected, width, mode, ascii } = input;
@@ -194,16 +259,20 @@ function adaptiveRows(input) {
194
259
  return homeItems.map(item => ({
195
260
  key: item.screen,
196
261
  label: item.label,
197
- detail: mode === 'compact' && width >= 52 ? item.detail : undefined
262
+ detail: mode === 'compact' && width >= 52 ? item.detail : undefined,
263
+ separator: ' '
198
264
  }));
199
265
  }
200
266
  if (screen === 'settings') {
201
- const labels = settingsItems.map(item => settingLabel(item, updateCheck));
267
+ const pageItems = settingsItemsForPage(input.settingsPage);
268
+ const labels = pageItems.map(item => settingLabel(item, updateCheck, input.appVersion));
202
269
  const labelWidth = pairedColumnWidth(labels, width, mode);
203
- return settingsItems.map((item, index) => ({
270
+ return pageItems.map((item, index) => ({
204
271
  key: item,
205
- label: padDisplayEnd(truncate(settingLabel(item, updateCheck), labelWidth), labelWidth),
206
- detail: settingValue(item, library.settings, diagnostics, backends, airPlayDevices, updateCheck),
272
+ label: padDisplayEnd(truncate(settingLabel(item, updateCheck, input.appVersion), labelWidth), labelWidth),
273
+ detail: input.settingsPage === 'root'
274
+ ? adaptiveSettingsRootValue(item)
275
+ : settingValue(item, library.settings, diagnostics, backends, airPlayDevices, updateCheck, input.appVersion),
207
276
  separator: ' ',
208
277
  index
209
278
  }));
@@ -216,6 +285,7 @@ function adaptiveRows(input) {
216
285
  label: padDisplayEnd(truncate(`${country.name} (${country.code})`, labelWidth), labelWidth),
217
286
  detail: country.stationCount.toLocaleString(),
218
287
  separator: ' ',
288
+ marquee: true,
219
289
  index
220
290
  }));
221
291
  }
@@ -239,14 +309,23 @@ function adaptiveRows(input) {
239
309
  }
240
310
  return rows;
241
311
  }
312
+ return stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii);
313
+ }
314
+ function adaptiveSettingsRootValue(item) {
315
+ const group = settingsGroups.find(candidate => candidate.label === item);
316
+ return group ? `${group.items.length} settings ›` : undefined;
317
+ }
318
+ function stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii) {
242
319
  return stations.map((station, index) => {
243
320
  const favorite = favoriteKeys.has(`${station.provider}:${station.id}`);
244
321
  const standardMetadata = `${stationLocation(station)} · ${stationTech(station)}`;
245
322
  const selectedMetadata = station.tags.length > 0 ? stationTags(station) : standardMetadata;
246
323
  return {
247
324
  key: `${station.provider}:${station.id}`,
248
- label: `${station.name}${favorite ? (ascii ? ' *' : ' ★') : ''}`,
249
- detail: width >= 42 ? (index === selected ? selectedMetadata : standardMetadata) : undefined,
325
+ label: station.name,
326
+ detail: index === selected ? selectedMetadata : width >= 42 ? standardMetadata : undefined,
327
+ marquee: true,
328
+ favoriteGlyph: favorite ? (ascii ? '*' : '★') : undefined,
250
329
  index
251
330
  };
252
331
  });
@@ -258,7 +337,7 @@ function pairedColumnWidth(labels, width, mode) {
258
337
  return Math.min(longest, cap);
259
338
  }
260
339
  function adaptiveEmptyState(input) {
261
- const { screen, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, nearbyEnabled, stationTitle } = input;
340
+ const { screen, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, nearbyEnabled, stationTitle, stationError } = input;
262
341
  if ((screen === 'countries' || screen === 'map') && loadingCountries) {
263
342
  return [{ key: 'loading', label: 'Loading countries…' }];
264
343
  }
@@ -286,13 +365,16 @@ function adaptiveEmptyState(input) {
286
365
  if (loadingStations) {
287
366
  return [{ key: 'loading', label: 'Loading stations…' }];
288
367
  }
368
+ if (stationError) {
369
+ return [{ key: 'error', label: stationError }, { key: 'hint', label: 'Try again later or reopen this view.' }];
370
+ }
289
371
  if (screen === 'airplay-settings') {
290
372
  return [{ key: 'empty', label: 'No AirPlay receivers found.' }, { key: 'hint', label: 'Press r to scan again.' }];
291
373
  }
292
374
  return [{ key: 'empty', label: `No stations in ${stationTitle.toLowerCase()}.` }, { key: 'hint', label: 'Try another view or clear filters.' }];
293
375
  }
294
376
  function adaptiveStatus(props) {
295
- const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel, selected } = props;
377
+ const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel } = props;
296
378
  if (screen === 'home') {
297
379
  return `${library.favorites.length} favorites · ${library.recent.length} recent · ${library.imported.length} imported`;
298
380
  }
@@ -300,7 +382,10 @@ function adaptiveStatus(props) {
300
382
  return `${playbackBackendLabel(playback.backend)} · ${playback.state} · vol ${playback.volume}`;
301
383
  }
302
384
  if (screen === 'settings') {
303
- return `${settingsSectionFor(settingsItems[selected])} · Enter changes the selected setting`;
385
+ const group = settingsGroup(props.settingsPage ?? 'root');
386
+ return group
387
+ ? `${group.label} · Enter changes setting · b categories`
388
+ : 'Choose a category · Updates are available here';
304
389
  }
305
390
  if (screen === 'search') {
306
391
  const query = searchQuery || (editingSearch ? 'type to search' : 'press / to search');