@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
|
@@ -1,65 +1,93 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { resolveCommand } from '../
|
|
2
|
+
import { resolveCommand } from '../platform/executables.js';
|
|
3
3
|
import { safeExternalHttpUrl } from '../safety.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
if (platform === 'win32') {
|
|
10
|
-
// Direct invocation avoids cmd.exe interpreting URL query characters.
|
|
11
|
-
return { command: 'explorer', args: [] };
|
|
12
|
-
}
|
|
13
|
-
return { command: 'xdg-open', args: [] };
|
|
14
|
-
}
|
|
15
|
-
// Candidate clipboard tools per platform, in priority order. pbcopy/clip ship
|
|
16
|
-
// with macOS/Windows; Linux/BSD rely on whichever of these is installed.
|
|
17
|
-
export function clipboardCommands(platform = process.platform) {
|
|
18
|
-
if (platform === 'darwin') {
|
|
19
|
-
return [{ command: 'pbcopy', args: [] }];
|
|
20
|
-
}
|
|
21
|
-
if (platform === 'win32') {
|
|
22
|
-
return [{ command: 'clip', args: [] }];
|
|
23
|
-
}
|
|
24
|
-
return [
|
|
25
|
-
{ command: 'wl-copy', args: [] },
|
|
26
|
-
{ command: 'xclip', args: ['-selection', 'clipboard'] },
|
|
27
|
-
{ command: 'xsel', args: ['--clipboard', '--input'] }
|
|
28
|
-
];
|
|
29
|
-
}
|
|
30
|
-
export function openExternal(url, platform = process.platform) {
|
|
4
|
+
import { browserCommands, clipboardCandidates, hasGraphicalSession } from '../platform/desktop.js';
|
|
5
|
+
import { identifyPlatform } from '../platform/runtime.js';
|
|
6
|
+
import { waitForLaunch } from '../platform/launch-command.js';
|
|
7
|
+
export async function openExternal(url, platform = process.platform, deps = {}) {
|
|
31
8
|
const safeUrl = safeExternalHttpUrl(url);
|
|
32
|
-
|
|
9
|
+
const env = deps.env ?? process.env;
|
|
10
|
+
const host = identifyPlatform({ platform, env });
|
|
11
|
+
if (!safeUrl || !hasGraphicalSession(host, env))
|
|
33
12
|
return false;
|
|
13
|
+
for (const { command, args } of browserCommands(host)) {
|
|
14
|
+
const executable = (deps.resolve ?? resolveCommand)(command);
|
|
15
|
+
if (!executable)
|
|
16
|
+
continue;
|
|
17
|
+
try {
|
|
18
|
+
// xdg-open may run for the browser's entire lifetime. Observe startup
|
|
19
|
+
// failure, then let the desktop application own its detached process.
|
|
20
|
+
const child = (deps.spawn ?? spawn)(executable, [...args, safeUrl], { stdio: 'ignore', env, detached: true, windowsHide: true });
|
|
21
|
+
await waitForLaunch(child);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch { /* Try the next available opener after a launch failure. */ }
|
|
34
25
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// Opening a browser is best-effort; never crash the TUI over it.
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export async function copyToClipboard(text, platform = process.platform, deps = {}) {
|
|
29
|
+
const env = deps.env ?? process.env;
|
|
30
|
+
const host = identifyPlatform({ platform, env });
|
|
31
|
+
if (!hasGraphicalSession(host, env))
|
|
44
32
|
return false;
|
|
33
|
+
for (const { command, args } of clipboardCandidates(host)) {
|
|
34
|
+
const executable = (deps.resolve ?? resolveCommand)(command);
|
|
35
|
+
if (executable && await runHelper(executable, args, text, deps))
|
|
36
|
+
return true;
|
|
45
37
|
}
|
|
38
|
+
return false;
|
|
46
39
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
function runHelper(command, args, input, deps) {
|
|
41
|
+
return new Promise(resolve => {
|
|
42
|
+
let child;
|
|
43
|
+
let settled = false;
|
|
44
|
+
let stopping = false;
|
|
45
|
+
let forceTimer;
|
|
46
|
+
let cleanupTimer;
|
|
47
|
+
const signal = (value) => {
|
|
48
|
+
try {
|
|
49
|
+
child?.kill(value);
|
|
50
|
+
}
|
|
51
|
+
catch { /* The failed action remains false; cleanup is still bounded. */ }
|
|
52
|
+
};
|
|
53
|
+
const finish = (ok) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
clearTimeout(forceTimer);
|
|
59
|
+
clearTimeout(cleanupTimer);
|
|
60
|
+
resolve(ok);
|
|
61
|
+
};
|
|
62
|
+
const stop = () => {
|
|
63
|
+
if (settled || stopping)
|
|
64
|
+
return;
|
|
65
|
+
stopping = true;
|
|
66
|
+
forceTimer = setTimeout(() => signal('SIGKILL'), 250);
|
|
67
|
+
cleanupTimer = setTimeout(() => {
|
|
68
|
+
// Even an uninterruptible OS process must not keep RadioCLI alive.
|
|
69
|
+
child?.unref();
|
|
70
|
+
finish(false);
|
|
71
|
+
}, 1_000);
|
|
72
|
+
child?.stdin?.destroy();
|
|
73
|
+
signal('SIGTERM');
|
|
74
|
+
};
|
|
75
|
+
const timer = setTimeout(stop, deps.timeoutMs ?? 5000);
|
|
53
76
|
try {
|
|
54
|
-
|
|
55
|
-
child.on('error', () =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
child = (deps.spawn ?? spawn)(command, args, { stdio: ['pipe', 'ignore', 'ignore'], env: deps.env ?? process.env, windowsHide: true });
|
|
78
|
+
child.on('error', () => { if (child?.pid)
|
|
79
|
+
stop();
|
|
80
|
+
else
|
|
81
|
+
finish(false); });
|
|
82
|
+
child.once('close', code => finish(!stopping && code === 0));
|
|
83
|
+
child.stdin?.on('error', stop);
|
|
84
|
+
child.stdin?.end(input);
|
|
59
85
|
}
|
|
60
86
|
catch {
|
|
61
|
-
|
|
87
|
+
if (child?.pid)
|
|
88
|
+
stop();
|
|
89
|
+
else
|
|
90
|
+
finish(false);
|
|
62
91
|
}
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
92
|
+
});
|
|
65
93
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { resolveTerminalCapabilities } from '../platform/terminal.js';
|
|
4
|
+
/**
|
|
5
|
+
* Ink uses Chalk's shared instance for both text and panel fills. FORCE_COLOR
|
|
6
|
+
* is a minimum in Chalk, so setting it cannot cap a Windows/CI/truecolor host
|
|
7
|
+
* at 16 or 256 colors. Set the instance's level before the first Ink render.
|
|
8
|
+
* Resolve from Ink itself so nested dependency installations work as well.
|
|
9
|
+
*/
|
|
10
|
+
export async function configureTerminalRenderer(env = process.env, evidence = {}) {
|
|
11
|
+
const terminal = resolveTerminalCapabilities(env, evidence);
|
|
12
|
+
const inkRequire = createRequire(import.meta.resolve('ink'));
|
|
13
|
+
const { default: chalk } = await import(pathToFileURL(inkRequire.resolve('chalk')).href);
|
|
14
|
+
chalk.level = terminal.colorLevel;
|
|
15
|
+
return terminal;
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { rightToLeftPattern, strongDirectionPattern } from './bidi-ranges.js';
|
|
2
|
+
/** First-strong direction for single-line text sanitized by sanitizeTerminalText. */
|
|
3
|
+
export function textDirection(text) {
|
|
4
|
+
// UAX #9 P2/P3: numbers, combining marks, emoji and punctuation generally
|
|
5
|
+
// don't establish direction. Inspect the whole name, never the moving slice.
|
|
6
|
+
// Pinned bidi classes avoid relying on locale or the host's ICU version.
|
|
7
|
+
const first = strongDirectionPattern.exec(text)?.[0];
|
|
8
|
+
return first && rightToLeftPattern.test(first) ? 'rtl' : 'ltr';
|
|
9
|
+
}
|
package/dist/ui/use-alarm-tui.js
CHANGED
|
@@ -3,7 +3,12 @@ import { applyTextInput, clamp, isEditableInput } from './app-state.js';
|
|
|
3
3
|
import { adjustTime, alarmEditorFields, alarmInputFromDraft, alarmTextFields, cycleWeekdays, defaultAlarmDraft, draftFromAlarm, toggleWeekday, validateAlarmDraft } from './alarm-editor.js';
|
|
4
4
|
import { alarmPickerChoices } from './screens/AlarmsScreen.js';
|
|
5
5
|
export function useAlarmTui(params) {
|
|
6
|
-
const { service, store, library, setLibrary, screen, selected, setSelected, go, setMessage, playingStation, previewStation } = params;
|
|
6
|
+
const { service, store, library, setLibrary, screen, screenRef, selected, selectedRef, setSelected: setSelectionState, go, setMessage, playingStation, previewStation } = params;
|
|
7
|
+
const setSelected = useCallback(next => {
|
|
8
|
+
const value = typeof next === 'function' ? next(selectedRef.current) : next;
|
|
9
|
+
selectedRef.current = value;
|
|
10
|
+
setSelectionState(value);
|
|
11
|
+
}, [selectedRef, setSelectionState]);
|
|
7
12
|
const [draft, setDraft] = useState(null);
|
|
8
13
|
const [editingField, setEditingField] = useState(false);
|
|
9
14
|
const [editorControl, setEditorControl] = useState(null);
|
|
@@ -38,8 +43,6 @@ export function useAlarmTui(params) {
|
|
|
38
43
|
const activeSelectedKeyRef = useRef(undefined);
|
|
39
44
|
const pendingActiveActionsRef = useRef(new Set());
|
|
40
45
|
const acknowledgedActiveKeysRef = useRef(new Set());
|
|
41
|
-
const screenRef = useRef(screen);
|
|
42
|
-
screenRef.current = screen;
|
|
43
46
|
const alarmsRef = useRef(library.alarms);
|
|
44
47
|
alarmsRef.current = library.alarms;
|
|
45
48
|
const visibleFields = useMemo(() => alarmEditorFields.filter(field => {
|
|
@@ -67,14 +70,16 @@ export function useAlarmTui(params) {
|
|
|
67
70
|
for (const key of pendingActiveActionsRef.current)
|
|
68
71
|
if (!liveKeys.has(key))
|
|
69
72
|
pendingActiveActionsRef.current.delete(key);
|
|
70
|
-
activeAlarmsRef.current
|
|
71
|
-
|
|
73
|
+
if (JSON.stringify(activeAlarmsRef.current) !== JSON.stringify(sessions)) {
|
|
74
|
+
activeAlarmsRef.current = sessions;
|
|
75
|
+
setActiveAlarms(sessions);
|
|
76
|
+
}
|
|
72
77
|
if (sessions.length > 0 && activeCountRef.current === 0 && screenRef.current !== 'alarm-ringing') {
|
|
73
78
|
returnScreenRef.current = screenRef.current;
|
|
74
79
|
go('alarm-ringing', { resetSelection: true, clearMessage: false });
|
|
75
80
|
}
|
|
76
81
|
activeCountRef.current = sessions.length;
|
|
77
|
-
}, [go]);
|
|
82
|
+
}, [go, screenRef]);
|
|
78
83
|
const refreshActive = useCallback(async () => {
|
|
79
84
|
if (activePollingRef.current)
|
|
80
85
|
return;
|
|
@@ -180,7 +185,41 @@ export function useAlarmTui(params) {
|
|
|
180
185
|
}
|
|
181
186
|
await refreshRuntime();
|
|
182
187
|
}, [refreshRuntime, service, setMessage]);
|
|
183
|
-
const
|
|
188
|
+
const deleteAlarm = useCallback(async (alarm) => {
|
|
189
|
+
let phase = 'disable';
|
|
190
|
+
let message;
|
|
191
|
+
try {
|
|
192
|
+
// Persist the safe state before touching native jobs. A failed cleanup
|
|
193
|
+
// can then be retried without another write in the error handler.
|
|
194
|
+
const disabled = await store.toggleAlarm(alarm.id, false);
|
|
195
|
+
const disabledSnapshot = store.snapshot();
|
|
196
|
+
alarmStateRef.current = disabledSnapshot.alarms;
|
|
197
|
+
setLibrary(disabledSnapshot);
|
|
198
|
+
phase = 'native';
|
|
199
|
+
await service.remove(disabled);
|
|
200
|
+
phase = 'definition';
|
|
201
|
+
await store.removeAlarm(alarm.id);
|
|
202
|
+
const snapshot = store.snapshot();
|
|
203
|
+
alarmStateRef.current = snapshot.alarms;
|
|
204
|
+
setLibrary(snapshot);
|
|
205
|
+
setSelected(value => clamp(value, snapshot.alarms.length + 1));
|
|
206
|
+
message = `Deleted ${alarm.label}.`;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
message = phase === 'disable'
|
|
210
|
+
? `Delete canceled: ${messageOf(error)}. Alarm and native jobs unchanged.`
|
|
211
|
+
: phase === 'native'
|
|
212
|
+
? `Delete cleanup failed: ${messageOf(error)}. ${alarm.label} was kept disabled; press r to repair.`
|
|
213
|
+
: `Native cleanup completed. Save failed: ${messageOf(error)}. ${alarm.label} was kept disabled; retry deletion.`;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
busyAlarmIdsRef.current.delete(alarm.id);
|
|
217
|
+
setBusyAlarmIds(new Set(busyAlarmIdsRef.current));
|
|
218
|
+
}
|
|
219
|
+
await refreshRuntime();
|
|
220
|
+
setMessage(message);
|
|
221
|
+
}, [refreshRuntime, service, setLibrary, setMessage, setSelected, store]);
|
|
222
|
+
const saveDraft = useCallback(async () => {
|
|
184
223
|
if (!draft || savingRef.current)
|
|
185
224
|
return;
|
|
186
225
|
const error = validateAlarmDraft(draft);
|
|
@@ -193,7 +232,7 @@ export function useAlarmTui(params) {
|
|
|
193
232
|
setSaving(true);
|
|
194
233
|
try {
|
|
195
234
|
const input = alarmInputFromDraft(draft);
|
|
196
|
-
const alarm = draft.id ? store.updateAlarm(draft.id, input) : store.addAlarm(input);
|
|
235
|
+
const alarm = draft.id ? await store.updateAlarm(draft.id, input) : await store.addAlarm(input);
|
|
197
236
|
setLibrary(store.snapshot());
|
|
198
237
|
setValidationError(null);
|
|
199
238
|
go('alarms', { resetSelection: true, clearMessage: false });
|
|
@@ -212,7 +251,7 @@ export function useAlarmTui(params) {
|
|
|
212
251
|
setSaving(false);
|
|
213
252
|
}, [draft, go, setLibrary, setMessage, store, syncPersisted]);
|
|
214
253
|
const editSelected = useCallback(() => {
|
|
215
|
-
const alarm = alarmStateRef.current[
|
|
254
|
+
const alarm = alarmStateRef.current[selectedRef.current - 1];
|
|
216
255
|
if (!alarm)
|
|
217
256
|
return;
|
|
218
257
|
setDraft(draftFromAlarm(alarm));
|
|
@@ -226,7 +265,7 @@ export function useAlarmTui(params) {
|
|
|
226
265
|
setValidationError(null);
|
|
227
266
|
setSelected(0);
|
|
228
267
|
go('alarm-editor', { resetSelection: true });
|
|
229
|
-
}, [go,
|
|
268
|
+
}, [go, selectedRef, setSelected]);
|
|
230
269
|
const runVerification = useCallback(() => {
|
|
231
270
|
if (verificationRunningRef.current)
|
|
232
271
|
return;
|
|
@@ -243,6 +282,9 @@ export function useAlarmTui(params) {
|
|
|
243
282
|
void service.verifySetup(alarm, library.settings, update => setVerification(update)).then(report => { setVerification(report); setMessage(report.state === 'passed' ? 'Alarm setup verified. This machine completed every rehearsal step.' : report.state === 'warning' ? 'Alarm setup works, with limitations shown in the verification report.' : 'Alarm setup verification found blockers. Review the failed steps before relying on it.'); }).catch(error => { setVerification(current => ({ ...current ?? initial, state: 'failed', finishedAt: new Date().toISOString(), steps: [...(current?.steps ?? []), { id: 'internal', label: 'Verification runner', state: 'failed', detail: messageOf(error), critical: true }] })); setMessage(`Alarm setup verification failed: ${messageOf(error)}`); }).finally(() => { verificationRunningRef.current = false; void refreshRuntime(); });
|
|
244
283
|
}, [library.settings, refreshRuntime, service, setMessage, store]);
|
|
245
284
|
const handleInput = useCallback((input, key) => {
|
|
285
|
+
const screen = screenRef.current;
|
|
286
|
+
const selected = selectedRef.current;
|
|
287
|
+
const editorField = visibleFields[clamp(selected, visibleFields.length - 1)] ?? 'label';
|
|
246
288
|
if (!['alarms', 'alarm-editor', 'alarm-picker', 'alarm-ringing'].includes(screen))
|
|
247
289
|
return false;
|
|
248
290
|
if (key.scrollDelta) {
|
|
@@ -321,11 +363,7 @@ export function useAlarmTui(params) {
|
|
|
321
363
|
return true;
|
|
322
364
|
}
|
|
323
365
|
if (input === ' ' && alarm) {
|
|
324
|
-
const updated = store.toggleAlarm(alarm.id);
|
|
325
|
-
const snapshot = store.snapshot();
|
|
326
|
-
alarmStateRef.current = snapshot.alarms;
|
|
327
|
-
setLibrary(snapshot);
|
|
328
|
-
void syncPersisted(updated, `${updated.enabled ? 'Enabled' : 'Disabled'} ${updated.label}.`);
|
|
366
|
+
void (async () => { const updated = await store.toggleAlarm(alarm.id); const snapshot = store.snapshot(); alarmStateRef.current = snapshot.alarms; setLibrary(snapshot); await syncPersisted(updated, `${updated.enabled ? 'Enabled' : 'Disabled'} ${updated.label}.`); })().catch(error => setMessage(`Alarm save failed: ${messageOf(error)}`));
|
|
329
367
|
return true;
|
|
330
368
|
}
|
|
331
369
|
if (input === 'x' && alarm) {
|
|
@@ -343,26 +381,7 @@ export function useAlarmTui(params) {
|
|
|
343
381
|
setDeletingId(undefined);
|
|
344
382
|
busyAlarmIdsRef.current.add(alarm.id);
|
|
345
383
|
setBusyAlarmIds(new Set(busyAlarmIdsRef.current));
|
|
346
|
-
void
|
|
347
|
-
if (store.getAlarm(alarm.id))
|
|
348
|
-
store.removeAlarm(alarm.id);
|
|
349
|
-
const snapshot = store.snapshot();
|
|
350
|
-
alarmStateRef.current = snapshot.alarms;
|
|
351
|
-
setLibrary(snapshot);
|
|
352
|
-
setSelected(value => clamp(value, snapshot.alarms.length + 1));
|
|
353
|
-
setMessage(`Deleted ${alarm.label}.`);
|
|
354
|
-
}).catch(error => {
|
|
355
|
-
const existing = store.getAlarm(alarm.id);
|
|
356
|
-
if (existing) {
|
|
357
|
-
const disabled = store.toggleAlarm(alarm.id, false);
|
|
358
|
-
const snapshot = store.snapshot();
|
|
359
|
-
alarmStateRef.current = snapshot.alarms;
|
|
360
|
-
setLibrary(snapshot);
|
|
361
|
-
setMessage(`Delete cleanup failed; ${disabled.label} was kept disabled so Repair can retry: ${messageOf(error)}`);
|
|
362
|
-
}
|
|
363
|
-
else
|
|
364
|
-
setMessage(`Delete cleanup failed after the local alarm disappeared: ${messageOf(error)}`);
|
|
365
|
-
}).finally(() => { busyAlarmIdsRef.current.delete(alarm.id); setBusyAlarmIds(new Set(busyAlarmIdsRef.current)); void refreshRuntime(); });
|
|
384
|
+
void deleteAlarm(alarm).catch(error => setMessage(`Unexpected alarm deletion failure: ${messageOf(error)}`));
|
|
366
385
|
return true;
|
|
367
386
|
}
|
|
368
387
|
if (input !== 'x') {
|
|
@@ -393,11 +412,7 @@ export function useAlarmTui(params) {
|
|
|
393
412
|
return true;
|
|
394
413
|
}
|
|
395
414
|
if (input === 'g' && alarm) {
|
|
396
|
-
const updated = store.updateAlarm(alarm.id, { reliability: { ...alarm.reliability, keepAwakeUntilAlarm: !alarm.reliability.keepAwakeUntilAlarm } });
|
|
397
|
-
const snapshot = store.snapshot();
|
|
398
|
-
alarmStateRef.current = snapshot.alarms;
|
|
399
|
-
setLibrary(snapshot);
|
|
400
|
-
void syncPersisted(updated, `Alarm Guard ${updated.reliability.keepAwakeUntilAlarm ? 'on' : 'off'} for ${alarm.label}.`);
|
|
415
|
+
void (async () => { const updated = await store.updateAlarm(alarm.id, { reliability: { ...alarm.reliability, keepAwakeUntilAlarm: !alarm.reliability.keepAwakeUntilAlarm } }); const snapshot = store.snapshot(); alarmStateRef.current = snapshot.alarms; setLibrary(snapshot); await syncPersisted(updated, `Alarm Guard ${updated.reliability.keepAwakeUntilAlarm ? 'on' : 'off'} for ${alarm.label}.`); })().catch(error => setMessage(`Alarm save failed: ${messageOf(error)}`));
|
|
401
416
|
return true;
|
|
402
417
|
}
|
|
403
418
|
if (input === 'b' || key.escape) {
|
|
@@ -677,7 +692,7 @@ export function useAlarmTui(params) {
|
|
|
677
692
|
return true;
|
|
678
693
|
}
|
|
679
694
|
return false;
|
|
680
|
-
}, [activeAlarms, deletingId, draft, editSelected, editingField, editorControl,
|
|
695
|
+
}, [activeAlarms, deleteAlarm, deletingId, draft, editSelected, editingField, editorControl, go, library.alarms, openForStation, pickerChoices, pickerFallback, previewStation, refreshActive, refreshRuntime, runVerification, saveDraft, screenRef, selectedRef, service, setLibrary, setMessage, setSelected, snoozeMinutes, store, syncPersisted, timeSegment, verification, visibleFields, weekdayIndex]);
|
|
681
696
|
const openActive = useCallback(() => {
|
|
682
697
|
if (!activeAlarms.length) {
|
|
683
698
|
setMessage('No alarm is currently playing.');
|
|
@@ -689,7 +704,7 @@ export function useAlarmTui(params) {
|
|
|
689
704
|
activeSelectedKeyRef.current = activeAlarms[0]?.key;
|
|
690
705
|
setActiveSelected(0);
|
|
691
706
|
go('alarm-ringing', { resetSelection: true, clearMessage: false });
|
|
692
|
-
}, [activeAlarms, go, setMessage]);
|
|
707
|
+
}, [activeAlarms, go, screenRef, setMessage]);
|
|
693
708
|
return { draft, editorField, editingField, editorControl, timeSegment, weekdayIndex, validationError, saving, pickerChoices, pickerFallback, runtime, verification, activeAlarms, activeSelected, snoozeMinutes, deletingId, busyAlarmIds, itemCount(current) { if (current === 'alarms')
|
|
694
709
|
return library.alarms.length + 2; if (current === 'alarm-editor')
|
|
695
710
|
return visibleFields.length; if (current === 'alarm-picker')
|
package/dist/ui/use-app-input.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
import { useInput } from 'ink';
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import { useInput, usePaste } from 'ink';
|
|
3
3
|
import { homeItems, settingsItemsForPage, settingsPageForRootItem } from './screen-items.js';
|
|
4
|
-
import { applyTextInput, favoriteTarget, isEditableInput, isPlainPrintableInput, mediaTransportActionForInput, searchEditingArrowAction, shouldHandleKeyboardEvent, shouldToggleNearbyLocationShortcut } from './app-state.js';
|
|
4
|
+
import { applyTextInput, favoriteTarget, isEditableInput, isPlainPrintableInput, isSearchFavoriteShortcut, mediaTransportActionForInput, searchEditingArrowAction, shouldHandleKeyboardEvent, shouldTuneSearchResult, shouldToggleNearbyLocationShortcut } from './app-state.js';
|
|
5
5
|
import { parseTerminalMouseEvents, primaryMousePress, wheelScrollDelta } from './terminal-mouse.js';
|
|
6
|
-
import { completeCommand } from './help-content.js';
|
|
6
|
+
import { completeCommand, commandNamesForAirPlay } from './help-content.js';
|
|
7
7
|
import { commitImmediateSelection } from './selection-state.js';
|
|
8
|
-
|
|
8
|
+
import { sanitizeTerminalText } from '../safety.js';
|
|
9
|
+
export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, airPlaySupported, cancelPendingAutoSkip, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, confirmCtrlCExit, copyStationUrl, openStationHomepage, currentItemCount, cycleDisplayColor, cycleAudioOutput, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openAirPlayCode, openAirPlaySettings, openSettingsPage, closeSettingsPage, openScreen, handleAlarmInput, openAlarmForStation, openActiveAlarms, playAdjacent, playStation, player, playingStation, recallSearchHistory, moveExploreCursor, moveExploreCursorToCell, refreshAirPlayTargets, refreshProviderHealth, resetLearnedTransportKeys, repairMcpIntegrations, setAgentIntegrationEnabled, runSearch, saveLearnedTransportKey, screen, screenRef, settingsPageRef, searchQuery, selectedRef, selectedStationForInput, selectAirPlayDeviceAt, setCapturingTransportAction, setAirPlayCode, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, submitAirPlayCode, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleSetting, toggleAgentSetting, toggleNearbyLocation, toggleDirectoryVoting, toggleRadioGarden, toggleSkipBrokenStreams, updateFromSettings }) {
|
|
10
|
+
const reportInputError = useCallback((error) => {
|
|
11
|
+
setMessage(sanitizeTerminalText(error instanceof Error ? error.message : String(error)) ?? 'Could not complete this action.');
|
|
12
|
+
}, [setMessage]);
|
|
9
13
|
const commitSelection = (next) => {
|
|
10
|
-
commitImmediateSelection(selectedRef, setSelected, next, currentItemCount(
|
|
14
|
+
commitImmediateSelection(selectedRef, setSelected, next, currentItemCount(screenRef.current));
|
|
11
15
|
};
|
|
12
16
|
const moveSelection = (delta) => {
|
|
13
|
-
if (hasStationSelection(
|
|
17
|
+
if (hasStationSelection(screenRef.current))
|
|
14
18
|
cancelPendingAutoSkip();
|
|
15
19
|
commitSelection(selectedRef.current + delta);
|
|
16
20
|
};
|
|
17
21
|
useEffect(() => {
|
|
18
|
-
const
|
|
22
|
+
const handleData = (data) => {
|
|
23
|
+
const screen = screenRef.current;
|
|
19
24
|
const rawInput = String(data);
|
|
20
25
|
if (capturingTransportAction) {
|
|
21
26
|
if (rawInput === '\u001B') {
|
|
@@ -63,6 +68,14 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
63
68
|
togglePause();
|
|
64
69
|
}
|
|
65
70
|
};
|
|
71
|
+
const onData = (data) => {
|
|
72
|
+
try {
|
|
73
|
+
handleData(data);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
reportInputError(error);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
66
79
|
stdin.on('data', onData);
|
|
67
80
|
return () => {
|
|
68
81
|
stdin.off('data', onData);
|
|
@@ -78,9 +91,11 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
78
91
|
lastRawTransportAtRef,
|
|
79
92
|
playAdjacent,
|
|
80
93
|
player,
|
|
94
|
+
reportInputError,
|
|
81
95
|
moveExploreCursorToCell,
|
|
82
96
|
saveLearnedTransportKey,
|
|
83
97
|
screen,
|
|
98
|
+
screenRef,
|
|
84
99
|
selectedRef,
|
|
85
100
|
setCapturingTransportAction,
|
|
86
101
|
setMessage,
|
|
@@ -89,7 +104,9 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
89
104
|
stdin,
|
|
90
105
|
togglePause
|
|
91
106
|
]);
|
|
92
|
-
|
|
107
|
+
const handleInput = async (input, key) => {
|
|
108
|
+
const screen = screenRef.current;
|
|
109
|
+
const settingsPage = settingsPageRef.current;
|
|
93
110
|
if (key.ctrl && input === 'c') {
|
|
94
111
|
confirmCtrlCExit();
|
|
95
112
|
return;
|
|
@@ -100,14 +117,17 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
100
117
|
if (capturingTransportAction) {
|
|
101
118
|
return;
|
|
102
119
|
}
|
|
103
|
-
|
|
120
|
+
// Suppress only the mouse/media event already handled by the raw listener.
|
|
121
|
+
// Other keys in the same chunk (or immediately afterward) must still work.
|
|
122
|
+
const editingPlainText = isPlainPrintableInput(input) && (commandMode || (screen === 'search' && editingSearch) || ((screen === 'countries' || screen === 'map') && editingCountryFilter));
|
|
123
|
+
if (!editingPlainText && Date.now() - lastRawTransportAtRef.current < 50 && isRawInputEcho(input, key, settingsRef.current)) {
|
|
104
124
|
return;
|
|
105
125
|
}
|
|
106
126
|
if (commandMode) {
|
|
107
127
|
if (key.return) {
|
|
108
|
-
void executeCommand(commandText);
|
|
109
128
|
setCommandText('');
|
|
110
129
|
setCommandMode(false);
|
|
130
|
+
await executeCommand(commandText);
|
|
111
131
|
return;
|
|
112
132
|
}
|
|
113
133
|
if (key.escape) {
|
|
@@ -117,7 +137,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
117
137
|
}
|
|
118
138
|
if (key.tab) {
|
|
119
139
|
// Complete the command name only while still typing it (no args yet).
|
|
120
|
-
setCommandText(value => (/\s/.test(value) ? value : completeCommand(value)));
|
|
140
|
+
setCommandText(value => (/\s/.test(value) ? value : completeCommand(value, commandNamesForAirPlay(airPlaySupported))));
|
|
121
141
|
return;
|
|
122
142
|
}
|
|
123
143
|
if (isEditableInput(input, key)) {
|
|
@@ -128,17 +148,21 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
128
148
|
if (handleAlarmInput(input, key)) {
|
|
129
149
|
return;
|
|
130
150
|
}
|
|
131
|
-
// An active text field owns
|
|
132
|
-
//
|
|
133
|
-
//
|
|
151
|
+
// An active text field owns ordinary keystrokes before app-wide shortcuts.
|
|
152
|
+
// Dedicated modified shortcuts are handled here so plain query characters
|
|
153
|
+
// never trigger actions or leave Search.
|
|
134
154
|
if (screen === 'search' && editingSearch) {
|
|
155
|
+
if (isSearchFavoriteShortcut(input, key)) {
|
|
156
|
+
toggleFavorite(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
135
159
|
if (key.return) {
|
|
136
160
|
const inputStation = selectedStationForInput();
|
|
137
|
-
if (
|
|
138
|
-
|
|
161
|
+
if (inputStation && shouldTuneSearchResult(searchQuery, lastSubmittedSearchRef.current)) {
|
|
162
|
+
await playStation(inputStation);
|
|
139
163
|
}
|
|
140
164
|
else {
|
|
141
|
-
|
|
165
|
+
await runSearch();
|
|
142
166
|
}
|
|
143
167
|
return;
|
|
144
168
|
}
|
|
@@ -240,7 +264,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
240
264
|
if (input.startsWith(':')) {
|
|
241
265
|
const seed = input.slice(1).replace(/[\r\n]+$/g, '');
|
|
242
266
|
if (/[\r\n]/.test(input)) {
|
|
243
|
-
|
|
267
|
+
await executeCommand(seed);
|
|
244
268
|
}
|
|
245
269
|
else {
|
|
246
270
|
setCommandMode(true);
|
|
@@ -372,11 +396,11 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
372
396
|
return;
|
|
373
397
|
}
|
|
374
398
|
if (input === 'O') {
|
|
375
|
-
openStationHomepage(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
399
|
+
await openStationHomepage(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
376
400
|
return;
|
|
377
401
|
}
|
|
378
402
|
if (input === 'y') {
|
|
379
|
-
|
|
403
|
+
await copyStationUrl(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
380
404
|
return;
|
|
381
405
|
}
|
|
382
406
|
if (input === ' ') {
|
|
@@ -418,12 +442,12 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
418
442
|
if (screen === 'countries') {
|
|
419
443
|
const country = filteredCountries[selectedRef.current];
|
|
420
444
|
if (country) {
|
|
421
|
-
|
|
445
|
+
await loadCountry(country);
|
|
422
446
|
}
|
|
423
447
|
return;
|
|
424
448
|
}
|
|
425
449
|
if (screen === 'settings') {
|
|
426
|
-
const item = settingsItemsForPage(settingsPage)[selectedRef.current];
|
|
450
|
+
const item = settingsItemsForPage(settingsPage, airPlaySupported)[selectedRef.current];
|
|
427
451
|
if (settingsPage === 'root') {
|
|
428
452
|
const targetPage = settingsPageForRootItem(item);
|
|
429
453
|
if (targetPage) {
|
|
@@ -483,10 +507,10 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
483
507
|
toggleSetting('automaticUpdateChecks');
|
|
484
508
|
}
|
|
485
509
|
else if (item === 'Allow local agent control') {
|
|
486
|
-
|
|
510
|
+
await setAgentIntegrationEnabled();
|
|
487
511
|
}
|
|
488
512
|
else if (item === 'Install or repair MCP integrations') {
|
|
489
|
-
|
|
513
|
+
await repairMcpIntegrations();
|
|
490
514
|
}
|
|
491
515
|
else if (item === 'Open TUI for agent playback') {
|
|
492
516
|
toggleAgentSetting('openUiOnPlay');
|
|
@@ -498,12 +522,12 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
498
522
|
setCommandText('export ');
|
|
499
523
|
setCommandMode(true);
|
|
500
524
|
}
|
|
501
|
-
else if (item === '
|
|
502
|
-
setCommandText('
|
|
525
|
+
else if (item === 'Restore preferences and library') {
|
|
526
|
+
setCommandText('restore ');
|
|
503
527
|
setCommandMode(true);
|
|
504
528
|
}
|
|
505
529
|
else if (item === 'Check for updates') {
|
|
506
|
-
|
|
530
|
+
await updateFromSettings();
|
|
507
531
|
}
|
|
508
532
|
else if (item === 'Refresh provider health') {
|
|
509
533
|
refreshProviderHealth();
|
|
@@ -530,17 +554,47 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
530
554
|
if (screen === 'map') {
|
|
531
555
|
const country = filteredCountries[selectedRef.current];
|
|
532
556
|
if (country) {
|
|
533
|
-
|
|
557
|
+
await loadCountry(country);
|
|
534
558
|
}
|
|
535
559
|
return;
|
|
536
560
|
}
|
|
537
561
|
const inputStation = selectedStationForInput();
|
|
538
562
|
if (inputStation) {
|
|
539
|
-
|
|
563
|
+
await playStation(inputStation);
|
|
540
564
|
}
|
|
541
565
|
}
|
|
566
|
+
};
|
|
567
|
+
useInput((input, key) => {
|
|
568
|
+
// Ink preserves non-escape text chunks for paste, including repeated Tab
|
|
569
|
+
// bytes. Only a chunk consisting entirely of Tabs represents navigation.
|
|
570
|
+
if (/^\t{2,}$/.test(input)) {
|
|
571
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
572
|
+
void handleInput('', { ...key, tab: true }).catch(reportInputError);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
void handleInput(input, key).catch(reportInputError);
|
|
577
|
+
});
|
|
578
|
+
// Keep bracketed paste on its own channel so pasted Tabs stay text. The
|
|
579
|
+
// existing text/command handling still receives each complete pasted string.
|
|
580
|
+
usePaste(input => {
|
|
581
|
+
void handleInput(input, pasteInputKey).catch(reportInputError);
|
|
542
582
|
});
|
|
543
583
|
}
|
|
584
|
+
const pasteInputKey = {
|
|
585
|
+
upArrow: false, downArrow: false, leftArrow: false, rightArrow: false,
|
|
586
|
+
pageDown: false, pageUp: false, home: false, end: false,
|
|
587
|
+
return: false, escape: false, ctrl: false, shift: false, tab: false,
|
|
588
|
+
backspace: false, delete: false, meta: false,
|
|
589
|
+
super: false, hyper: false, capsLock: false, numLock: false
|
|
590
|
+
};
|
|
591
|
+
function isRawInputEcho(input, key, settings) {
|
|
592
|
+
return Boolean(mediaTransportActionForInput(input, settings.mediaKeys) ||
|
|
593
|
+
mediaTransportActionForInput(`\u001B${input}`, settings.mediaKeys) ||
|
|
594
|
+
((key.shift || key.ctrl || key.meta) && (key.leftArrow || key.rightArrow)) ||
|
|
595
|
+
/^\[<\d+;\d+;\d+[Mm]$/.test(input) ||
|
|
596
|
+
input.startsWith('[M'));
|
|
597
|
+
}
|
|
544
598
|
function shouldScrollSelectionWithWheel(screen, commandMode, editingCountryFilter) {
|
|
545
599
|
if (commandMode || editingCountryFilter) {
|
|
546
600
|
return false;
|