@ciphore/radiocli 0.1.3 → 0.1.5

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.
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { airPlayAvailability, airPlayDeviceDetail, selectedAirPlayDevice } from '../airplay-settings.js';
4
+ import { audioOutputLabel } from '../audio-output.js';
5
+ import { Menu, Pointer } from '../components/Menu.js';
6
+ import { ScreenHeader } from '../components/ScreenHeader.js';
7
+ import { truncate } from '../format.js';
8
+ import { textDim, themeAccent } from '../theme.js';
9
+ export function AirPlaySettingsScreen({ selected, settings, backends, devices, theme, width }) {
10
+ const accent = themeAccent(theme);
11
+ const availability = airPlayAvailability(backends, devices);
12
+ const selectedDevice = selectedAirPlayDevice(settings, devices);
13
+ const selectedMissing = Boolean(settings.preferredAirPlayDevice && !selectedDevice);
14
+ const canEnterCode = availability.ready && selectedDevice?.requiresPassword && !selectedDevice.local;
15
+ const lineWidth = Math.max(24, width - 4);
16
+ const preferredOutput = audioOutputLabel(settings.preferredBackend);
17
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "AirPlay", subtitle: "Choose where AirPlay playback should go", right: availability.label, width: width, theme: theme }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: ["Audio output: ", _jsx(Text, { color: accent, children: preferredOutput }), " \u00B7 Streaming:", ' ', _jsx(Text, { color: availability.ready ? accent : 'yellow', children: availability.ready ? 'ready' : 'unavailable' }), " \u00B7 Receivers:", ' ', _jsx(Text, { color: accent, children: devices.length })] }), _jsx(Text, { color: availability.ready ? 'gray' : 'yellow', children: truncate(availability.detail, lineWidth) }), _jsxs(Text, { color: "gray", children: ["Selected: ", _jsx(Text, { color: selectedDevice ? accent : textDim, children: truncate(selectedDevice?.name ?? 'none', Math.max(4, lineWidth - 10)) })] }), availability.ready && selectedDevice?.requiresPassword && !selectedDevice.local ? (_jsxs(Text, { color: "gray", children: ["Code: ", _jsx(Text, { color: accent, children: "if the receiver shows a code while tuning, RadioCLI will ask for it" })] })) : null, selectedMissing ? (_jsxs(Text, { color: "yellow", children: ["Saved receiver is not visible: ", truncate(settings.preferredAirPlayDevice ?? '', Math.max(8, lineWidth - 31))] })) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: "Receivers" }), devices.length > 0 ? (_jsx(Menu, { items: devices, selected: selected, keyFor: device => device.id, render: (device, _index, active) => {
18
+ const isSelected = device.id === settings.preferredAirPlayDevice;
19
+ const name = truncate(device.name, Math.max(8, Math.floor(lineWidth * 0.35)));
20
+ const selectedLabel = isSelected ? ' selected' : '';
21
+ const detail = truncate(airPlayDeviceDetail(device), Math.max(12, lineWidth - name.length - selectedLabel.length - 5));
22
+ return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? accent : undefined, bold: active, children: name }), isSelected ? _jsx(Text, { color: accent, children: selectedLabel }) : null, _jsxs(Text, { color: "gray", children: [" \u00B7 ", detail] })] }));
23
+ } })) : (_jsx(Text, { color: "gray", children: "No receivers discovered." })), _jsx(Text, { color: textDim, children: canEnterCode ? 'Enter selects · c opens code entry · r refreshes receivers' : 'Enter selects · r refreshes receivers' })] })] }));
24
+ }
@@ -5,7 +5,9 @@ import { ScreenHeader } from '../components/ScreenHeader.js';
5
5
  import { truncate } from '../format.js';
6
6
  import { settingsItems } from '../screen-items.js';
7
7
  import { themeAccent } from '../theme.js';
8
- import { playbackBackendCapabilities, playbackBackendLabel } from '../../player/backend-install.js';
8
+ import { playbackBackendCapabilities } from '../../player/backend-install.js';
9
+ import { airPlayReceiverSettingValue } from '../airplay-settings.js';
10
+ import { audioOutputLabel, audioOutputSettingValue } from '../audio-output.js';
9
11
  export function SettingsScreen({ selected, settings, storePath, playback, backends, airPlayDevices, providerHealth, theme, diagnostics, width }) {
10
12
  const accent = themeAccent(theme);
11
13
  const lineWidth = Math.max(32, width - 4);
@@ -13,7 +15,7 @@ export function SettingsScreen({ selected, settings, storePath, playback, backen
13
15
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Settings", subtitle: "Enter changes the highlighted setting \u00B7 shortcuts in the footer", width: width, theme: theme }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Menu, { items: settingsItems, selected: selected, keyFor: item => item, render: (item, _index, active) => {
14
16
  const value = settingValue(item, settings, diagnostics, backends, airPlayDevices);
15
17
  return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? accent : undefined, bold: active, children: item }), value ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: accent, children: value })] })) : null] }));
16
- } }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: "Status" }), _jsxs(Text, { color: "gray", children: ["Player: ", _jsx(Text, { color: accent, children: playbackBackendLabel(playback.backend) }), " / ", playback.state, " \u00B7", ' ', diagnostics.muted ? 'muted' : `vol ${diagnostics.volume}`, " \u00B7 tune timeout ", settings.tuneTimeoutSeconds, "s"] }), _jsxs(Text, { color: "gray", children: ["Provider health: ", health.length ? health.map(([provider, status]) => `${provider} ${status}`).join(' · ') : 'not checked yet'] }), _jsxs(Text, { color: "gray", children: ["Active stream: ", truncate(diagnostics.streamUrl ?? 'none', lineWidth - 15)] }), _jsxs(Text, { color: "gray", children: ["Library: ", truncate(storePath, lineWidth - 9)] })] })] }));
18
+ } }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: "Status" }), _jsxs(Text, { color: "gray", children: ["Output: ", _jsx(Text, { color: accent, children: audioOutputLabel(playback.backend) }), " / ", playback.state, " \u00B7", ' ', "Selected: ", _jsx(Text, { color: accent, children: audioOutputLabel(settings.preferredBackend) }), " \u00B7", ' ', diagnostics.muted ? 'muted' : `vol ${diagnostics.volume}`, " \u00B7 tune timeout ", settings.tuneTimeoutSeconds, "s"] }), _jsxs(Text, { color: "gray", children: ["Provider health: ", health.length ? health.map(([provider, status]) => `${provider} ${status}`).join(' · ') : 'not checked yet'] }), _jsxs(Text, { color: "gray", children: ["Active stream: ", truncate(diagnostics.streamUrl ?? 'none', lineWidth - 15)] }), _jsxs(Text, { color: "gray", children: ["Library: ", truncate(storePath, lineWidth - 9)] })] })] }));
17
19
  }
18
20
  function settingValue(item, settings, diagnostics, backends, airPlayDevices) {
19
21
  switch (item) {
@@ -25,12 +27,10 @@ function settingValue(item, settings, diagnostics, backends, airPlayDevices) {
25
27
  return settings.enableRadioGarden ? 'on' : 'off';
26
28
  case 'Toggle nearby location lookup':
27
29
  return settings.enableNearbyLocation ? 'on' : 'off';
28
- case 'Cycle playback backend':
29
- return `${settings.preferredBackend} · available ${backends.length ? backends.map(playbackBackendLabel).join(', ') : 'none'}`;
30
- case 'Cycle AirPlay target': {
31
- const device = airPlayDevices.find(candidate => candidate.id === settings.preferredAirPlayDevice);
32
- return `${device?.name ?? settings.preferredAirPlayDevice ?? 'auto'} · ${airPlayDevices.length || 'no'} found`;
33
- }
30
+ case 'Audio output':
31
+ return audioOutputSettingValue(settings, diagnostics, backends);
32
+ case 'AirPlay receiver':
33
+ return airPlayReceiverSettingValue(settings, airPlayDevices, backends);
34
34
  case 'Mute or unmute':
35
35
  if (diagnostics.backend === 'ffplay' && !playbackBackendCapabilities(diagnostics.backend).supportsMute) {
36
36
  return 'requires mpv';
@@ -3,7 +3,7 @@ import { useInput } from 'ink';
3
3
  import { homeItems, settingsItems } from './screen-items.js';
4
4
  import { applyTextInput, clamp, favoriteTarget, isEditableInput, isPlainPrintableInput, mediaTransportActionForInput, shouldHandleKeyboardEvent } from './app-state.js';
5
5
  import { parseSgrMouseEvents, primaryMousePress } from './terminal-mouse.js';
6
- export function useAppInput({ adjustVolume, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, currentItemCount, cycleAirPlayTarget, cycleDisplayColor, cyclePlaybackBackend, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openScreen, playAdjacent, playStation, player, playingStation, moveExploreCursor, moveExploreCursorToCell, refreshProviderHealth, resetLearnedTransportKeys, runSearch, saveLearnedTransportKey, screen, searchQuery, selected, selectedStation, setCapturingTransportAction, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleNearbyLocation, toggleRadioGarden, toggleSkipBrokenStreams }) {
6
+ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, currentItemCount, cycleDisplayColor, cycleAudioOutput, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openAirPlayCode, openAirPlaySettings, openScreen, playAdjacent, playStation, player, playingStation, moveExploreCursor, moveExploreCursorToCell, refreshAirPlayTargets, refreshProviderHealth, resetLearnedTransportKeys, runSearch, saveLearnedTransportKey, screen, searchQuery, selected, selectedStation, selectAirPlayDeviceAt, setCapturingTransportAction, setAirPlayCode, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, submitAirPlayCode, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleNearbyLocation, toggleRadioGarden, toggleSkipBrokenStreams }) {
7
7
  useEffect(() => {
8
8
  const onData = (data) => {
9
9
  const rawInput = String(data);
@@ -149,6 +149,21 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
149
149
  }
150
150
  return;
151
151
  }
152
+ if (screen === 'airplay-code') {
153
+ if (key.return) {
154
+ submitAirPlayCode(airPlayCode);
155
+ return;
156
+ }
157
+ if (key.escape) {
158
+ setAirPlayCode('');
159
+ go('airplay-settings');
160
+ return;
161
+ }
162
+ if (isEditableInput(input, key)) {
163
+ setAirPlayCode(value => applyTextInput(value, input, key).slice(0, 64));
164
+ }
165
+ return;
166
+ }
152
167
  if (screen === 'explore') {
153
168
  const exploreMove = exploreMoveForInput(input);
154
169
  if (exploreMove) {
@@ -200,11 +215,15 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
200
215
  return;
201
216
  }
202
217
  if (input === 'o') {
203
- cyclePlaybackBackend();
218
+ cycleAudioOutput();
204
219
  return;
205
220
  }
206
221
  if (input === 'a' && screen === 'settings') {
207
- cycleAirPlayTarget();
222
+ openAirPlaySettings();
223
+ return;
224
+ }
225
+ if (input === 'c' && screen === 'airplay-settings' && canEnterAirPlayCode) {
226
+ openAirPlayCode();
208
227
  return;
209
228
  }
210
229
  if (input === 'g') {
@@ -219,6 +238,10 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
219
238
  toggleSkipBrokenStreams();
220
239
  return;
221
240
  }
241
+ if (input === 'r' && screen === 'airplay-settings') {
242
+ refreshAirPlayTargets();
243
+ return;
244
+ }
222
245
  if (input === 'r') {
223
246
  refreshProviderHealth();
224
247
  setMessage('Provider health refreshed.');
@@ -249,6 +272,10 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
249
272
  }
250
273
  return;
251
274
  }
275
+ if ((input === 'b' || key.escape) && screen === 'airplay-settings') {
276
+ go('settings');
277
+ return;
278
+ }
252
279
  if (input === 'b' || key.escape) {
253
280
  go('home');
254
281
  return;
@@ -331,11 +358,11 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
331
358
  else if (item === 'Toggle nearby location lookup') {
332
359
  toggleNearbyLocation();
333
360
  }
334
- else if (item === 'Cycle playback backend') {
335
- cyclePlaybackBackend();
361
+ else if (item === 'Audio output') {
362
+ cycleAudioOutput();
336
363
  }
337
- else if (item === 'Cycle AirPlay target') {
338
- cycleAirPlayTarget();
364
+ else if (item === 'AirPlay receiver') {
365
+ openAirPlaySettings();
339
366
  }
340
367
  else if (item === 'Volume up') {
341
368
  adjustVolume(5);
@@ -367,6 +394,10 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
367
394
  }
368
395
  return;
369
396
  }
397
+ if (screen === 'airplay-settings') {
398
+ selectAirPlayDeviceAt(selected);
399
+ return;
400
+ }
370
401
  if (screen === 'map') {
371
402
  const country = filteredCountries[selected];
372
403
  if (country) {
@@ -1,6 +1,6 @@
1
1
  import { useCallback } from 'react';
2
2
  import { favoriteTarget, normalizeMediaKeyBindings, parseMediaActionName } from './app-state.js';
3
- export function useCommandExecutor({ beginLearningTransportKey, countries, go, loadCountry, openLibrary, player, playingStation, providers, resetLearnedTransportKeys, runSearch, screen, selectedStation, setCountries, setFilters, setLibrary, setMessage, setSearchQuery, setSleepUntil, setVolume, settingsRef, store, toggleFavorite, toggleMute, updateSettings }) {
3
+ export function useCommandExecutor({ beginLearningTransportKey, countries, go, loadCountry, openAirPlaySettings, openLibrary, player, playingStation, providers, resetLearnedTransportKeys, runSearch, screen, selectedStation, setCountries, setFilters, setLibrary, setMessage, setSearchQuery, setSleepUntil, setVolume, settingsRef, store, toggleFavorite, toggleMute, updateSettings }) {
4
4
  return useCallback(async (rawCommand) => {
5
5
  const trimmed = rawCommand.trim();
6
6
  if (!trimmed) {
@@ -98,10 +98,17 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
98
98
  }
99
99
  if (name === 'airplay-code' || name === 'airplay-passcode') {
100
100
  if (!value.trim()) {
101
- setMessage('Usage: :airplay-code <code>');
101
+ go('airplay-code');
102
102
  return;
103
103
  }
104
- player.submitAirPlayPasscode(value.trim());
104
+ const result = player.submitAirPlayPasscode(value.trim());
105
+ if (result.message) {
106
+ setMessage(result.message);
107
+ }
108
+ return;
109
+ }
110
+ if (name === 'airplay' || name === 'airplay-settings') {
111
+ openAirPlaySettings();
105
112
  return;
106
113
  }
107
114
  if (name === 'sleep') {
@@ -140,6 +147,7 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
140
147
  countries,
141
148
  go,
142
149
  loadCountry,
150
+ openAirPlaySettings,
143
151
  openLibrary,
144
152
  player,
145
153
  playingStation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciphore/radiocli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "A terminal-first world radio receiver built with Ink, mpv, and resilient public-radio providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -69,6 +69,9 @@
69
69
  "react": "^19.2.6",
70
70
  "zod": "^4.4.3"
71
71
  },
72
+ "overrides": {
73
+ "protobufjs": "^7.5.8"
74
+ },
72
75
  "devDependencies": {
73
76
  "@types/node": "^25.9.1",
74
77
  "@types/react": "^19.2.15",
@@ -80,5 +83,8 @@
80
83
  },
81
84
  "engines": {
82
85
  "node": ">=22"
86
+ },
87
+ "optionalDependencies": {
88
+ "node-airtunes2": "2.5.0"
83
89
  }
84
90
  }