@ciphore/radiocli 0.2.3 → 0.2.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.
- package/CHANGELOG.md +147 -2
- package/CONTRIBUTING.md +46 -6
- package/README.md +108 -209
- package/SECURITY.md +23 -1
- package/dist/agent/alarm-service.js +10 -7
- package/dist/agent/cli.js +16 -1
- package/dist/agent/command-schema.js +47 -0
- package/dist/agent/headless-host.js +234 -67
- package/dist/agent/launcher.js +7 -67
- package/dist/agent/mcp-install.js +15 -15
- package/dist/agent/mcp-server.js +10 -5
- package/dist/agent/service.js +74 -39
- package/dist/agent/session.js +227 -53
- package/dist/agent/tui-session.js +213 -0
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/cli.js +19 -3
- package/dist/alarms/guard.js +79 -36
- package/dist/alarms/inhibitor.js +27 -22
- package/dist/alarms/occurrence-cache.js +25 -0
- package/dist/alarms/power-guard-store.js +2 -10
- package/dist/alarms/runner.js +140 -45
- package/dist/alarms/schedule.js +37 -2
- package/dist/alarms/scheduler.js +202 -52
- package/dist/alarms/setup-verification.js +1 -2
- package/dist/alarms/system-volume-linux.js +160 -0
- package/dist/alarms/system-volume-ownership.js +335 -0
- package/dist/alarms/system-volume.js +188 -26
- package/dist/alarms/terminal-launcher.js +76 -136
- package/dist/alarms/tui-presence.js +2 -4
- package/dist/cli/arguments.js +212 -0
- package/dist/cli/completion.js +44 -0
- package/dist/cli/doctor.js +156 -0
- package/dist/cli/interactive.js +79 -0
- package/dist/cli/listener.js +227 -0
- package/dist/cli/output.js +121 -0
- package/dist/cli/render-tui.js +34 -0
- package/dist/cli.js +198 -154
- package/dist/platform/capabilities.js +60 -0
- package/dist/platform/desktop-media.js +61 -0
- package/dist/platform/desktop.js +36 -0
- package/dist/platform/diagnostics.js +273 -0
- package/dist/{player/command.js → platform/executables.js} +43 -9
- package/dist/platform/ipc.js +14 -0
- package/dist/platform/json-worker.js +111 -0
- package/dist/platform/launch-command.js +138 -0
- package/dist/platform/loopback.js +44 -0
- package/dist/platform/mpris.js +356 -0
- package/dist/platform/network.js +380 -0
- package/dist/platform/omarchy.js +147 -0
- package/dist/platform/packages.js +222 -0
- package/dist/platform/paths.js +40 -0
- package/dist/platform/runtime.js +67 -0
- package/dist/platform/shell.js +24 -0
- package/dist/platform/storage.js +48 -0
- package/dist/platform/support.js +214 -0
- package/dist/platform/terminal.js +63 -0
- package/dist/platform/terminals.js +223 -0
- package/dist/player/airplay-discovery.js +145 -60
- package/dist/player/airplay-policy.js +15 -0
- package/dist/player/airplay-process.js +63 -0
- package/dist/player/airplay-readiness.js +42 -0
- package/dist/player/airplay-sender-health.js +49 -49
- package/dist/player/airplay-sender-package.js +5 -0
- package/dist/player/airplay-sender-patch.js +102 -4
- package/dist/player/airplay-setup.js +51 -0
- package/dist/player/airplay-worker.js +97 -41
- package/dist/player/backend-install.js +17 -95
- package/dist/player/command-diagnostics.js +2 -2
- package/dist/player/ffmpeg-input.js +37 -0
- package/dist/player/mpv-desktop-media.js +194 -0
- package/dist/player/mpv-ipc-client.js +52 -2
- package/dist/player/player-controller.js +569 -82
- package/dist/player/radio-engine.js +282 -0
- package/dist/playlists/playlist.js +18 -16
- package/dist/providers/cache.js +293 -99
- package/dist/providers/discovery-session.js +111 -0
- package/dist/providers/provider-manager.js +106 -28
- package/dist/providers/radio-browser.js +418 -89
- package/dist/providers/radio-garden.js +18 -23
- package/dist/setup-playback.js +152 -0
- package/dist/setup.js +163 -168
- package/dist/storage/file-lock.js +113 -0
- package/dist/storage/read-settings.js +33 -0
- package/dist/storage/replace-file.js +20 -0
- package/dist/storage/repository.js +247 -0
- package/dist/storage/station-identity.js +21 -0
- package/dist/storage/station-schema.js +27 -0
- package/dist/storage/store.js +333 -107
- package/dist/streams/import-stream.js +163 -0
- package/dist/ui/AdaptiveContent.js +49 -32
- package/dist/ui/App.js +822 -481
- package/dist/ui/AppContent.js +5 -5
- package/dist/ui/airplay-settings.js +12 -5
- package/dist/ui/app-state.js +8 -1
- package/dist/ui/ascii.js +11 -2
- package/dist/ui/bidi-ranges.js +45 -0
- package/dist/ui/components/AdaptiveMarquee.js +20 -10
- package/dist/ui/components/Logo.js +5 -2
- package/dist/ui/components/Menu.js +2 -2
- package/dist/ui/components/ScreenHeader.js +1 -1
- package/dist/ui/components/StationList.js +6 -4
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/display-context.js +8 -11
- package/dist/ui/format.js +37 -4
- package/dist/ui/help-content.js +9 -6
- package/dist/ui/layout.js +5 -2
- package/dist/ui/page-footer.js +3 -3
- package/dist/ui/rtl-output.js +137 -0
- package/dist/ui/screen-items.js +12 -6
- package/dist/ui/screen-meta.js +1 -1
- package/dist/ui/screens/AirPlayCodeScreen.js +6 -2
- package/dist/ui/screens/AirPlaySettingsScreen.js +10 -6
- package/dist/ui/screens/AlarmsScreen.js +5 -2
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +8 -4
- package/dist/ui/screens/HomeScreen.js +5 -1
- package/dist/ui/screens/MapScreen.js +1 -1
- package/dist/ui/screens/NowPlayingScreen.js +4 -4
- package/dist/ui/screens/SearchScreen.js +3 -1
- package/dist/ui/screens/SettingsScreen.js +28 -21
- package/dist/ui/screens/StatsScreen.js +3 -1
- package/dist/ui/system-actions.js +80 -52
- package/dist/ui/terminal-renderer.js +16 -0
- package/dist/ui/text-direction.js +9 -0
- package/dist/ui/use-alarm-tui.js +57 -42
- package/dist/ui/use-app-input.js +84 -30
- package/dist/ui/use-command-executor.js +50 -19
- package/dist/ui/visualizers/gallop.js +1 -1
- package/dist/ui/visualizers/receiver-style-registry.js +2 -1
- package/dist/update-check.js +10 -18
- package/docs/THIRD_PARTY_NOTICES.md +6 -4
- package/package.json +34 -11
- package/scripts/build-airplay-native.mjs +22 -0
- package/vendor/node-airtunes2/LICENSE +208 -0
- package/vendor/node-airtunes2/RADIOCLI_PATCH.md +29 -0
- package/vendor/node-airtunes2/README.md +225 -0
- package/vendor/node-airtunes2/UPSTREAM.json +50 -0
- package/vendor/node-airtunes2/binding.gyp +41 -0
- package/vendor/node-airtunes2/lib/ap2_test.js +4 -0
- package/vendor/node-airtunes2/lib/atv.js +307 -0
- package/vendor/node-airtunes2/lib/atvAuthenticator.js +136 -0
- package/vendor/node-airtunes2/lib/audio_out.js +69 -0
- package/vendor/node-airtunes2/lib/circular_buffer.js +133 -0
- package/vendor/node-airtunes2/lib/config.js +26 -0
- package/vendor/node-airtunes2/lib/device_airtunes.js +532 -0
- package/vendor/node-airtunes2/lib/devices.js +266 -0
- package/vendor/node-airtunes2/lib/ed25519.js +26 -0
- package/vendor/node-airtunes2/lib/homekit/credentials.js +86 -0
- package/vendor/node-airtunes2/lib/homekit/encryption.js +69 -0
- package/vendor/node-airtunes2/lib/homekit/number.js +44 -0
- package/vendor/node-airtunes2/lib/homekit/tlv.js +95 -0
- package/vendor/node-airtunes2/lib/http.js +189 -0
- package/vendor/node-airtunes2/lib/index.d.ts +141 -0
- package/vendor/node-airtunes2/lib/index.js +127 -0
- package/vendor/node-airtunes2/lib/ntp.js +37 -0
- package/vendor/node-airtunes2/lib/num_util.js +21 -0
- package/vendor/node-airtunes2/lib/packet_pool.js +43 -0
- package/vendor/node-airtunes2/lib/rtsp.js +1807 -0
- package/vendor/node-airtunes2/lib/srp.js +158 -0
- package/vendor/node-airtunes2/lib/udp_servers.js +214 -0
- package/vendor/node-airtunes2/lib/util.js +12 -0
- package/vendor/node-airtunes2/package.json +31 -0
- package/vendor/node-airtunes2/src/CADebugMacros.h +257 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.cpp +105 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.h +224 -0
- package/vendor/node-airtunes2/src/aes_utils.c +864 -0
- package/vendor/node-airtunes2/src/aes_utils.h +24 -0
- package/vendor/node-airtunes2/src/base64.c +136 -0
- package/vendor/node-airtunes2/src/base64.h +42 -0
- package/vendor/node-airtunes2/src/bindings.cc +29 -0
- package/vendor/node-airtunes2/src/codec.cc +170 -0
- package/vendor/node-airtunes2/src/coreaudio.cc +332 -0
package/dist/ui/AppContent.js
CHANGED
|
@@ -15,7 +15,7 @@ import { AirPlayCodeScreen } from './screens/AirPlayCodeScreen.js';
|
|
|
15
15
|
import { selectedAirPlayDevice } from './airplay-settings.js';
|
|
16
16
|
import { AdaptiveContent } from './AdaptiveContent.js';
|
|
17
17
|
import { AlarmsScreen, AlarmEditorScreen, AlarmPickerScreen, AlarmRingingScreen } from './screens/AlarmsScreen.js';
|
|
18
|
-
export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends, countryFilter, diagnostics, displayStations, editingCountryFilter, editingSearch, exploreCursor, favoriteKeys, filterLabel, filteredCountries, frameWidth, layout, library, loadingCountries, loadingStations, nowPlaying, playback, playingStation, providerHealth, searchQuery, screen, settingsPage, selected, showDiagnostics, sleepLabel, stationContext, stationFavorite, stationTime, storePath, theme, updateCheck, alarmTui }) {
|
|
18
|
+
export function AppContent({ platform = process.platform, airPlayDevices, airPlayCode, appVersion, backends, countryFilter, diagnostics, displayStations, editingCountryFilter, editingSearch, exploreCursor, favoriteKeys, filterLabel, filteredCountries, frameWidth, layout, library, loadingCountries, loadingStations, nowPlaying, playback, playingStation, providerHealth, searchQuery, screen, settingsPage, selected, showDiagnostics, sleepLabel, stationContext, stationFavorite, stationTime, storePath, theme, updateCheck, alarmTui, airPlaySupported = true }) {
|
|
19
19
|
if (layout.compact) {
|
|
20
20
|
if (screen === 'alarms')
|
|
21
21
|
return _jsx(AlarmsScreen, { alarms: library.alarms, selected: selected, runtime: alarmTui.runtime, verification: alarmTui.verification, deletingId: alarmTui.deletingId, busyAlarmIds: alarmTui.busyAlarmIds, theme: theme, width: frameWidth, height: layout.contentRows, mode: layout.mode });
|
|
@@ -25,7 +25,7 @@ export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends,
|
|
|
25
25
|
return _jsx(AlarmPickerScreen, { choices: alarmTui.pickerChoices, selected: selected, fallback: alarmTui.pickerFallback, theme: theme, width: frameWidth, height: layout.contentRows });
|
|
26
26
|
if (screen === 'alarm-ringing')
|
|
27
27
|
return _jsx(AlarmRingingScreen, { sessions: alarmTui.activeAlarms, alarms: library.alarms, selected: alarmTui.activeSelected, snoozeMinutes: alarmTui.snoozeMinutes, theme: theme, width: frameWidth, height: layout.contentRows });
|
|
28
|
-
return (_jsx(AdaptiveContent, { mode: layout.mode === 'micro' ? 'micro' : 'compact', screen: screen, settingsPage: settingsPage, selected: selected, height: layout.contentRows, width: frameWidth, theme: theme, playback: playback, playingStation: playingStation, nowPlaying: nowPlaying, stations: displayStations, countries: filteredCountries, airPlayDevices: airPlayDevices, airPlayCode: airPlayCode, searchQuery: searchQuery, editingSearch: editingSearch, countryFilter: countryFilter, editingCountryFilter: editingCountryFilter, loadingCountries: loadingCountries, loadingStations: loadingStations, exploreCursor: exploreCursor, library: library, diagnostics: diagnostics, backends: backends, updateCheck: updateCheck, appVersion: appVersion, favoriteKeys: favoriteKeys, stationTitle: stationContext.title, stationError: stationContext.error, filterLabel: filterLabel, sleepLabel: sleepLabel }));
|
|
28
|
+
return (_jsx(AdaptiveContent, { platform: platform, mode: layout.mode === 'micro' ? 'micro' : 'compact', screen: screen, settingsPage: settingsPage, selected: selected, height: layout.contentRows, width: frameWidth, theme: theme, playback: playback, playingStation: playingStation, nowPlaying: nowPlaying, stations: displayStations, countries: filteredCountries, airPlayDevices: airPlayDevices, airPlayCode: airPlayCode, searchQuery: searchQuery, editingSearch: editingSearch, countryFilter: countryFilter, editingCountryFilter: editingCountryFilter, loadingCountries: loadingCountries, loadingStations: loadingStations, exploreCursor: exploreCursor, library: library, diagnostics: diagnostics, backends: backends, updateCheck: updateCheck, appVersion: appVersion, favoriteKeys: favoriteKeys, stationTitle: stationContext.title, stationError: stationContext.error, filterLabel: filterLabel, sleepLabel: sleepLabel, airPlaySupported: airPlaySupported }));
|
|
29
29
|
}
|
|
30
30
|
if (screen === 'alarms')
|
|
31
31
|
return _jsx(AlarmsScreen, { alarms: library.alarms, selected: selected, runtime: alarmTui.runtime, verification: alarmTui.verification, deletingId: alarmTui.deletingId, busyAlarmIds: alarmTui.busyAlarmIds, theme: theme, width: frameWidth, height: layout.contentRows, mode: layout.mode });
|
|
@@ -60,16 +60,16 @@ export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends,
|
|
|
60
60
|
return _jsx(StatsScreen, { library: library, theme: theme, width: frameWidth, height: layout.contentRows });
|
|
61
61
|
}
|
|
62
62
|
if (screen === 'settings') {
|
|
63
|
-
return (_jsx(SettingsScreen, { page: settingsPage, selected: selected, settings: library.settings, appVersion: appVersion, updateCheck: updateCheck, storePath: storePath, playback: playback, backends: backends, airPlayDevices: airPlayDevices, providerHealth: providerHealth, theme: theme, diagnostics: diagnostics, width: frameWidth, height: layout.contentRows }));
|
|
63
|
+
return (_jsx(SettingsScreen, { platform: platform, page: settingsPage, selected: selected, settings: library.settings, appVersion: appVersion, updateCheck: updateCheck, storePath: storePath, playback: playback, backends: backends, airPlayDevices: airPlayDevices, providerHealth: providerHealth, theme: theme, diagnostics: diagnostics, width: frameWidth, height: layout.contentRows, airPlaySupported: airPlaySupported }));
|
|
64
64
|
}
|
|
65
65
|
if (screen === 'airplay-settings') {
|
|
66
|
-
return (_jsx(AirPlaySettingsScreen, { selected: selected, settings: library.settings, backends: backends, devices: airPlayDevices, theme: theme, width: frameWidth, height: layout.contentRows }));
|
|
66
|
+
return (_jsx(AirPlaySettingsScreen, { platform: platform, selected: selected, settings: library.settings, backends: backends, devices: airPlayDevices, theme: theme, width: frameWidth, height: layout.contentRows }));
|
|
67
67
|
}
|
|
68
68
|
if (screen === 'airplay-code') {
|
|
69
69
|
return (_jsx(AirPlayCodeScreen, { code: airPlayCode, playback: playback, selectedDevice: selectedAirPlayDevice(library.settings, airPlayDevices), theme: theme, width: frameWidth }));
|
|
70
70
|
}
|
|
71
71
|
if (screen === 'help') {
|
|
72
|
-
return _jsx(HelpScreen, { theme: theme, width: frameWidth, height: layout.contentRows, selected: selected });
|
|
72
|
+
return _jsx(HelpScreen, { airPlaySupported: airPlaySupported, theme: theme, width: frameWidth, height: layout.contentRows, selected: selected });
|
|
73
73
|
}
|
|
74
74
|
return _jsx(Text, { children: "Unknown screen." });
|
|
75
75
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { airPlayLocalDeviceLabel } from '../player/backend-install.js';
|
|
1
2
|
export function isAirPlayBackendAvailable(backends) {
|
|
2
3
|
return backends.includes('airplay');
|
|
3
4
|
}
|
|
4
5
|
export function selectedAirPlayDevice(settings, devices) {
|
|
5
6
|
return devices.find(device => device.id === settings.preferredAirPlayDevice);
|
|
6
7
|
}
|
|
7
|
-
export function airPlayReceiverSettingValue(settings, devices, backends) {
|
|
8
|
+
export function airPlayReceiverSettingValue(settings, devices, backends, platform = process.platform) {
|
|
9
|
+
if (platform === 'linux' && !isAirPlayBackendAvailable(backends))
|
|
10
|
+
return 'Setup required';
|
|
8
11
|
const device = selectedAirPlayDevice(settings, devices);
|
|
9
12
|
const count = airPlayDeviceCountLabel(devices.length);
|
|
10
13
|
const playbackStatus = isAirPlayBackendAvailable(backends) ? undefined : 'playback unavailable';
|
|
11
14
|
if (device) {
|
|
12
|
-
return [device.name, device.local ?
|
|
15
|
+
return [device.name, device.local ? airPlayLocalDeviceLabel(platform) : playbackStatus ?? count].join(' · ');
|
|
13
16
|
}
|
|
14
17
|
if (settings.preferredAirPlayDevice) {
|
|
15
18
|
return ['Saved receiver missing', playbackStatus ?? count].join(' · ');
|
|
@@ -19,8 +22,10 @@ export function airPlayReceiverSettingValue(settings, devices, backends) {
|
|
|
19
22
|
}
|
|
20
23
|
return playbackStatus ? `No receivers found · ${playbackStatus}` : 'No receivers found';
|
|
21
24
|
}
|
|
22
|
-
export function airPlayAvailability(backends, devices) {
|
|
25
|
+
export function airPlayAvailability(backends, devices, platform = process.platform) {
|
|
23
26
|
if (!isAirPlayBackendAvailable(backends)) {
|
|
27
|
+
if (platform === 'linux')
|
|
28
|
+
return { ready: false, label: 'Setup required', detail: 'Run radiocli setup --airplay, then radiocli doctor to check FFmpeg, Avahi, and the optional sender.' };
|
|
24
29
|
const detail = devices.length > 0
|
|
25
30
|
? 'RadioCLI can see receivers, but this install cannot start AirPlay yet. Run radiocli doctor.'
|
|
26
31
|
: 'AirPlay is not ready on this install. Run radiocli doctor.';
|
|
@@ -31,6 +36,8 @@ export function airPlayAvailability(backends, devices) {
|
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
if (devices.length === 0) {
|
|
39
|
+
if (platform === 'linux')
|
|
40
|
+
return { ready: false, label: 'Unverified', detail: 'Refresh to find IPv4 receivers and verify experimental AirPlay. Select a receiver each session.' };
|
|
34
41
|
return {
|
|
35
42
|
ready: true,
|
|
36
43
|
label: 'Ready',
|
|
@@ -43,9 +50,9 @@ export function airPlayAvailability(backends, devices) {
|
|
|
43
50
|
detail: `${airPlayDeviceCountLabel(devices.length)} visible on this network.`
|
|
44
51
|
};
|
|
45
52
|
}
|
|
46
|
-
export function airPlayDeviceDetail(device) {
|
|
53
|
+
export function airPlayDeviceDetail(device, platform = process.platform) {
|
|
47
54
|
if (device.local) {
|
|
48
|
-
return `${device.host}:${device.port} ·
|
|
55
|
+
return `${device.host}:${device.port} · ${airPlayLocalDeviceLabel(platform)} · use local output`;
|
|
49
56
|
}
|
|
50
57
|
const security = device.requiresPassword ? 'code shown on receiver' : 'no code';
|
|
51
58
|
const protocol = device.airplay2 ? 'AirPlay 2' : 'AirPlay';
|
package/dist/ui/app-state.js
CHANGED
|
@@ -38,7 +38,7 @@ export const initialStationContexts = {
|
|
|
38
38
|
},
|
|
39
39
|
library: {
|
|
40
40
|
title: 'Library',
|
|
41
|
-
subtitle: 'Favorites,
|
|
41
|
+
subtitle: 'Favorites, imported streams, and recent stations',
|
|
42
42
|
stations: []
|
|
43
43
|
}
|
|
44
44
|
};
|
|
@@ -58,6 +58,13 @@ export function searchEditingArrowAction(key, hasResults) {
|
|
|
58
58
|
}
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
|
+
export function shouldTuneSearchResult(query, lastSubmittedQuery) {
|
|
62
|
+
const trimmedQuery = query.trim();
|
|
63
|
+
return !trimmedQuery || trimmedQuery === lastSubmittedQuery.trim();
|
|
64
|
+
}
|
|
65
|
+
export function isSearchFavoriteShortcut(input, key) {
|
|
66
|
+
return input.toLowerCase() === 'f' && Boolean(key.ctrl || key.meta || key.super);
|
|
67
|
+
}
|
|
61
68
|
export function clampVolume(value) {
|
|
62
69
|
return Math.min(100, Math.max(0, Math.round(value)));
|
|
63
70
|
}
|
package/dist/ui/ascii.js
CHANGED
|
@@ -64,6 +64,14 @@ function asciiGlyph(code) {
|
|
|
64
64
|
if (code === 0x25c9 || code === 0x25d2) {
|
|
65
65
|
return 'o'; // ◉ ◒
|
|
66
66
|
}
|
|
67
|
+
if (code === 0x2218)
|
|
68
|
+
return 'o'; // ring operator used by receiver patterns
|
|
69
|
+
if (code === 0x25c6 || code === 0x25c7)
|
|
70
|
+
return '*'; // decorative diamonds
|
|
71
|
+
if (code === 0x201c || code === 0x201d)
|
|
72
|
+
return '"';
|
|
73
|
+
if (code === 0x2018 || code === 0x2019)
|
|
74
|
+
return "'";
|
|
67
75
|
if (code === 0x25b2) {
|
|
68
76
|
return '^'; // ▲
|
|
69
77
|
}
|
|
@@ -105,8 +113,9 @@ function asciiGlyph(code) {
|
|
|
105
113
|
return '^';
|
|
106
114
|
if (code === 0x2193)
|
|
107
115
|
return 'v';
|
|
108
|
-
// Drop
|
|
109
|
-
|
|
116
|
+
// Drop invisible separators, but retain combining accents in station data.
|
|
117
|
+
// Width measurement already accounts for combining marks as zero columns.
|
|
118
|
+
if (code === 0x200b || code === 0xfeff) {
|
|
110
119
|
return '';
|
|
111
120
|
}
|
|
112
121
|
// Unknown Unicode is user/content data more often than decoration. Preserve
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Generated by scripts/generate-bidi-ranges.mjs; do not edit.
|
|
2
|
+
// Unicode 17.0.0: https://www.unicode.org/Public/17.0.0/ucd/extracted/DerivedBidiClass.txt
|
|
3
|
+
/*
|
|
4
|
+
UNICODE LICENSE V3
|
|
5
|
+
|
|
6
|
+
COPYRIGHT AND PERMISSION NOTICE
|
|
7
|
+
|
|
8
|
+
Copyright © 1991-2026 Unicode, Inc.
|
|
9
|
+
|
|
10
|
+
NOTICE TO USER: Carefully read the following legal agreement. BY
|
|
11
|
+
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
|
12
|
+
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
|
13
|
+
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
|
14
|
+
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
|
15
|
+
|
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
17
|
+
copy of data files and any associated documentation (the "Data Files") or
|
|
18
|
+
software and any associated documentation (the "Software") to deal in the
|
|
19
|
+
Data Files or Software without restriction, including without limitation
|
|
20
|
+
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
|
21
|
+
copies of the Data Files or Software, and to permit persons to whom the
|
|
22
|
+
Data Files or Software are furnished to do so, provided that either (a)
|
|
23
|
+
this copyright and permission notice appear with all copies of the Data
|
|
24
|
+
Files or Software, or (b) this copyright and permission notice appear in
|
|
25
|
+
associated Documentation.
|
|
26
|
+
|
|
27
|
+
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
28
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
29
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
|
30
|
+
THIRD PARTY RIGHTS.
|
|
31
|
+
|
|
32
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
|
33
|
+
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
|
34
|
+
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
35
|
+
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
36
|
+
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
|
37
|
+
FILES OR SOFTWARE.
|
|
38
|
+
|
|
39
|
+
Except as contained in this notice, the name of a copyright holder shall
|
|
40
|
+
not be used in advertising or otherwise to promote the sale, use or other
|
|
41
|
+
dealings in these Data Files or Software without prior written
|
|
42
|
+
authorization of the copyright holder.
|
|
43
|
+
*/
|
|
44
|
+
export const strongDirectionPattern = /[\u{41}-\u{5a}\u{61}-\u{7a}\u{aa}\u{b5}\u{ba}\u{c0}-\u{d6}\u{d8}-\u{f6}\u{f8}-\u{2b8}\u{2bb}-\u{2c1}\u{2d0}-\u{2d1}\u{2e0}-\u{2e4}\u{2ee}\u{370}-\u{373}\u{376}-\u{37d}\u{37f}-\u{383}\u{386}\u{388}-\u{3f5}\u{3f7}-\u{482}\u{48a}-\u{589}\u{58b}-\u{58c}\u{590}\u{5be}\u{5c0}\u{5c3}\u{5c6}\u{5c8}-\u{5ff}\u{608}\u{60b}\u{60d}\u{61b}-\u{64a}\u{66d}-\u{66f}\u{671}-\u{6d5}\u{6e5}-\u{6e6}\u{6ee}-\u{6ef}\u{6fa}-\u{710}\u{712}-\u{72f}\u{74b}-\u{7a5}\u{7b1}-\u{7ea}\u{7f4}-\u{7f5}\u{7fa}-\u{7fc}\u{7fe}-\u{815}\u{81a}\u{824}\u{828}\u{82e}-\u{858}\u{85c}-\u{88f}\u{892}-\u{896}\u{8a0}-\u{8c9}\u{903}-\u{939}\u{93b}\u{93d}-\u{940}\u{949}-\u{94c}\u{94e}-\u{950}\u{958}-\u{961}\u{964}-\u{980}\u{982}-\u{9bb}\u{9bd}-\u{9c0}\u{9c5}-\u{9cc}\u{9ce}-\u{9e1}\u{9e4}-\u{9f1}\u{9f4}-\u{9fa}\u{9fc}-\u{9fd}\u{9ff}-\u{a00}\u{a03}-\u{a3b}\u{a3d}-\u{a40}\u{a43}-\u{a46}\u{a49}-\u{a4a}\u{a4e}-\u{a50}\u{a52}-\u{a6f}\u{a72}-\u{a74}\u{a76}-\u{a80}\u{a83}-\u{abb}\u{abd}-\u{ac0}\u{ac6}\u{ac9}-\u{acc}\u{ace}-\u{ae1}\u{ae4}-\u{af0}\u{af2}-\u{af9}\u{b00}\u{b02}-\u{b3b}\u{b3d}-\u{b3e}\u{b40}\u{b45}-\u{b4c}\u{b4e}-\u{b54}\u{b57}-\u{b61}\u{b64}-\u{b81}\u{b83}-\u{bbf}\u{bc1}-\u{bcc}\u{bce}-\u{bf2}\u{bfb}-\u{bff}\u{c01}-\u{c03}\u{c05}-\u{c3b}\u{c3d}\u{c41}-\u{c45}\u{c49}\u{c4e}-\u{c54}\u{c57}-\u{c61}\u{c64}-\u{c77}\u{c7f}-\u{c80}\u{c82}-\u{cbb}\u{cbd}-\u{ccb}\u{cce}-\u{ce1}\u{ce4}-\u{cff}\u{d02}-\u{d3a}\u{d3d}-\u{d40}\u{d45}-\u{d4c}\u{d4e}-\u{d61}\u{d64}-\u{d80}\u{d82}-\u{dc9}\u{dcb}-\u{dd1}\u{dd5}\u{dd7}-\u{e30}\u{e32}-\u{e33}\u{e3b}-\u{e3e}\u{e40}-\u{e46}\u{e4f}-\u{eb0}\u{eb2}-\u{eb3}\u{ebd}-\u{ec7}\u{ecf}-\u{f17}\u{f1a}-\u{f34}\u{f36}\u{f38}\u{f3e}-\u{f70}\u{f7f}\u{f85}\u{f88}-\u{f8c}\u{f98}\u{fbd}-\u{fc5}\u{fc7}-\u{102c}\u{1031}\u{1038}\u{103b}-\u{103c}\u{103f}-\u{1057}\u{105a}-\u{105d}\u{1061}-\u{1070}\u{1075}-\u{1081}\u{1083}-\u{1084}\u{1087}-\u{108c}\u{108e}-\u{109c}\u{109e}-\u{135c}\u{1360}-\u{138f}\u{139a}-\u{13ff}\u{1401}-\u{167f}\u{1681}-\u{169a}\u{169d}-\u{1711}\u{1715}-\u{1731}\u{1734}-\u{1751}\u{1754}-\u{1771}\u{1774}-\u{17b3}\u{17b6}\u{17be}-\u{17c5}\u{17c7}-\u{17c8}\u{17d4}-\u{17da}\u{17dc}\u{17de}-\u{17ef}\u{17fa}-\u{17ff}\u{1810}-\u{1884}\u{1887}-\u{18a8}\u{18aa}-\u{191f}\u{1923}-\u{1926}\u{1929}-\u{1931}\u{1933}-\u{1938}\u{193c}-\u{193f}\u{1941}-\u{1943}\u{1946}-\u{19dd}\u{1a00}-\u{1a16}\u{1a19}-\u{1a1a}\u{1a1c}-\u{1a55}\u{1a57}\u{1a5f}\u{1a61}\u{1a63}-\u{1a64}\u{1a6d}-\u{1a72}\u{1a7d}-\u{1a7e}\u{1a80}-\u{1aaf}\u{1ade}-\u{1adf}\u{1aec}-\u{1aff}\u{1b04}-\u{1b33}\u{1b35}\u{1b3b}\u{1b3d}-\u{1b41}\u{1b43}-\u{1b6a}\u{1b74}-\u{1b7f}\u{1b82}-\u{1ba1}\u{1ba6}-\u{1ba7}\u{1baa}\u{1bae}-\u{1be5}\u{1be7}\u{1bea}-\u{1bec}\u{1bee}\u{1bf2}-\u{1c2b}\u{1c34}-\u{1c35}\u{1c38}-\u{1ccf}\u{1cd3}\u{1ce1}\u{1ce9}-\u{1cec}\u{1cee}-\u{1cf3}\u{1cf5}-\u{1cf7}\u{1cfa}-\u{1dbf}\u{1e00}-\u{1fbc}\u{1fbe}\u{1fc2}-\u{1fcc}\u{1fd0}-\u{1fdc}\u{1fe0}-\u{1fec}\u{1ff0}-\u{1ffc}\u{1fff}\u{200e}-\u{200f}\u{2071}-\u{2073}\u{207f}\u{208f}-\u{209f}\u{20f1}-\u{20ff}\u{2102}\u{2107}\u{210a}-\u{2113}\u{2115}\u{2119}-\u{211d}\u{2124}\u{2126}\u{2128}\u{212a}-\u{212d}\u{212f}-\u{2139}\u{213c}-\u{213f}\u{2145}-\u{2149}\u{214e}-\u{214f}\u{2160}-\u{2188}\u{218c}-\u{218f}\u{2336}-\u{237a}\u{2395}\u{242a}-\u{243f}\u{244b}-\u{245f}\u{249c}-\u{24e9}\u{26ac}\u{2800}-\u{28ff}\u{2b74}-\u{2b75}\u{2c00}-\u{2ce4}\u{2ceb}-\u{2cee}\u{2cf2}-\u{2cf8}\u{2d00}-\u{2d7e}\u{2d80}-\u{2ddf}\u{2e5e}-\u{2e7f}\u{2e9a}\u{2ef4}-\u{2eff}\u{2fd6}-\u{2fef}\u{3005}-\u{3007}\u{3021}-\u{3029}\u{302e}-\u{302f}\u{3031}-\u{3035}\u{3038}-\u{303c}\u{3040}-\u{3098}\u{309d}-\u{309f}\u{30a1}-\u{30fa}\u{30fc}-\u{31bf}\u{31e6}-\u{31ee}\u{31f0}-\u{321c}\u{321f}-\u{324f}\u{3260}-\u{327b}\u{327f}-\u{32b0}\u{32c0}-\u{32cb}\u{32d0}-\u{3376}\u{337b}-\u{33dd}\u{33e0}-\u{33fe}\u{3400}-\u{4dbf}\u{4e00}-\u{a48f}\u{a4c7}-\u{a60c}\u{a610}-\u{a66e}\u{a680}-\u{a69d}\u{a6a0}-\u{a6ef}\u{a6f2}-\u{a6ff}\u{a722}-\u{a787}\u{a789}-\u{a801}\u{a803}-\u{a805}\u{a807}-\u{a80a}\u{a80c}-\u{a824}\u{a827}\u{a82d}-\u{a837}\u{a83a}-\u{a873}\u{a878}-\u{a8c3}\u{a8c6}-\u{a8df}\u{a8f2}-\u{a8fe}\u{a900}-\u{a925}\u{a92e}-\u{a946}\u{a952}-\u{a97f}\u{a983}-\u{a9b2}\u{a9b4}-\u{a9b5}\u{a9ba}-\u{a9bb}\u{a9be}-\u{a9e4}\u{a9e6}-\u{aa28}\u{aa2f}-\u{aa30}\u{aa33}-\u{aa34}\u{aa37}-\u{aa42}\u{aa44}-\u{aa4b}\u{aa4d}-\u{aa7b}\u{aa7d}-\u{aaaf}\u{aab1}\u{aab5}-\u{aab6}\u{aab9}-\u{aabd}\u{aac0}\u{aac2}-\u{aaeb}\u{aaee}-\u{aaf5}\u{aaf7}-\u{ab69}\u{ab6c}-\u{abe4}\u{abe6}-\u{abe7}\u{abe9}-\u{abec}\u{abee}-\u{fb1d}\u{fb1f}-\u{fb28}\u{fb2a}-\u{fbc2}\u{fbd3}-\u{fd3d}\u{fd50}-\u{fd8f}\u{fd92}-\u{fdc7}\u{fdf0}-\u{fdfc}\u{fe1a}-\u{fe1f}\u{fe53}\u{fe67}\u{fe6c}-\u{fefe}\u{ff00}\u{ff21}-\u{ff3a}\u{ff41}-\u{ff5a}\u{ff66}-\u{ffdf}\u{ffe7}\u{ffef}\u{10000}-\u{10100}\u{10102}-\u{1013f}\u{1018d}-\u{1018f}\u{1019d}-\u{1019f}\u{101a1}-\u{101fc}\u{101fe}-\u{102df}\u{102fc}-\u{10375}\u{1037b}-\u{1091e}\u{10920}-\u{10a00}\u{10a04}\u{10a07}-\u{10a0b}\u{10a10}-\u{10a37}\u{10a3b}-\u{10a3e}\u{10a40}-\u{10ae4}\u{10ae7}-\u{10b38}\u{10b40}-\u{10d23}\u{10d28}-\u{10d2f}\u{10d3a}-\u{10d3f}\u{10d4a}-\u{10d68}\u{10d6f}-\u{10e5f}\u{10e7f}-\u{10eaa}\u{10ead}-\u{10ecf}\u{10ed9}-\u{10ef9}\u{10f00}-\u{10f45}\u{10f51}-\u{10f81}\u{10f86}-\u{11000}\u{11002}-\u{11037}\u{11047}-\u{11051}\u{11066}-\u{1106f}\u{11071}-\u{11072}\u{11075}-\u{1107e}\u{11082}-\u{110b2}\u{110b7}-\u{110b8}\u{110bb}-\u{110c1}\u{110c3}-\u{110ff}\u{11103}-\u{11126}\u{1112c}\u{11135}-\u{11172}\u{11174}-\u{1117f}\u{11182}-\u{111b5}\u{111bf}-\u{111c8}\u{111cd}-\u{111ce}\u{111d0}-\u{1122e}\u{11232}-\u{11233}\u{11235}\u{11238}-\u{1123d}\u{1123f}-\u{11240}\u{11242}-\u{112de}\u{112e0}-\u{112e2}\u{112eb}-\u{112ff}\u{11302}-\u{1133a}\u{1133d}-\u{1133f}\u{11341}-\u{11365}\u{1136d}-\u{1136f}\u{11375}-\u{113ba}\u{113c1}-\u{113cd}\u{113cf}\u{113d1}\u{113d3}-\u{113e0}\u{113e3}-\u{11437}\u{11440}-\u{11441}\u{11445}\u{11447}-\u{1145d}\u{1145f}-\u{114b2}\u{114b9}\u{114bb}-\u{114be}\u{114c1}\u{114c4}-\u{115b1}\u{115b6}-\u{115bb}\u{115be}\u{115c1}-\u{115db}\u{115de}-\u{11632}\u{1163b}-\u{1163c}\u{1163e}\u{11641}-\u{1165f}\u{1166d}-\u{116aa}\u{116ac}\u{116ae}-\u{116af}\u{116b6}\u{116b8}-\u{1171c}\u{1171e}\u{11720}-\u{11721}\u{11726}\u{1172c}-\u{1182e}\u{11838}\u{1183b}-\u{1193a}\u{1193d}\u{1193f}-\u{11942}\u{11944}-\u{119d3}\u{119d8}-\u{119d9}\u{119dc}-\u{119df}\u{119e1}-\u{11a00}\u{11a07}-\u{11a08}\u{11a0b}-\u{11a32}\u{11a39}-\u{11a3a}\u{11a3f}-\u{11a46}\u{11a48}-\u{11a50}\u{11a57}-\u{11a58}\u{11a5c}-\u{11a89}\u{11a97}\u{11a9a}-\u{11b5f}\u{11b61}\u{11b65}\u{11b67}-\u{11c2f}\u{11c37}\u{11c3e}-\u{11c91}\u{11ca8}-\u{11ca9}\u{11cb1}\u{11cb4}\u{11cb7}-\u{11d30}\u{11d37}-\u{11d39}\u{11d3b}\u{11d3e}\u{11d46}\u{11d48}-\u{11d8f}\u{11d92}-\u{11d94}\u{11d96}\u{11d98}-\u{11ef2}\u{11ef5}-\u{11eff}\u{11f02}-\u{11f35}\u{11f3b}-\u{11f3f}\u{11f41}\u{11f43}-\u{11f59}\u{11f5b}-\u{11fd4}\u{11ff2}-\u{1343f}\u{13441}-\u{13446}\u{13456}-\u{1611d}\u{1612a}-\u{1612c}\u{16130}-\u{16aef}\u{16af5}-\u{16b2f}\u{16b37}-\u{16f4e}\u{16f50}-\u{16f8e}\u{16f93}-\u{16fe1}\u{16fe3}\u{16fe5}-\u{1bc9c}\u{1bc9f}\u{1bca4}-\u{1cbff}\u{1ccd6}-\u{1ccef}\u{1ccfd}-\u{1ccff}\u{1ceb4}-\u{1ceb9}\u{1ced1}-\u{1cedf}\u{1cef1}-\u{1ceff}\u{1cf2e}-\u{1cf2f}\u{1cf47}-\u{1d166}\u{1d16a}-\u{1d172}\u{1d183}-\u{1d184}\u{1d18c}-\u{1d1a9}\u{1d1ae}-\u{1d1e8}\u{1d1eb}-\u{1d1ff}\u{1d246}-\u{1d2ff}\u{1d357}-\u{1d6c0}\u{1d6c2}-\u{1d6da}\u{1d6dc}-\u{1d6fa}\u{1d6fc}-\u{1d714}\u{1d716}-\u{1d734}\u{1d736}-\u{1d74e}\u{1d750}-\u{1d76e}\u{1d770}-\u{1d788}\u{1d78a}-\u{1d7a8}\u{1d7aa}-\u{1d7c2}\u{1d7c4}-\u{1d7cd}\u{1d800}-\u{1d9ff}\u{1da37}-\u{1da3a}\u{1da6d}-\u{1da74}\u{1da76}-\u{1da83}\u{1da85}-\u{1da9a}\u{1daa0}\u{1dab0}-\u{1dfff}\u{1e007}\u{1e019}-\u{1e01a}\u{1e022}\u{1e025}\u{1e02b}-\u{1e08e}\u{1e090}-\u{1e12f}\u{1e137}-\u{1e2ad}\u{1e2af}-\u{1e2eb}\u{1e2f0}-\u{1e2fe}\u{1e300}-\u{1e4eb}\u{1e4f0}-\u{1e5ed}\u{1e5f0}-\u{1e6e2}\u{1e6e4}-\u{1e6e5}\u{1e6e7}-\u{1e6ed}\u{1e6f0}-\u{1e6f4}\u{1e6f6}-\u{1e8cf}\u{1e8d7}-\u{1e943}\u{1e94b}-\u{1eeef}\u{1eef2}-\u{1efff}\u{1f02c}-\u{1f02f}\u{1f094}-\u{1f09f}\u{1f0af}-\u{1f0b0}\u{1f0c0}\u{1f0d0}\u{1f0f6}-\u{1f0ff}\u{1f110}-\u{1f12e}\u{1f130}-\u{1f169}\u{1f170}-\u{1f1ac}\u{1f1ae}-\u{1f25f}\u{1f266}-\u{1f2ff}\u{1f6d9}-\u{1f6db}\u{1f6ed}-\u{1f6ef}\u{1f6fd}-\u{1f6ff}\u{1f7da}-\u{1f7df}\u{1f7ec}-\u{1f7ef}\u{1f7f1}-\u{1f7ff}\u{1f80c}-\u{1f80f}\u{1f848}-\u{1f84f}\u{1f85a}-\u{1f85f}\u{1f888}-\u{1f88f}\u{1f8ae}-\u{1f8af}\u{1f8bc}-\u{1f8bf}\u{1f8c2}-\u{1f8cf}\u{1f8d9}-\u{1f8ff}\u{1fa58}-\u{1fa5f}\u{1fa6e}-\u{1fa6f}\u{1fa7d}-\u{1fa7f}\u{1fa8b}-\u{1fa8d}\u{1fac7}\u{1fac9}-\u{1facc}\u{1fadd}-\u{1fade}\u{1faeb}-\u{1faee}\u{1faf9}-\u{1faff}\u{1fb93}\u{1fbfb}-\u{1fffd}\u{20000}-\u{2fffd}\u{30000}-\u{3fffd}\u{40000}-\u{4fffd}\u{50000}-\u{5fffd}\u{60000}-\u{6fffd}\u{70000}-\u{7fffd}\u{80000}-\u{8fffd}\u{90000}-\u{9fffd}\u{a0000}-\u{afffd}\u{b0000}-\u{bfffd}\u{c0000}-\u{cfffd}\u{d0000}-\u{dfffd}\u{e1000}-\u{efffd}\u{f0000}-\u{ffffd}\u{100000}-\u{10fffd}]/u;
|
|
45
|
+
export const rightToLeftPattern = /[\u{590}\u{5be}\u{5c0}\u{5c3}\u{5c6}\u{5c8}-\u{5ff}\u{608}\u{60b}\u{60d}\u{61b}-\u{64a}\u{66d}-\u{66f}\u{671}-\u{6d5}\u{6e5}-\u{6e6}\u{6ee}-\u{6ef}\u{6fa}-\u{710}\u{712}-\u{72f}\u{74b}-\u{7a5}\u{7b1}-\u{7ea}\u{7f4}-\u{7f5}\u{7fa}-\u{7fc}\u{7fe}-\u{815}\u{81a}\u{824}\u{828}\u{82e}-\u{858}\u{85c}-\u{88f}\u{892}-\u{896}\u{8a0}-\u{8c9}\u{200f}\u{fb1d}\u{fb1f}-\u{fb28}\u{fb2a}-\u{fbc2}\u{fbd3}-\u{fd3d}\u{fd50}-\u{fd8f}\u{fd92}-\u{fdc7}\u{fdf0}-\u{fdfc}\u{fe70}-\u{fefe}\u{10800}-\u{1091e}\u{10920}-\u{10a00}\u{10a04}\u{10a07}-\u{10a0b}\u{10a10}-\u{10a37}\u{10a3b}-\u{10a3e}\u{10a40}-\u{10ae4}\u{10ae7}-\u{10b38}\u{10b40}-\u{10d23}\u{10d28}-\u{10d2f}\u{10d3a}-\u{10d3f}\u{10d4a}-\u{10d68}\u{10d6f}-\u{10e5f}\u{10e7f}-\u{10eaa}\u{10ead}-\u{10ecf}\u{10ed9}-\u{10ef9}\u{10f00}-\u{10f45}\u{10f51}-\u{10f81}\u{10f86}-\u{10fff}\u{1e800}-\u{1e8cf}\u{1e8d7}-\u{1e943}\u{1e94b}-\u{1eeef}\u{1eef2}-\u{1efff}]/u;
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { Text } from 'ink';
|
|
4
|
-
import { displayWidth, sliceDisplay, truncate } from '../format.js';
|
|
4
|
+
import { displayWidth, sliceDisplay, sliceDisplayFromEnd, truncate } from '../format.js';
|
|
5
|
+
import { useDisplay } from '../display-context.js';
|
|
6
|
+
import { toAsciiSafe } from '../ascii.js';
|
|
7
|
+
import { textDirection } from '../text-direction.js';
|
|
5
8
|
const edgePauseMs = 1000;
|
|
6
9
|
const stepMs = 180;
|
|
7
10
|
export function AdaptiveMarquee({ text, width, active, reduceMotion }) {
|
|
8
|
-
const
|
|
11
|
+
const { ascii, reduceMotion: terminalReduceMotion } = useDisplay();
|
|
12
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
9
13
|
const maxOffset = Math.max(0, displayWidth(text) - safeWidth);
|
|
10
|
-
const animated = active && !reduceMotion && maxOffset > 0;
|
|
14
|
+
const animated = active && !reduceMotion && !terminalReduceMotion && safeWidth > 0 && maxOffset > 0;
|
|
15
|
+
// Reset position and cancel the old timer before rendering a changed name or
|
|
16
|
+
// viewport. An effect-based reset can emit a frame using the previous offset.
|
|
17
|
+
return _jsx(MarqueeText, { text: text, width: safeWidth, maxOffset: maxOffset, animated: animated, ascii: ascii }, JSON.stringify([text, safeWidth, animated]));
|
|
18
|
+
}
|
|
19
|
+
function MarqueeText({ text, width, maxOffset, animated, ascii }) {
|
|
20
|
+
const direction = useMemo(() => textDirection(text), [text]);
|
|
11
21
|
const [offset, setOffset] = useState(0);
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
setOffset(0);
|
|
14
|
-
}, [active, reduceMotion, safeWidth, text]);
|
|
15
22
|
useEffect(() => {
|
|
16
23
|
if (!animated)
|
|
17
24
|
return;
|
|
@@ -21,8 +28,11 @@ export function AdaptiveMarquee({ text, width, active, reduceMotion }) {
|
|
|
21
28
|
}, delay);
|
|
22
29
|
return () => clearTimeout(timer);
|
|
23
30
|
}, [animated, maxOffset, offset]);
|
|
31
|
+
const progress = Math.min(offset, maxOffset);
|
|
32
|
+
// Move the window toward the beginning for RTL text, keeping the original
|
|
33
|
+
// character order. Glyph shaping and bidi rendering remain the terminal's job.
|
|
24
34
|
const visible = animated
|
|
25
|
-
? sliceDisplay(text,
|
|
26
|
-
: truncate(text,
|
|
27
|
-
return _jsx(Text, { children: visible });
|
|
35
|
+
? (direction === 'rtl' ? sliceDisplayFromEnd : sliceDisplay)(text, progress, width)
|
|
36
|
+
: truncate(text, width);
|
|
37
|
+
return _jsx(Text, { "aria-label": text, children: ascii ? toAsciiSafe(visible) : visible });
|
|
28
38
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { textMuted } from '../theme.js';
|
|
4
|
+
import { useDisplay } from '../display-context.js';
|
|
4
5
|
const logoSpectrumColors = ['#ff4b5c', '#ff9f43', '#ffd166', '#a3e635', '#22c55e', '#2dd4bf', '#38bdf8', '#818cf8', '#c084fc'];
|
|
5
6
|
function LogoSpectrum() {
|
|
6
|
-
|
|
7
|
+
const { ascii } = useDisplay();
|
|
8
|
+
return (_jsx(_Fragment, { children: logoSpectrumColors.map(color => (_jsx(Text, { color: color, "aria-hidden": true, children: ascii ? '##' : '██' }, color))) }));
|
|
7
9
|
}
|
|
8
10
|
export function Logo({ compact = false, width = 80 } = {}) {
|
|
11
|
+
const { ascii } = useDisplay();
|
|
9
12
|
if (compact) {
|
|
10
13
|
const spectrumColumns = Math.max(1, width - 10);
|
|
11
14
|
const colors = logoSpectrumColors.slice(0, Math.min(logoSpectrumColors.length, spectrumColumns));
|
|
12
|
-
return (_jsxs(Box, { width: width, overflow: "hidden", children: [_jsx(Text, { bold: true, children: "RADIOCLI" }), _jsx(Text, { children: " " }), colors.map(color => _jsx(Text, { color: color, children:
|
|
15
|
+
return (_jsxs(Box, { width: width, overflow: "hidden", children: [_jsx(Text, { bold: true, children: "RADIOCLI" }), _jsx(Text, { children: " " }), colors.map(color => _jsx(Text, { color: color, "aria-hidden": true, children: ascii ? '#' : '█' }, color))] }));
|
|
13
16
|
}
|
|
14
17
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "RADIOCLI" }), _jsx(Text, { children: " " }), _jsx(LogoSpectrum, {})] }), _jsx(Text, { color: textMuted, children: "Live public radio from around the world" })] }));
|
|
15
18
|
}
|
|
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
export function Menu({ items, selected, keyFor, render }) {
|
|
4
4
|
const activeIndex = items.length > 0 ? Math.min(Math.max(selected, 0), items.length - 1) : -1;
|
|
5
|
-
return (_jsx(Box, { flexDirection: "column", children: items.map((item, position) => (_jsx(Box, { children: render(item, position, position === activeIndex) }, keyFor?.(item, position) ?? defaultMenuKey(item)))) }));
|
|
5
|
+
return (_jsx(Box, { flexDirection: "column", "aria-role": "list", children: items.map((item, position) => (_jsx(Box, { "aria-role": "listitem", "aria-state": { selected: position === activeIndex }, children: render(item, position, position === activeIndex) }, keyFor?.(item, position) ?? defaultMenuKey(item)))) }));
|
|
6
6
|
}
|
|
7
7
|
export function Pointer({ active }) {
|
|
8
|
-
return _jsx(Text, { bold: active, children: active ? '> ' : ' ' });
|
|
8
|
+
return _jsx(Text, { bold: active, "aria-hidden": true, children: active ? '> ' : ' ' });
|
|
9
9
|
}
|
|
10
10
|
function defaultMenuKey(item) {
|
|
11
11
|
if (typeof item === 'string' || typeof item === 'number') {
|
|
@@ -15,5 +15,5 @@ export function ScreenHeader({ title, width, theme, subtitle, right }) {
|
|
|
15
15
|
const rightText = right ? ` ${right}` : '';
|
|
16
16
|
const ruleWidth = Math.max(1, width - displayWidth(safeTitle) - displayWidth(rightText) - 2);
|
|
17
17
|
const rule = (ascii ? '-' : '─').repeat(ruleWidth);
|
|
18
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: accent, bold: true, children: a(safeTitle) }), _jsxs(Text, { color: textDim, children: [" ", rule] }), rightText ? _jsx(Text, { color: textMuted, children: a(rightText) }) : null] }), subtitle ? _jsx(Text, { color: textMuted, children: a(truncate(subtitle, width)) }) : null] }));
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: accent, bold: true, children: a(safeTitle) }), _jsxs(Text, { color: textDim, "aria-hidden": true, children: [" ", rule] }), rightText ? _jsx(Text, { color: textMuted, children: a(rightText) }) : null] }), subtitle ? _jsx(Text, { color: textMuted, children: a(truncate(subtitle, width)) }) : null] }));
|
|
19
19
|
}
|
|
@@ -6,10 +6,12 @@ import { Menu, Pointer } from './Menu.js';
|
|
|
6
6
|
import { visibleWindow } from '../list-window.js';
|
|
7
7
|
import { useDisplay } from '../display-context.js';
|
|
8
8
|
import { AdaptiveMarquee } from './AdaptiveMarquee.js';
|
|
9
|
+
import { toAsciiSafe } from '../ascii.js';
|
|
9
10
|
export function StationList({ stations, selected, theme, favorites, pageSize, width, emptyTitle = 'No stations found.', emptyHint = 'Try another view or clear active filters.', showCount = true }) {
|
|
10
|
-
const { reduceMotion } = useDisplay();
|
|
11
|
+
const { reduceMotion, ascii } = useDisplay();
|
|
12
|
+
const a = (value) => ascii ? toAsciiSafe(value) : value;
|
|
11
13
|
if (stations.length === 0) {
|
|
12
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: textMuted, children: emptyTitle }), _jsx(Text, { color: textMuted, children: emptyHint })] }));
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: textMuted, children: a(emptyTitle) }), _jsx(Text, { color: textMuted, children: a(emptyHint) })] }));
|
|
13
15
|
}
|
|
14
16
|
const window = visibleWindow(stations, selected, pageSize);
|
|
15
17
|
const rowWidth = Math.max(42, width - 4);
|
|
@@ -19,12 +21,12 @@ export function StationList({ stations, selected, theme, favorites, pageSize, wi
|
|
|
19
21
|
const favorite = favorites.has(`${station.provider}:${station.id}`);
|
|
20
22
|
if (width < 42) {
|
|
21
23
|
const compactNameWidth = Math.max(1, width - 2 - (favorite ? 2 : 0));
|
|
22
|
-
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: _jsx(AdaptiveMarquee, { text: station.name, width: compactNameWidth, active: active, reduceMotion: reduceMotion }) }), favorite ? _jsx(Text, { color: "yellow",
|
|
24
|
+
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: _jsx(AdaptiveMarquee, { text: station.name, width: compactNameWidth, active: active, reduceMotion: reduceMotion }) }), favorite ? _jsx(Text, { color: "yellow", "aria-label": "Favorite", children: a(' ★') }) : null] }));
|
|
23
25
|
}
|
|
24
26
|
const stationNameWidth = favorite ? Math.max(1, nameWidth - 2) : nameWidth;
|
|
25
27
|
const titleWidth = nameWidth + 2;
|
|
26
28
|
const standardMetadata = `${stationLocation(station)} · ${stationTech(station)}`;
|
|
27
29
|
const selectedMetadata = station.tags.length > 0 ? stationTags(station) : standardMetadata;
|
|
28
|
-
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsxs(Box, { width: titleWidth, children: [_jsx(Box, { width: stationNameWidth, children: _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: _jsx(AdaptiveMarquee, { text: station.name, width: stationNameWidth, active: active, reduceMotion: reduceMotion }) }) }), favorite ? _jsx(Text, { color: "yellow",
|
|
30
|
+
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsxs(Box, { width: titleWidth, children: [_jsx(Box, { width: stationNameWidth, children: _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: _jsx(AdaptiveMarquee, { text: station.name, width: stationNameWidth, active: active, reduceMotion: reduceMotion }) }) }), favorite ? _jsx(Text, { color: "yellow", "aria-label": "Favorite", children: a(' ★') }) : null] }), _jsx(Text, { color: active ? themeAccent(theme) : textMuted, children: a(truncate(active ? selectedMetadata : standardMetadata, metaWidth)) })] }));
|
|
29
31
|
} })] }));
|
|
30
32
|
}
|
|
@@ -21,7 +21,7 @@ export function TopTabs({ tabs, active, theme, width, backendLabel }) {
|
|
|
21
21
|
const visibleTabs = fitTabs(tabs, active, tabsAvailableWidth);
|
|
22
22
|
const visibleTabsWidth = tabsWidth(visibleTabs);
|
|
23
23
|
const tabPaddingWidth = Math.max(0, bodyWidth - visibleTabsWidth - (canShowRight ? rightWidth : 0));
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", backgroundColor: background, width: width, children: [_jsxs(Text, { backgroundColor: background, children: [_jsxs(Text, { color: panelBorder, children: [box.tl, " "] }), _jsx(Text, { color: accent, bold: true, children: brand }), _jsxs(Text, { color: panelBorder, children: [" ", box.h.repeat(titleRuleWidth), box.tr] })] }), _jsxs(Text, { backgroundColor: background, children: [_jsxs(Text, { color: panelBorder, children: [box.v, " "] }), visibleTabs.map((item, index) => (_jsxs(Text, { children: [item.type === 'overflow' ? (_jsx(Text, { color: textMuted, children:
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", backgroundColor: background, width: width, "aria-label": `RadioCLI. Tabs: ${tabs.map(tab => `${tab.label}${tab.screen === active ? ' (selected)' : ''}`).join(', ')}. Audio output: ${backendLabel}.`, children: [_jsxs(Text, { backgroundColor: background, children: [_jsxs(Text, { color: panelBorder, children: [box.tl, " "] }), _jsx(Text, { color: accent, bold: true, children: brand }), _jsxs(Text, { color: panelBorder, children: [" ", box.h.repeat(titleRuleWidth), box.tr] })] }), _jsxs(Text, { backgroundColor: background, children: [_jsxs(Text, { color: panelBorder, children: [box.v, " "] }), visibleTabs.map((item, index) => (_jsxs(Text, { children: [item.type === 'overflow' ? (_jsx(Text, { color: textMuted, children: ascii ? '.' : '…' })) : (_jsx(Text, { color: item.tab.screen === active ? accent : textMuted, bold: item.tab.screen === active, children: item.tab.label })), index < visibleTabs.length - 1 ? _jsxs(Text, { color: textMuted, children: [" ", box.v, " "] }) : null] }, item.type === 'overflow' ? `${item.side}-overflow` : item.tab.screen))), _jsx(Text, { children: ' '.repeat(tabPaddingWidth) }), canShowRight ? _jsx(Text, { color: textMuted, children: rightText }) : null, _jsxs(Text, { color: panelBorder, children: [" ", box.v] })] }), _jsxs(Text, { backgroundColor: background, color: panelBorder, children: [box.bl, box.h.repeat(Math.max(0, width - 2)), box.br] })] }));
|
|
25
25
|
}
|
|
26
26
|
function fitTabs(tabs, active, maxWidth) {
|
|
27
27
|
const all = tabs.map(tab => ({ type: 'tab', tab }));
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
|
+
import { resolveTerminalCapabilities } from '../platform/terminal.js';
|
|
2
3
|
import { appBackground, panelBackground } from './theme.js';
|
|
3
4
|
const opaqueBackgrounds = { app: appBackground, panel: panelBackground };
|
|
4
5
|
const transparentBackgrounds = { app: undefined, panel: undefined };
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
export function
|
|
8
|
-
|
|
6
|
+
// Sixteen-color palettes cannot safely reproduce our dark fills. Let those
|
|
7
|
+
// terminals keep their own background while Ink quantizes the foregrounds.
|
|
8
|
+
export function resolveDisplayMode(settings, env = process.env, evidence = {}) {
|
|
9
|
+
const terminal = resolveTerminalCapabilities(env, { ...settings, ...evidence });
|
|
10
|
+
const backgrounds = settings.transparentBackground || terminal.colorLevel < 2 ? transparentBackgrounds : opaqueBackgrounds;
|
|
11
|
+
return { ...backgrounds, ascii: !terminal.unicode, reduceMotion: terminal.reduceMotion, screenReader: terminal.screenReader, colorLevel: terminal.colorLevel };
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
// environment so light terminals keep their own background.
|
|
12
|
-
export function resolveDisplayMode(settings, env = process.env) {
|
|
13
|
-
const backgrounds = settings.transparentBackground || noColorRequested(env) ? transparentBackgrounds : opaqueBackgrounds;
|
|
14
|
-
return { ...backgrounds, ascii: Boolean(settings.asciiMode), reduceMotion: Boolean(settings.reduceMotion) };
|
|
15
|
-
}
|
|
16
|
-
const defaultMode = { ...opaqueBackgrounds, ascii: false, reduceMotion: false };
|
|
13
|
+
const defaultMode = resolveDisplayMode({}, {});
|
|
17
14
|
export const DisplayContext = createContext(defaultMode);
|
|
18
15
|
export function useDisplay() {
|
|
19
16
|
return useContext(DisplayContext);
|
package/dist/ui/format.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Use Ink's width rules on supported Node runtimes, including emoji sequences
|
|
2
|
+
// and text/emoji presentation selectors. Retain code-point fallback for hosts
|
|
3
|
+
// without Intl.Segmenter, where full grapheme rendering is unavailable.
|
|
4
|
+
const rendererWidth = typeof Intl.Segmenter === 'function' ? (await import('string-width')).default : undefined;
|
|
1
5
|
export function stationLocation(station) {
|
|
2
6
|
return [station.city, station.state, station.country].filter(Boolean).join(', ') || station.countryCode || 'Unknown origin';
|
|
3
7
|
}
|
|
@@ -35,6 +39,8 @@ export function truncate(value, length) {
|
|
|
35
39
|
return `${result}…`;
|
|
36
40
|
}
|
|
37
41
|
export function displayWidth(value) {
|
|
42
|
+
if (rendererWidth)
|
|
43
|
+
return rendererWidth(value);
|
|
38
44
|
let width = 0;
|
|
39
45
|
for (const grapheme of graphemes(value)) {
|
|
40
46
|
if (/^\p{Mark}+$/u.test(grapheme))
|
|
@@ -54,12 +60,14 @@ export function sliceDisplay(value, start, length) {
|
|
|
54
60
|
let skippedWidth = 0;
|
|
55
61
|
let outputWidth = 0;
|
|
56
62
|
let result = '';
|
|
63
|
+
let started = false;
|
|
57
64
|
for (const grapheme of graphemes(value)) {
|
|
58
65
|
const graphemeWidth = displayWidth(grapheme);
|
|
59
|
-
if (skippedWidth + graphemeWidth <= safeStart) {
|
|
66
|
+
if (!started && skippedWidth + graphemeWidth <= safeStart) {
|
|
60
67
|
skippedWidth += graphemeWidth;
|
|
61
68
|
continue;
|
|
62
69
|
}
|
|
70
|
+
started = true;
|
|
63
71
|
if (outputWidth + graphemeWidth > safeLength)
|
|
64
72
|
break;
|
|
65
73
|
result += grapheme;
|
|
@@ -67,15 +75,40 @@ export function sliceDisplay(value, start, length) {
|
|
|
67
75
|
}
|
|
68
76
|
return result;
|
|
69
77
|
}
|
|
78
|
+
/** Clip from the logical end for rightward scrolling, without reversing text. */
|
|
79
|
+
export function sliceDisplayFromEnd(value, start, length) {
|
|
80
|
+
const safeStart = Math.max(0, Math.floor(start));
|
|
81
|
+
const safeLength = Math.max(0, Math.floor(length));
|
|
82
|
+
if (safeLength === 0)
|
|
83
|
+
return '';
|
|
84
|
+
let skippedWidth = 0;
|
|
85
|
+
let outputWidth = 0;
|
|
86
|
+
const parts = graphemes(value);
|
|
87
|
+
const result = [];
|
|
88
|
+
for (let index = parts.length - 1; index >= 0; index--) {
|
|
89
|
+
const grapheme = parts[index];
|
|
90
|
+
const graphemeWidth = displayWidth(grapheme);
|
|
91
|
+
// Skip the entire cluster when the moving edge lands inside a wide glyph.
|
|
92
|
+
if (skippedWidth < safeStart) {
|
|
93
|
+
skippedWidth += graphemeWidth;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (outputWidth + graphemeWidth > safeLength)
|
|
97
|
+
break;
|
|
98
|
+
result.push(grapheme);
|
|
99
|
+
outputWidth += graphemeWidth;
|
|
100
|
+
}
|
|
101
|
+
return result.reverse().join('');
|
|
102
|
+
}
|
|
70
103
|
export function removeLastGrapheme(value) {
|
|
71
104
|
const parts = graphemes(value);
|
|
72
105
|
parts.pop();
|
|
73
106
|
return parts.join('');
|
|
74
107
|
}
|
|
108
|
+
const graphemeSegmenter = typeof Intl.Segmenter === 'function' ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null;
|
|
75
109
|
function graphemes(value) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return [...new Segmenter(undefined, { granularity: 'grapheme' }).segment(value)].map(item => item.segment);
|
|
110
|
+
if (graphemeSegmenter) {
|
|
111
|
+
return [...graphemeSegmenter.segment(value)].map(item => item.segment);
|
|
79
112
|
}
|
|
80
113
|
return Array.from(value);
|
|
81
114
|
}
|
package/dist/ui/help-content.js
CHANGED
|
@@ -20,7 +20,7 @@ export const keyHelpSections = [
|
|
|
20
20
|
{ keys: ', / . or F7/F9', description: 'Previous / next station' },
|
|
21
21
|
{ keys: '+ / -', description: 'Volume up / down' },
|
|
22
22
|
{ keys: 'm', description: 'Mute / unmute' },
|
|
23
|
-
{ keys: 'f', description: 'Favorite the station' },
|
|
23
|
+
{ keys: 'f / Ctrl+F', description: 'Favorite the station (Ctrl+F while typing in Search)' },
|
|
24
24
|
{ keys: 'O', description: 'Open station homepage' },
|
|
25
25
|
{ keys: 'y', description: 'Copy stream URL' },
|
|
26
26
|
{ keys: 's', description: 'Sleep timer (Now Playing)' },
|
|
@@ -70,7 +70,7 @@ export const commandHelp = [
|
|
|
70
70
|
{ name: 'ascii', args: '<on|off>', description: 'ASCII-safe display' },
|
|
71
71
|
{ name: 'motion', args: '<on|off>', description: 'Reduce motion' },
|
|
72
72
|
{ name: 'background', args: '<on|off>', description: 'Transparent background' },
|
|
73
|
-
{ name: 'airplay', description: 'Open AirPlay settings' },
|
|
73
|
+
{ name: 'airplay', description: 'Open experimental AirPlay settings' },
|
|
74
74
|
{ name: 'learn', args: '<previous|play|next>', description: 'Learn a media key' },
|
|
75
75
|
{ name: 'keys', args: '[reset]', description: 'Show or reset learned media keys' },
|
|
76
76
|
{ name: 'favorite', description: 'Favorite the current station (alias :fav)' },
|
|
@@ -78,13 +78,14 @@ export const commandHelp = [
|
|
|
78
78
|
{ name: 'map', description: 'Open the world map' },
|
|
79
79
|
{ name: 'stats', description: 'Open listening stats' },
|
|
80
80
|
{ name: 'settings', description: 'Open settings' },
|
|
81
|
-
{ name: '
|
|
82
|
-
{ name: '
|
|
81
|
+
{ name: 'import', args: '<stream-url> [name]', description: 'Add a custom station and discover its details' },
|
|
82
|
+
{ name: 'export', args: '[backup.json]', description: 'Back up preferences and library' },
|
|
83
|
+
{ name: 'restore', args: '<backup.json>', description: 'Restore preferences and library' },
|
|
83
84
|
{ name: 'update', description: 'Show the install command for the latest release' },
|
|
84
85
|
{ name: 'stop', description: 'Stop playback' },
|
|
85
86
|
{ name: 'help', description: 'Open this help' }
|
|
86
87
|
];
|
|
87
|
-
|
|
88
|
+
const helpItemCount = keyHelpSections.reduce((total, section) => total + 1 + section.entries.length, 0) +
|
|
88
89
|
1 +
|
|
89
90
|
commandHelp.length;
|
|
90
91
|
// Primary command names plus the aliases the executor accepts, used for
|
|
@@ -103,7 +104,6 @@ export const commandNames = [
|
|
|
103
104
|
'airplay-code',
|
|
104
105
|
'airplay-settings',
|
|
105
106
|
'backup',
|
|
106
|
-
'restore',
|
|
107
107
|
...commandHelp.map(entry => entry.name)
|
|
108
108
|
].sort((a, b) => a.localeCompare(b));
|
|
109
109
|
// Complete a partial command name to the longest shared prefix of its matches.
|
|
@@ -134,3 +134,6 @@ function longestCommonPrefix(values) {
|
|
|
134
134
|
}
|
|
135
135
|
return prefix;
|
|
136
136
|
}
|
|
137
|
+
export function commandHelpForAirPlay(visible = true) { return commandHelp.filter(command => visible || !command.name.startsWith('airplay')); }
|
|
138
|
+
export function commandNamesForAirPlay(visible = true) { return commandNames.filter(name => visible || !name.startsWith('airplay')); }
|
|
139
|
+
export function helpItemCountForAirPlay(visible = true) { return helpItemCount - commandHelp.length + commandHelpForAirPlay(visible).length; }
|
package/dist/ui/layout.js
CHANGED
|
@@ -18,8 +18,11 @@ export function computeTerminalLayout(columns = 100, rows = 30, footerRows = 2)
|
|
|
18
18
|
const reservedFooterRows = Math.min(desiredFooterRows, Math.max(0, safeRows - topRows - 1));
|
|
19
19
|
const contentRows = Math.max(1, safeRows - reservedFooterRows - topRows);
|
|
20
20
|
const mapMode = frameWidth >= 88 && contentRows >= 24 ? 'full' : 'compact';
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Derive list windows from the live terminal height. Capping these values
|
|
22
|
+
// leaves the lower half of tall/full-screen terminals unused while the
|
|
23
|
+
// selection continues scrolling through rows that could already be visible.
|
|
24
|
+
const stationRows = Math.max(1, contentRows - 6);
|
|
25
|
+
const countryRows = Math.max(1, contentRows - 5);
|
|
23
26
|
return {
|
|
24
27
|
columns: safeColumns,
|
|
25
28
|
rows: safeRows,
|
package/dist/ui/page-footer.js
CHANGED
|
@@ -13,7 +13,7 @@ export function fullStatusFooterRows(screen, message, footerMessage, playbackSta
|
|
|
13
13
|
{ key: 'playback', text: footerMessage ?? playbackStatus ?? ' ' }
|
|
14
14
|
];
|
|
15
15
|
}
|
|
16
|
-
export function pageFooterText({ capturingTransportAction, commandMode, commandText, editingCountryFilter, editingSearch, canEnterAirPlayCode, playbackBackend, screen, settingsPage = 'root' }) {
|
|
16
|
+
export function pageFooterText({ capturingTransportAction, commandMode, commandText, editingCountryFilter, editingSearch, canEnterAirPlayCode, airPlaySupported = true, playbackBackend, screen, settingsPage = 'root' }) {
|
|
17
17
|
if (capturingTransportAction) {
|
|
18
18
|
return `Learn ${mediaActionLabel(capturingTransportAction)} key: press key · Esc cancel`;
|
|
19
19
|
}
|
|
@@ -24,7 +24,7 @@ export function pageFooterText({ capturingTransportAction, commandMode, commandT
|
|
|
24
24
|
return '↑/↓ move · Enter open · number jump · l location · : command';
|
|
25
25
|
}
|
|
26
26
|
if (screen === 'search' && editingSearch) {
|
|
27
|
-
return 'Type query · ↑/↓ move results · Ctrl+↑/↓ history · Enter search/tune · Esc finish';
|
|
27
|
+
return 'Type query · ↑/↓ move results · Ctrl+↑/↓ history · Enter search/tune · Ctrl+F favorite · Esc finish';
|
|
28
28
|
}
|
|
29
29
|
if (screen === 'search') {
|
|
30
30
|
return '/ edit query · ↑/↓ or n/p move · Enter tune · f favorite · b Overview';
|
|
@@ -59,7 +59,7 @@ export function pageFooterText({ capturingTransportAction, commandMode, commandT
|
|
|
59
59
|
}
|
|
60
60
|
if (screen === 'settings') {
|
|
61
61
|
return settingsPage === 'root'
|
|
62
|
-
?
|
|
62
|
+
? `↑/↓ choose · Enter open · o output${airPlaySupported ? ' · a AirPlay' : ''} · b Overview`
|
|
63
63
|
: '↑/↓ choose · Enter change · b Settings · shortcuts still work';
|
|
64
64
|
}
|
|
65
65
|
if (screen === 'airplay-settings') {
|