@moldable-ai/ui 0.2.18 → 0.2.19
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/dist/components/chat/chat-panel.d.ts +18 -12
- package/dist/components/chat/chat-panel.d.ts.map +1 -1
- package/dist/components/chat/chat-panel.js +73 -19
- package/dist/components/chat/index.d.ts +2 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +2 -1
- package/dist/components/chat/model-effort-power-effects.d.ts +7 -0
- package/dist/components/chat/model-effort-power-effects.d.ts.map +1 -0
- package/dist/components/chat/model-effort-power-effects.js +54 -0
- package/dist/components/chat/model-effort-selector.d.ts +26 -0
- package/dist/components/chat/model-effort-selector.d.ts.map +1 -0
- package/dist/components/chat/model-effort-selector.js +179 -0
- package/dist/components/chat/model-effort-ultra-effects.d.ts +5 -0
- package/dist/components/chat/model-effort-ultra-effects.d.ts.map +1 -0
- package/dist/components/chat/model-effort-ultra-effects.js +56 -0
- package/dist/components/hardware/bluetooth-panel-helpers.d.ts +9 -0
- package/dist/components/hardware/bluetooth-panel-helpers.d.ts.map +1 -0
- package/dist/components/hardware/bluetooth-panel-helpers.js +37 -0
- package/dist/components/hardware/bluetooth-panel.d.ts +56 -0
- package/dist/components/hardware/bluetooth-panel.d.ts.map +1 -0
- package/dist/components/hardware/bluetooth-panel.js +470 -0
- package/dist/components/hardware/camera-preview.d.ts +39 -0
- package/dist/components/hardware/camera-preview.d.ts.map +1 -0
- package/dist/components/hardware/camera-preview.js +228 -0
- package/dist/components/hardware/capability-badge.d.ts +23 -0
- package/dist/components/hardware/capability-badge.d.ts.map +1 -0
- package/dist/components/hardware/capability-badge.js +39 -0
- package/dist/components/hardware/capability-matrix.d.ts +14 -0
- package/dist/components/hardware/capability-matrix.d.ts.map +1 -0
- package/dist/components/hardware/capability-matrix.js +97 -0
- package/dist/components/hardware/clipboard-button.d.ts +26 -0
- package/dist/components/hardware/clipboard-button.d.ts.map +1 -0
- package/dist/components/hardware/clipboard-button.js +114 -0
- package/dist/components/hardware/device-io-controls.d.ts +4 -0
- package/dist/components/hardware/device-io-controls.d.ts.map +1 -0
- package/dist/components/hardware/device-io-controls.js +28 -0
- package/dist/components/hardware/device-list.d.ts +20 -0
- package/dist/components/hardware/device-list.d.ts.map +1 -0
- package/dist/components/hardware/device-list.js +14 -0
- package/dist/components/hardware/display-map.d.ts +23 -0
- package/dist/components/hardware/display-map.d.ts.map +1 -0
- package/dist/components/hardware/display-map.js +114 -0
- package/dist/components/hardware/haptic-button.d.ts +26 -0
- package/dist/components/hardware/haptic-button.d.ts.map +1 -0
- package/dist/components/hardware/haptic-button.js +94 -0
- package/dist/components/hardware/hardware-panel.d.ts +11 -0
- package/dist/components/hardware/hardware-panel.d.ts.map +1 -0
- package/dist/components/hardware/hardware-panel.js +28 -0
- package/dist/components/hardware/hid-panel.d.ts +40 -0
- package/dist/components/hardware/hid-panel.d.ts.map +1 -0
- package/dist/components/hardware/hid-panel.js +373 -0
- package/dist/components/hardware/index.d.ts +27 -0
- package/dist/components/hardware/index.d.ts.map +1 -0
- package/dist/components/hardware/index.js +26 -0
- package/dist/components/hardware/live-indicator.d.ts +15 -0
- package/dist/components/hardware/live-indicator.d.ts.map +1 -0
- package/dist/components/hardware/live-indicator.js +26 -0
- package/dist/components/hardware/local-auth-button.d.ts +33 -0
- package/dist/components/hardware/local-auth-button.d.ts.map +1 -0
- package/dist/components/hardware/local-auth-button.js +115 -0
- package/dist/components/hardware/location-panel.d.ts +29 -0
- package/dist/components/hardware/location-panel.d.ts.map +1 -0
- package/dist/components/hardware/location-panel.js +154 -0
- package/dist/components/hardware/microphone-meter.d.ts +35 -0
- package/dist/components/hardware/microphone-meter.d.ts.map +1 -0
- package/dist/components/hardware/microphone-meter.js +244 -0
- package/dist/components/hardware/midi-monitor.d.ts +54 -0
- package/dist/components/hardware/midi-monitor.d.ts.map +1 -0
- package/dist/components/hardware/midi-monitor.js +534 -0
- package/dist/components/hardware/notification-panel.d.ts +24 -0
- package/dist/components/hardware/notification-panel.d.ts.map +1 -0
- package/dist/components/hardware/notification-panel.js +124 -0
- package/dist/components/hardware/power-blocker-controller.d.ts +19 -0
- package/dist/components/hardware/power-blocker-controller.d.ts.map +1 -0
- package/dist/components/hardware/power-blocker-controller.js +83 -0
- package/dist/components/hardware/power-panel.d.ts +67 -0
- package/dist/components/hardware/power-panel.d.ts.map +1 -0
- package/dist/components/hardware/power-panel.js +309 -0
- package/dist/components/hardware/readout.d.ts +10 -0
- package/dist/components/hardware/readout.d.ts.map +1 -0
- package/dist/components/hardware/readout.js +11 -0
- package/dist/components/hardware/screen-share-preview.d.ts +36 -0
- package/dist/components/hardware/screen-share-preview.d.ts.map +1 -0
- package/dist/components/hardware/screen-share-preview.js +191 -0
- package/dist/components/hardware/secure-storage-panel.d.ts +36 -0
- package/dist/components/hardware/secure-storage-panel.d.ts.map +1 -0
- package/dist/components/hardware/secure-storage-panel.js +193 -0
- package/dist/components/hardware/serial-console.d.ts +64 -0
- package/dist/components/hardware/serial-console.d.ts.map +1 -0
- package/dist/components/hardware/serial-console.js +399 -0
- package/dist/components/hardware/shortcut-recorder.d.ts +35 -0
- package/dist/components/hardware/shortcut-recorder.d.ts.map +1 -0
- package/dist/components/hardware/shortcut-recorder.js +217 -0
- package/dist/components/hardware/stream-log.d.ts +35 -0
- package/dist/components/hardware/stream-log.d.ts.map +1 -0
- package/dist/components/hardware/stream-log.js +65 -0
- package/dist/components/hardware/system-audio-monitor.d.ts +34 -0
- package/dist/components/hardware/system-audio-monitor.d.ts.map +1 -0
- package/dist/components/hardware/system-audio-monitor.js +275 -0
- package/dist/components/hardware/usb-panel.d.ts +54 -0
- package/dist/components/hardware/usb-panel.d.ts.map +1 -0
- package/dist/components/hardware/usb-panel.js +363 -0
- package/dist/components/hardware/use-capability.d.ts +20 -0
- package/dist/components/hardware/use-capability.d.ts.map +1 -0
- package/dist/components/hardware/use-capability.js +61 -0
- package/dist/components/ui/input-group.d.ts +1 -1
- package/dist/components/ui/slider.d.ts +4 -1
- package/dist/components/ui/slider.d.ts.map +1 -1
- package/dist/components/ui/slider.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/native-hardware/bluetooth-access.d.ts +14 -0
- package/dist/lib/native-hardware/bluetooth-access.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-access.js +40 -0
- package/dist/lib/native-hardware/bluetooth-connection.d.ts +19 -0
- package/dist/lib/native-hardware/bluetooth-connection.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-connection.js +88 -0
- package/dist/lib/native-hardware/bluetooth-gatt.d.ts +7 -0
- package/dist/lib/native-hardware/bluetooth-gatt.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-gatt.js +77 -0
- package/dist/lib/native-hardware/bluetooth-internal.d.ts +10 -0
- package/dist/lib/native-hardware/bluetooth-internal.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-internal.js +105 -0
- package/dist/lib/native-hardware/bluetooth-native-fallback.d.ts +33 -0
- package/dist/lib/native-hardware/bluetooth-native-fallback.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-native-fallback.js +214 -0
- package/dist/lib/native-hardware/bluetooth-types.d.ts +85 -0
- package/dist/lib/native-hardware/bluetooth-types.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-types.js +2 -0
- package/dist/lib/native-hardware/bluetooth.d.ts +7 -0
- package/dist/lib/native-hardware/bluetooth.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth.js +4 -0
- package/dist/lib/native-hardware/bridge.d.ts +23 -0
- package/dist/lib/native-hardware/bridge.d.ts.map +1 -0
- package/dist/lib/native-hardware/bridge.js +266 -0
- package/dist/lib/native-hardware/capabilities.d.ts +7 -0
- package/dist/lib/native-hardware/capabilities.d.ts.map +1 -0
- package/dist/lib/native-hardware/capabilities.js +95 -0
- package/dist/lib/native-hardware/clipboard.d.ts +11 -0
- package/dist/lib/native-hardware/clipboard.d.ts.map +1 -0
- package/dist/lib/native-hardware/clipboard.js +55 -0
- package/dist/lib/native-hardware/device-io-errors.d.ts +11 -0
- package/dist/lib/native-hardware/device-io-errors.d.ts.map +1 -0
- package/dist/lib/native-hardware/device-io-errors.js +82 -0
- package/dist/lib/native-hardware/displays.d.ts +38 -0
- package/dist/lib/native-hardware/displays.d.ts.map +1 -0
- package/dist/lib/native-hardware/displays.js +12 -0
- package/dist/lib/native-hardware/global-shortcuts.d.ts +86 -0
- package/dist/lib/native-hardware/global-shortcuts.d.ts.map +1 -0
- package/dist/lib/native-hardware/global-shortcuts.js +351 -0
- package/dist/lib/native-hardware/haptics.d.ts +25 -0
- package/dist/lib/native-hardware/haptics.d.ts.map +1 -0
- package/dist/lib/native-hardware/haptics.js +18 -0
- package/dist/lib/native-hardware/hid-native.d.ts +26 -0
- package/dist/lib/native-hardware/hid-native.d.ts.map +1 -0
- package/dist/lib/native-hardware/hid-native.js +229 -0
- package/dist/lib/native-hardware/hid.d.ts +52 -0
- package/dist/lib/native-hardware/hid.d.ts.map +1 -0
- package/dist/lib/native-hardware/hid.js +160 -0
- package/dist/lib/native-hardware/index.d.ts +38 -0
- package/dist/lib/native-hardware/index.d.ts.map +1 -0
- package/dist/lib/native-hardware/index.js +20 -0
- package/dist/lib/native-hardware/local-authentication.d.ts +32 -0
- package/dist/lib/native-hardware/local-authentication.d.ts.map +1 -0
- package/dist/lib/native-hardware/local-authentication.js +46 -0
- package/dist/lib/native-hardware/location.d.ts +49 -0
- package/dist/lib/native-hardware/location.d.ts.map +1 -0
- package/dist/lib/native-hardware/location.js +114 -0
- package/dist/lib/native-hardware/media-permissions.d.ts +28 -0
- package/dist/lib/native-hardware/media-permissions.d.ts.map +1 -0
- package/dist/lib/native-hardware/media-permissions.js +37 -0
- package/dist/lib/native-hardware/media.d.ts +17 -0
- package/dist/lib/native-hardware/media.d.ts.map +1 -0
- package/dist/lib/native-hardware/media.js +99 -0
- package/dist/lib/native-hardware/midi-access.d.ts +13 -0
- package/dist/lib/native-hardware/midi-access.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-access.js +66 -0
- package/dist/lib/native-hardware/midi-internal.d.ts +12 -0
- package/dist/lib/native-hardware/midi-internal.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-internal.js +88 -0
- package/dist/lib/native-hardware/midi-native-fallback.d.ts +27 -0
- package/dist/lib/native-hardware/midi-native-fallback.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-native-fallback.js +127 -0
- package/dist/lib/native-hardware/midi-ports.d.ts +6 -0
- package/dist/lib/native-hardware/midi-ports.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-ports.js +115 -0
- package/dist/lib/native-hardware/midi-types.d.ts +25 -0
- package/dist/lib/native-hardware/midi-types.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-types.js +1 -0
- package/dist/lib/native-hardware/midi.d.ts +6 -0
- package/dist/lib/native-hardware/midi.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi.js +3 -0
- package/dist/lib/native-hardware/native-device-io.d.ts +28 -0
- package/dist/lib/native-hardware/native-device-io.d.ts.map +1 -0
- package/dist/lib/native-hardware/native-device-io.js +91 -0
- package/dist/lib/native-hardware/native-device-lifecycle.d.ts +13 -0
- package/dist/lib/native-hardware/native-device-lifecycle.d.ts.map +1 -0
- package/dist/lib/native-hardware/native-device-lifecycle.js +40 -0
- package/dist/lib/native-hardware/notifications.d.ts +38 -0
- package/dist/lib/native-hardware/notifications.d.ts.map +1 -0
- package/dist/lib/native-hardware/notifications.js +50 -0
- package/dist/lib/native-hardware/power-session.d.ts +113 -0
- package/dist/lib/native-hardware/power-session.d.ts.map +1 -0
- package/dist/lib/native-hardware/power-session.js +264 -0
- package/dist/lib/native-hardware/secure-storage.d.ts +36 -0
- package/dist/lib/native-hardware/secure-storage.d.ts.map +1 -0
- package/dist/lib/native-hardware/secure-storage.js +64 -0
- package/dist/lib/native-hardware/serial-native.d.ts +23 -0
- package/dist/lib/native-hardware/serial-native.d.ts.map +1 -0
- package/dist/lib/native-hardware/serial-native.js +181 -0
- package/dist/lib/native-hardware/serial.d.ts +76 -0
- package/dist/lib/native-hardware/serial.d.ts.map +1 -0
- package/dist/lib/native-hardware/serial.js +212 -0
- package/dist/lib/native-hardware/system-audio.d.ts +127 -0
- package/dist/lib/native-hardware/system-audio.d.ts.map +1 -0
- package/dist/lib/native-hardware/system-audio.js +330 -0
- package/dist/lib/native-hardware/types.d.ts +62 -0
- package/dist/lib/native-hardware/types.d.ts.map +1 -0
- package/dist/lib/native-hardware/types.js +27 -0
- package/dist/lib/native-hardware/usb-native.d.ts +39 -0
- package/dist/lib/native-hardware/usb-native.d.ts.map +1 -0
- package/dist/lib/native-hardware/usb-native.js +217 -0
- package/dist/lib/native-hardware/usb.d.ts +85 -0
- package/dist/lib/native-hardware/usb.d.ts.map +1 -0
- package/dist/lib/native-hardware/usb.js +222 -0
- package/package.json +1 -1
- package/src/styles/index.css +377 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Cable, CornerDownLeft, Plug, RefreshCw, ShieldX, Unplug, } from 'lucide-react';
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { closeMoldableSerialPort, forgetMoldableSerialPort, getMoldableSerialSignals, openMoldableSerialPort, readMoldableSerialPort, requestMoldableSerialPort, setMoldableSerialSignals, writeMoldableSerialPort, } from '../../lib/native-hardware';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
import { Input } from '../ui/input';
|
|
8
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../ui/select';
|
|
9
|
+
import { Spinner } from '../ui/spinner';
|
|
10
|
+
import { Switch } from '../ui/switch';
|
|
11
|
+
import { CapabilityBadge } from './capability-badge';
|
|
12
|
+
import { HardwarePanel, HardwarePanelAction, HardwarePanelContent, HardwarePanelFooter, HardwarePanelHeader, HardwarePanelIcon, HardwarePanelTitle, } from './hardware-panel';
|
|
13
|
+
import { LiveIndicator } from './live-indicator';
|
|
14
|
+
import { StreamLog, useStreamLog } from './stream-log';
|
|
15
|
+
const BAUD_RATES = [9600, 19200, 38400, 57600, 115200, 230400];
|
|
16
|
+
const DEFAULT_BAUD_RATE = 115200;
|
|
17
|
+
function toError(cause) {
|
|
18
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
19
|
+
}
|
|
20
|
+
function formatHexId(value) {
|
|
21
|
+
return `0x${value.toString(16).padStart(4, '0')}`;
|
|
22
|
+
}
|
|
23
|
+
function formatSerialPortLabel(info) {
|
|
24
|
+
if (info?.usbVendorId !== undefined && info.usbProductId !== undefined) {
|
|
25
|
+
return `${formatHexId(info.usbVendorId)}:${formatHexId(info.usbProductId)}`;
|
|
26
|
+
}
|
|
27
|
+
if (info?.usbVendorId !== undefined)
|
|
28
|
+
return formatHexId(info.usbVendorId);
|
|
29
|
+
return 'Serial port';
|
|
30
|
+
}
|
|
31
|
+
function formatHexBytes(bytes) {
|
|
32
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(' ');
|
|
33
|
+
}
|
|
34
|
+
const SIGNALS_OFF = {
|
|
35
|
+
dataTerminalReady: false,
|
|
36
|
+
requestToSend: false,
|
|
37
|
+
break: false,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Serial state machine behind `SerialConsole`: request → open → streaming
|
|
41
|
+
* read loop with UTF-8 decode, line sends, DTR/RTS control, RX/TX log.
|
|
42
|
+
* Use directly to build custom serial UI on the same lifecycle handling.
|
|
43
|
+
*/
|
|
44
|
+
export function useMoldableSerial(options = {}) {
|
|
45
|
+
const [status, setStatus] = useState('idle');
|
|
46
|
+
const [port, setPort] = useState(null);
|
|
47
|
+
const [portInfo, setPortInfo] = useState(null);
|
|
48
|
+
const [error, setError] = useState(null);
|
|
49
|
+
const [signals, setSignalsState] = useState(SIGNALS_OFF);
|
|
50
|
+
const [inputSignals, setInputSignals] = useState(null);
|
|
51
|
+
const [rxBytes, setRxBytes] = useState(0);
|
|
52
|
+
const [txBytes, setTxBytes] = useState(0);
|
|
53
|
+
const { entries, append, clear: clearEntries, } = useStreamLog(options.maxEntries ?? 500);
|
|
54
|
+
const portRef = useRef(null);
|
|
55
|
+
const portOpenRef = useRef(false);
|
|
56
|
+
const abortRef = useRef(null);
|
|
57
|
+
const readLoopRef = useRef(null);
|
|
58
|
+
const disconnectListenerRef = useRef(null);
|
|
59
|
+
const tokenRef = useRef(0);
|
|
60
|
+
const signalsRef = useRef(SIGNALS_OFF);
|
|
61
|
+
const optionsRef = useRef(options);
|
|
62
|
+
optionsRef.current = options;
|
|
63
|
+
const clearPort = useCallback((active) => {
|
|
64
|
+
if (portRef.current !== active)
|
|
65
|
+
return;
|
|
66
|
+
const registered = disconnectListenerRef.current;
|
|
67
|
+
if (registered?.port === active) {
|
|
68
|
+
active.removeEventListener('disconnect', registered.listener);
|
|
69
|
+
disconnectListenerRef.current = null;
|
|
70
|
+
}
|
|
71
|
+
portRef.current = null;
|
|
72
|
+
portOpenRef.current = false;
|
|
73
|
+
signalsRef.current = SIGNALS_OFF;
|
|
74
|
+
setSignalsState(SIGNALS_OFF);
|
|
75
|
+
setInputSignals(null);
|
|
76
|
+
setPort(null);
|
|
77
|
+
setPortInfo(null);
|
|
78
|
+
}, []);
|
|
79
|
+
const teardown = useCallback(async (forgetPermission = false) => {
|
|
80
|
+
abortRef.current?.abort();
|
|
81
|
+
abortRef.current = null;
|
|
82
|
+
const loop = readLoopRef.current;
|
|
83
|
+
readLoopRef.current = null;
|
|
84
|
+
if (loop)
|
|
85
|
+
await loop.catch(() => undefined);
|
|
86
|
+
const active = portRef.current;
|
|
87
|
+
if (!active)
|
|
88
|
+
return;
|
|
89
|
+
if (portOpenRef.current) {
|
|
90
|
+
await closeMoldableSerialPort(active);
|
|
91
|
+
portOpenRef.current = false;
|
|
92
|
+
}
|
|
93
|
+
if (forgetPermission)
|
|
94
|
+
await forgetMoldableSerialPort(active);
|
|
95
|
+
clearPort(active);
|
|
96
|
+
}, [clearPort]);
|
|
97
|
+
const refreshSignals = useCallback(async () => {
|
|
98
|
+
const active = portRef.current;
|
|
99
|
+
if (!active || !portOpenRef.current)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
setInputSignals(await getMoldableSerialSignals(active));
|
|
103
|
+
}
|
|
104
|
+
catch (cause) {
|
|
105
|
+
const failure = toError(cause);
|
|
106
|
+
setError(failure);
|
|
107
|
+
append({ direction: 'error', data: failure.message });
|
|
108
|
+
}
|
|
109
|
+
}, [append]);
|
|
110
|
+
const runReadLoop = useCallback(async (activePort, token, controller) => {
|
|
111
|
+
const decoder = new TextDecoder();
|
|
112
|
+
try {
|
|
113
|
+
for (;;) {
|
|
114
|
+
const result = await readMoldableSerialPort(activePort, {
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
if (tokenRef.current !== token)
|
|
118
|
+
return;
|
|
119
|
+
const bytes = result.value;
|
|
120
|
+
if (bytes && bytes.byteLength > 0) {
|
|
121
|
+
setRxBytes((count) => count + bytes.byteLength);
|
|
122
|
+
optionsRef.current.onData?.(bytes);
|
|
123
|
+
const text = decoder.decode(bytes, { stream: true });
|
|
124
|
+
if (text.length > 0)
|
|
125
|
+
append({ direction: 'in', data: text });
|
|
126
|
+
}
|
|
127
|
+
if (result.done) {
|
|
128
|
+
append({ direction: 'info', data: 'Serial stream ended' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (cause) {
|
|
134
|
+
if (tokenRef.current !== token || controller.signal.aborted)
|
|
135
|
+
return;
|
|
136
|
+
const failure = toError(cause);
|
|
137
|
+
setError(failure);
|
|
138
|
+
setStatus('error');
|
|
139
|
+
append({ direction: 'error', data: failure.message });
|
|
140
|
+
}
|
|
141
|
+
}, [append]);
|
|
142
|
+
const connect = useCallback(async (overrides = {}) => {
|
|
143
|
+
const token = ++tokenRef.current;
|
|
144
|
+
setStatus('connecting');
|
|
145
|
+
setError(null);
|
|
146
|
+
try {
|
|
147
|
+
await teardown();
|
|
148
|
+
if (tokenRef.current !== token)
|
|
149
|
+
return;
|
|
150
|
+
const filters = overrides.filters ?? optionsRef.current.filters;
|
|
151
|
+
const requested = await requestMoldableSerialPort({
|
|
152
|
+
...(filters ? { filters } : {}),
|
|
153
|
+
...(optionsRef.current.selectNativePort
|
|
154
|
+
? { selectNativePort: optionsRef.current.selectNativePort }
|
|
155
|
+
: {}),
|
|
156
|
+
});
|
|
157
|
+
if (tokenRef.current !== token)
|
|
158
|
+
return;
|
|
159
|
+
const baudRate = overrides.baudRate ?? optionsRef.current.baudRate ?? DEFAULT_BAUD_RATE;
|
|
160
|
+
await openMoldableSerialPort(requested, {
|
|
161
|
+
baudRate,
|
|
162
|
+
dataBits: 8,
|
|
163
|
+
stopBits: 1,
|
|
164
|
+
parity: 'none',
|
|
165
|
+
flowControl: 'none',
|
|
166
|
+
});
|
|
167
|
+
if (tokenRef.current !== token) {
|
|
168
|
+
portRef.current = requested;
|
|
169
|
+
portOpenRef.current = true;
|
|
170
|
+
try {
|
|
171
|
+
await closeMoldableSerialPort(requested);
|
|
172
|
+
clearPort(requested);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
setPort(requested);
|
|
176
|
+
setPortInfo(requested.getInfo());
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const handleDisconnect = () => {
|
|
181
|
+
if (tokenRef.current !== token)
|
|
182
|
+
return;
|
|
183
|
+
tokenRef.current++;
|
|
184
|
+
abortRef.current?.abort();
|
|
185
|
+
abortRef.current = null;
|
|
186
|
+
const loop = readLoopRef.current;
|
|
187
|
+
readLoopRef.current = null;
|
|
188
|
+
void Promise.resolve(loop)
|
|
189
|
+
.catch(() => undefined)
|
|
190
|
+
.then(async () => {
|
|
191
|
+
try {
|
|
192
|
+
if (portOpenRef.current) {
|
|
193
|
+
await closeMoldableSerialPort(requested);
|
|
194
|
+
portOpenRef.current = false;
|
|
195
|
+
}
|
|
196
|
+
clearPort(requested);
|
|
197
|
+
const failure = new Error('Serial port disconnected.');
|
|
198
|
+
setError(failure);
|
|
199
|
+
setStatus('error');
|
|
200
|
+
append({ direction: 'error', data: failure.message });
|
|
201
|
+
}
|
|
202
|
+
catch (cause) {
|
|
203
|
+
const failure = toError(cause);
|
|
204
|
+
setError(failure);
|
|
205
|
+
setStatus('error');
|
|
206
|
+
append({ direction: 'error', data: failure.message });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
requested.addEventListener('disconnect', handleDisconnect, {
|
|
211
|
+
once: true,
|
|
212
|
+
});
|
|
213
|
+
disconnectListenerRef.current = {
|
|
214
|
+
port: requested,
|
|
215
|
+
listener: handleDisconnect,
|
|
216
|
+
};
|
|
217
|
+
portRef.current = requested;
|
|
218
|
+
portOpenRef.current = true;
|
|
219
|
+
setPort(requested);
|
|
220
|
+
setPortInfo(requested.getInfo());
|
|
221
|
+
setStatus('connected');
|
|
222
|
+
append({ direction: 'info', data: `Connected · ${baudRate} baud` });
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
abortRef.current = controller;
|
|
225
|
+
readLoopRef.current = runReadLoop(requested, token, controller);
|
|
226
|
+
void refreshSignals();
|
|
227
|
+
}
|
|
228
|
+
catch (cause) {
|
|
229
|
+
if (tokenRef.current !== token)
|
|
230
|
+
return;
|
|
231
|
+
const failure = toError(cause);
|
|
232
|
+
setError(failure);
|
|
233
|
+
setStatus('error');
|
|
234
|
+
append({ direction: 'error', data: failure.message });
|
|
235
|
+
}
|
|
236
|
+
}, [append, clearPort, refreshSignals, runReadLoop, teardown]);
|
|
237
|
+
const disconnect = useCallback(async () => {
|
|
238
|
+
tokenRef.current++;
|
|
239
|
+
try {
|
|
240
|
+
await teardown();
|
|
241
|
+
setStatus('idle');
|
|
242
|
+
setError(null);
|
|
243
|
+
}
|
|
244
|
+
catch (cause) {
|
|
245
|
+
const failure = toError(cause);
|
|
246
|
+
setError(failure);
|
|
247
|
+
setStatus('error');
|
|
248
|
+
append({ direction: 'error', data: failure.message });
|
|
249
|
+
}
|
|
250
|
+
}, [append, teardown]);
|
|
251
|
+
const forget = useCallback(async () => {
|
|
252
|
+
tokenRef.current++;
|
|
253
|
+
try {
|
|
254
|
+
await teardown(true);
|
|
255
|
+
setStatus('idle');
|
|
256
|
+
setError(null);
|
|
257
|
+
append({ direction: 'info', data: 'Serial permission forgotten' });
|
|
258
|
+
}
|
|
259
|
+
catch (cause) {
|
|
260
|
+
const failure = toError(cause);
|
|
261
|
+
setError(failure);
|
|
262
|
+
setStatus('error');
|
|
263
|
+
append({ direction: 'error', data: failure.message });
|
|
264
|
+
}
|
|
265
|
+
}, [append, teardown]);
|
|
266
|
+
const send = useCallback(async (data) => {
|
|
267
|
+
const active = portRef.current;
|
|
268
|
+
if (!active)
|
|
269
|
+
return;
|
|
270
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(`${data}\n`) : data;
|
|
271
|
+
try {
|
|
272
|
+
await writeMoldableSerialPort(active, bytes);
|
|
273
|
+
setTxBytes((count) => count + bytes.byteLength);
|
|
274
|
+
append({
|
|
275
|
+
direction: 'out',
|
|
276
|
+
data: typeof data === 'string' ? data : formatHexBytes(data),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (cause) {
|
|
280
|
+
const failure = toError(cause);
|
|
281
|
+
setError(failure);
|
|
282
|
+
append({ direction: 'error', data: failure.message });
|
|
283
|
+
}
|
|
284
|
+
}, [append]);
|
|
285
|
+
const setSignals = useCallback(async (next) => {
|
|
286
|
+
const active = portRef.current;
|
|
287
|
+
if (!active)
|
|
288
|
+
return;
|
|
289
|
+
try {
|
|
290
|
+
await setMoldableSerialSignals(active, next);
|
|
291
|
+
signalsRef.current = {
|
|
292
|
+
dataTerminalReady: next.dataTerminalReady ?? signalsRef.current.dataTerminalReady,
|
|
293
|
+
requestToSend: next.requestToSend ?? signalsRef.current.requestToSend,
|
|
294
|
+
break: next.break ?? signalsRef.current.break,
|
|
295
|
+
};
|
|
296
|
+
setSignalsState(signalsRef.current);
|
|
297
|
+
void refreshSignals();
|
|
298
|
+
}
|
|
299
|
+
catch (cause) {
|
|
300
|
+
const failure = toError(cause);
|
|
301
|
+
setError(failure);
|
|
302
|
+
append({ direction: 'error', data: failure.message });
|
|
303
|
+
}
|
|
304
|
+
}, [append, refreshSignals]);
|
|
305
|
+
useEffect(() => () => {
|
|
306
|
+
tokenRef.current++;
|
|
307
|
+
abortRef.current?.abort();
|
|
308
|
+
abortRef.current = null;
|
|
309
|
+
const loop = readLoopRef.current;
|
|
310
|
+
readLoopRef.current = null;
|
|
311
|
+
const active = portRef.current;
|
|
312
|
+
if (active) {
|
|
313
|
+
const registered = disconnectListenerRef.current;
|
|
314
|
+
if (registered?.port === active) {
|
|
315
|
+
active.removeEventListener('disconnect', registered.listener);
|
|
316
|
+
disconnectListenerRef.current = null;
|
|
317
|
+
}
|
|
318
|
+
void Promise.resolve(loop)
|
|
319
|
+
.catch(() => undefined)
|
|
320
|
+
.then(async () => {
|
|
321
|
+
if (portOpenRef.current)
|
|
322
|
+
await closeMoldableSerialPort(active);
|
|
323
|
+
})
|
|
324
|
+
.catch(() => undefined);
|
|
325
|
+
}
|
|
326
|
+
}, []);
|
|
327
|
+
return {
|
|
328
|
+
status,
|
|
329
|
+
port,
|
|
330
|
+
portInfo,
|
|
331
|
+
error,
|
|
332
|
+
signals,
|
|
333
|
+
inputSignals,
|
|
334
|
+
entries,
|
|
335
|
+
rxBytes,
|
|
336
|
+
txBytes,
|
|
337
|
+
connect,
|
|
338
|
+
disconnect,
|
|
339
|
+
forget,
|
|
340
|
+
send,
|
|
341
|
+
setSignals,
|
|
342
|
+
refreshSignals,
|
|
343
|
+
clearEntries,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Serial console panel: baud-rate chooser, connect/disconnect lifecycle,
|
|
348
|
+
* auto-following RX/TX log, line composer, signal controls, byte counters.
|
|
349
|
+
*/
|
|
350
|
+
function SerialConsole({ className, baudRate = DEFAULT_BAUD_RATE, filters, selectNativePort, onData, ...props }) {
|
|
351
|
+
const [nativePortChoices, setNativePortChoices] = useState([]);
|
|
352
|
+
const pendingSelectionRef = useRef(null);
|
|
353
|
+
const selectPort = useCallback((ports) => {
|
|
354
|
+
if (selectNativePort)
|
|
355
|
+
return selectNativePort(ports);
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
pendingSelectionRef.current?.reject(new DOMException('Serial port selection was replaced.', 'AbortError'));
|
|
358
|
+
pendingSelectionRef.current = { resolve, reject };
|
|
359
|
+
setNativePortChoices(ports);
|
|
360
|
+
});
|
|
361
|
+
}, [selectNativePort]);
|
|
362
|
+
const serial = useMoldableSerial({
|
|
363
|
+
baudRate,
|
|
364
|
+
filters,
|
|
365
|
+
selectNativePort: selectPort,
|
|
366
|
+
onData,
|
|
367
|
+
});
|
|
368
|
+
const [baud, setBaud] = useState(() => String(baudRate));
|
|
369
|
+
const [draft, setDraft] = useState('');
|
|
370
|
+
const rates = BAUD_RATES.includes(baudRate)
|
|
371
|
+
? BAUD_RATES
|
|
372
|
+
: [...BAUD_RATES, baudRate].sort((left, right) => left - right);
|
|
373
|
+
const connected = serial.status === 'connected';
|
|
374
|
+
const connecting = serial.status === 'connecting';
|
|
375
|
+
const retainedPort = serial.port !== null;
|
|
376
|
+
useEffect(() => () => {
|
|
377
|
+
pendingSelectionRef.current?.reject(new DOMException('Serial port selection was cancelled.', 'AbortError'));
|
|
378
|
+
pendingSelectionRef.current = null;
|
|
379
|
+
}, []);
|
|
380
|
+
const chooseNativePort = (port) => {
|
|
381
|
+
pendingSelectionRef.current?.resolve(port);
|
|
382
|
+
pendingSelectionRef.current = null;
|
|
383
|
+
setNativePortChoices([]);
|
|
384
|
+
};
|
|
385
|
+
const cancelNativePortSelection = () => {
|
|
386
|
+
pendingSelectionRef.current?.reject(new DOMException('Serial port selection was cancelled.', 'AbortError'));
|
|
387
|
+
pendingSelectionRef.current = null;
|
|
388
|
+
setNativePortChoices([]);
|
|
389
|
+
};
|
|
390
|
+
const submitLine = (event) => {
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
if (!connected)
|
|
393
|
+
return;
|
|
394
|
+
void serial.send(draft);
|
|
395
|
+
setDraft('');
|
|
396
|
+
};
|
|
397
|
+
return (_jsxs(HardwarePanel, { "data-slot": "serial-console", "data-status": serial.status, className: className, ...props, children: [_jsxs(HardwarePanelHeader, { children: [_jsx(HardwarePanelIcon, { children: _jsx(Cable, {}) }), _jsx(HardwarePanelTitle, { children: "Serial" }), _jsx(CapabilityBadge, { capability: "serial" }), _jsx(HardwarePanelAction, { children: retainedPort ? (_jsxs(_Fragment, { children: [_jsx("span", { "data-slot": "serial-console-port", className: "animate-in fade-in text-muted-foreground bg-muted/50 rounded-md border px-2 py-0.5 font-mono text-xs tabular-nums duration-200 motion-reduce:animate-none", children: formatSerialPortLabel(serial.portInfo) }), connected ? _jsx(LiveIndicator, {}) : null, _jsxs(Button, { size: "sm", variant: "outline", onClick: () => void serial.disconnect(), children: [_jsx(Unplug, {}), " ", connected ? 'Disconnect' : 'Retry disconnect'] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Select, { value: baud, onValueChange: setBaud, disabled: connecting, children: [_jsx(SelectTrigger, { size: "sm", "aria-label": "Baud rate", className: "w-28 font-mono text-xs tabular-nums", children: _jsx(SelectValue, { placeholder: "Baud" }) }), _jsx(SelectContent, { children: rates.map((rate) => (_jsx(SelectItem, { value: String(rate), children: rate }, rate))) })] }), _jsxs(Button, { size: "sm", disabled: connecting, onClick: () => void serial.connect({ baudRate: Number(baud) }), children: [connecting ? _jsx(Spinner, {}) : _jsx(Plug, {}), connecting ? 'Connecting…' : 'Connect'] })] })) })] }), _jsxs(HardwarePanelContent, { className: "space-y-3", children: [nativePortChoices.length > 0 ? (_jsxs("div", { "data-slot": "serial-console-port-chooser", className: "bg-muted/30 space-y-2 rounded-lg border p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-xs font-medium", children: "Choose a serial port" }), _jsx(Button, { size: "xs", variant: "ghost", onClick: cancelNativePortSelection, children: "Cancel" })] }), _jsx("div", { className: "grid gap-1.5", children: nativePortChoices.map((choice, index) => (_jsxs(Button, { size: "sm", variant: "outline", className: "justify-start font-mono", onClick: () => chooseNativePort(choice), children: [_jsx(Cable, {}), " ", formatSerialPortLabel(choice.getInfo())] }, `${formatSerialPortLabel(choice.getInfo())}-${index}`))) })] })) : null, serial.error ? (_jsx("p", { className: "text-status-error text-xs", children: serial.error.message })) : null, _jsx(StreamLog, { entries: serial.entries, maxHeight: 260, emptyLabel: connected ? 'Waiting for data' : 'Connect a port to start streaming' }), _jsxs("form", { "data-slot": "serial-console-composer", className: "flex items-center gap-2", onSubmit: submitLine, children: [_jsx(Input, { value: draft, onChange: (event) => setDraft(event.target.value), placeholder: "Send a line\u2026", disabled: !connected, spellCheck: false, autoComplete: "off", className: "h-8 flex-1 font-mono text-xs" }), _jsx(Button, { type: "submit", size: "icon-sm", variant: "outline", disabled: !connected, "aria-label": "Send line", children: _jsx(CornerDownLeft, {}) })] })] }), _jsxs(HardwarePanelFooter, { className: "flex-wrap", children: [_jsxs("label", { "data-slot": "serial-console-signal", className: "flex items-center gap-1.5", children: [_jsx(Switch, { size: "sm", checked: serial.signals.dataTerminalReady, disabled: !connected, onCheckedChange: (checked) => void serial.setSignals({ dataTerminalReady: checked }), "aria-label": "Data terminal ready" }), _jsx("span", { className: "text-[10px] font-medium uppercase tracking-wider", children: "DTR" })] }), _jsxs("label", { "data-slot": "serial-console-signal", className: "flex items-center gap-1.5", children: [_jsx(Switch, { size: "sm", checked: serial.signals.requestToSend, disabled: !connected, onCheckedChange: (checked) => void serial.setSignals({ requestToSend: checked }), "aria-label": "Request to send" }), _jsx("span", { className: "text-[10px] font-medium uppercase tracking-wider", children: "RTS" })] }), _jsxs("label", { "data-slot": "serial-console-signal", className: "flex items-center gap-1.5", children: [_jsx(Switch, { size: "sm", checked: serial.signals.break, disabled: !connected, onCheckedChange: (checked) => void serial.setSignals({ break: checked }), "aria-label": "Break signal" }), _jsx("span", { className: "text-[10px] font-medium uppercase tracking-wider", children: "BRK" })] }), _jsx(Button, { size: "icon-xs", variant: "ghost", disabled: !connected, onClick: () => void serial.refreshSignals(), "aria-label": "Refresh input signals", title: "Refresh input signals", children: _jsx(RefreshCw, {}) }), serial.inputSignals ? (_jsxs("span", { "data-slot": "serial-console-input-signals", className: "font-mono text-[10px] tabular-nums", children: ["CTS ", serial.inputSignals.clearToSend ? '1' : '0', " \u00B7 DCD", ' ', serial.inputSignals.dataCarrierDetect ? '1' : '0', " \u00B7 DSR", ' ', serial.inputSignals.dataSetReady ? '1' : '0', " \u00B7 RI", ' ', serial.inputSignals.ringIndicator ? '1' : '0'] })) : null, retainedPort ? (_jsxs(Button, { size: "xs", variant: "ghost", onClick: () => void serial.forget(), children: [_jsx(ShieldX, {}), " Forget"] })) : null, _jsxs("span", { "data-slot": "serial-console-counters", className: "ml-auto shrink-0 font-mono tabular-nums", children: ["RX ", serial.rxBytes, " \u00B7 TX ", serial.txBytes] })] })] }));
|
|
398
|
+
}
|
|
399
|
+
export { SerialConsole };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type MoldableGlobalShortcutEvent, type MoldableGlobalShortcutHandler } from '../../lib/native-hardware';
|
|
3
|
+
/**
|
|
4
|
+
* Splits an Electron-style accelerator ("CommandOrControl+Shift+K") into
|
|
5
|
+
* display keys for `Kbd` chips — ['⌘', '⇧', 'K'] on macOS, ['Ctrl', 'Shift',
|
|
6
|
+
* 'K'] elsewhere.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatShortcutKeys(accelerator: string): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Registers a global shortcut while `shortcut` is set, unregistering on
|
|
11
|
+
* change and unmount. The handler is kept in a ref so it can change without
|
|
12
|
+
* re-registering.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useMoldableGlobalShortcut(shortcut: string | null, handler?: MoldableGlobalShortcutHandler): {
|
|
15
|
+
registered: boolean;
|
|
16
|
+
error: Error | null;
|
|
17
|
+
lastEvent: MoldableGlobalShortcutEvent | null;
|
|
18
|
+
};
|
|
19
|
+
export interface ShortcutRecorderProps extends Omit<React.ComponentProps<'div'>, 'onChange' | 'defaultValue'> {
|
|
20
|
+
/** Controlled accelerator value; `null` means no shortcut. */
|
|
21
|
+
value?: string | null;
|
|
22
|
+
defaultValue?: string | null;
|
|
23
|
+
onChange?: (shortcut: string | null) => void;
|
|
24
|
+
/** Fires when the registered global shortcut is pressed. */
|
|
25
|
+
onTrigger?: (event: MoldableGlobalShortcutEvent) => void;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Field-level global shortcut recorder: click to arm, press a modifier combo
|
|
30
|
+
* to capture it, and the accelerator registers system-wide. Registered chips
|
|
31
|
+
* pulse when the shortcut fires anywhere in the OS.
|
|
32
|
+
*/
|
|
33
|
+
declare function ShortcutRecorder({ value, defaultValue, onChange, onTrigger, disabled, className, ...props }: ShortcutRecorderProps): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
export { ShortcutRecorder };
|
|
35
|
+
//# sourceMappingURL=shortcut-recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortcut-recorder.d.ts","sourceRoot":"","sources":["../../../src/components/hardware/shortcut-recorder.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EACL,KAAK,2BAA2B,EAChC,KAAK,6BAA6B,EAInC,MAAM,2BAA2B,CAAA;AAUlC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BhE;AA4CD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,OAAO,CAAC,EAAE,6BAA6B;;;;EA8CxC;AAED,MAAM,WAAW,qBACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,cAAc,CAAC;IACtE,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5C,4DAA4D;IAC5D,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,2BAA2B,KAAK,IAAI,CAAA;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,iBAAS,gBAAgB,CAAC,EACxB,KAAK,EACL,YAAmB,EACnB,QAAQ,EACR,SAAS,EACT,QAAgB,EAChB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,qBAAqB,2CA+IvB;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAA"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { isMoldableGlobalShortcut, registerMoldableGlobalShortcut, } from '../../lib/native-hardware';
|
|
6
|
+
import { cn } from '../../lib/utils';
|
|
7
|
+
import { Button } from '../ui/button';
|
|
8
|
+
import { Kbd, KbdGroup } from '../ui/kbd';
|
|
9
|
+
function isApplePlatform() {
|
|
10
|
+
if (typeof navigator === 'undefined')
|
|
11
|
+
return false;
|
|
12
|
+
return /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Splits an Electron-style accelerator ("CommandOrControl+Shift+K") into
|
|
16
|
+
* display keys for `Kbd` chips — ['⌘', '⇧', 'K'] on macOS, ['Ctrl', 'Shift',
|
|
17
|
+
* 'K'] elsewhere.
|
|
18
|
+
*/
|
|
19
|
+
export function formatShortcutKeys(accelerator) {
|
|
20
|
+
const apple = isApplePlatform();
|
|
21
|
+
return accelerator
|
|
22
|
+
.split('+')
|
|
23
|
+
.filter((part) => part.length > 0)
|
|
24
|
+
.map((part) => {
|
|
25
|
+
switch (part.toLowerCase()) {
|
|
26
|
+
case 'commandorcontrol':
|
|
27
|
+
case 'cmdorctrl':
|
|
28
|
+
return apple ? '⌘' : 'Ctrl';
|
|
29
|
+
case 'command':
|
|
30
|
+
case 'cmd':
|
|
31
|
+
case 'meta':
|
|
32
|
+
case 'super':
|
|
33
|
+
return apple ? '⌘' : 'Win';
|
|
34
|
+
case 'control':
|
|
35
|
+
case 'ctrl':
|
|
36
|
+
return apple ? '⌃' : 'Ctrl';
|
|
37
|
+
case 'alt':
|
|
38
|
+
case 'option':
|
|
39
|
+
return apple ? '⌥' : 'Alt';
|
|
40
|
+
case 'shift':
|
|
41
|
+
return apple ? '⇧' : 'Shift';
|
|
42
|
+
default:
|
|
43
|
+
return part.length === 1 ? part.toUpperCase() : part;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const MODIFIER_KEYS = new Set([
|
|
48
|
+
'Alt',
|
|
49
|
+
'AltGraph',
|
|
50
|
+
'CapsLock',
|
|
51
|
+
'Control',
|
|
52
|
+
'Dead',
|
|
53
|
+
'Fn',
|
|
54
|
+
'FnLock',
|
|
55
|
+
'Hyper',
|
|
56
|
+
'Meta',
|
|
57
|
+
'NumLock',
|
|
58
|
+
'OS',
|
|
59
|
+
'ScrollLock',
|
|
60
|
+
'Shift',
|
|
61
|
+
'Super',
|
|
62
|
+
'Symbol',
|
|
63
|
+
'SymbolLock',
|
|
64
|
+
]);
|
|
65
|
+
function acceleratorKeyFromEvent(event) {
|
|
66
|
+
if (MODIFIER_KEYS.has(event.key))
|
|
67
|
+
return null;
|
|
68
|
+
const code = event.code;
|
|
69
|
+
if (/^Key[A-Z]$/.test(code))
|
|
70
|
+
return code.slice(3);
|
|
71
|
+
if (/^Digit[0-9]$/.test(code))
|
|
72
|
+
return code.slice(5);
|
|
73
|
+
if (event.key === ' ')
|
|
74
|
+
return 'Space';
|
|
75
|
+
if (event.key.startsWith('Arrow'))
|
|
76
|
+
return event.key.slice(5);
|
|
77
|
+
if (/^[A-Za-z0-9]$/.test(event.key))
|
|
78
|
+
return event.key.toUpperCase();
|
|
79
|
+
if (/^[A-Za-z0-9]{2,32}$/.test(event.key))
|
|
80
|
+
return event.key;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function acceleratorModifiersFromEvent(event) {
|
|
84
|
+
const apple = isApplePlatform();
|
|
85
|
+
const modifiers = [];
|
|
86
|
+
if (apple ? event.metaKey : event.ctrlKey)
|
|
87
|
+
modifiers.push('CommandOrControl');
|
|
88
|
+
if (apple && event.ctrlKey)
|
|
89
|
+
modifiers.push('Control');
|
|
90
|
+
if (!apple && event.metaKey)
|
|
91
|
+
modifiers.push('Super');
|
|
92
|
+
if (event.altKey)
|
|
93
|
+
modifiers.push('Alt');
|
|
94
|
+
if (event.shiftKey)
|
|
95
|
+
modifiers.push('Shift');
|
|
96
|
+
return modifiers;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Registers a global shortcut while `shortcut` is set, unregistering on
|
|
100
|
+
* change and unmount. The handler is kept in a ref so it can change without
|
|
101
|
+
* re-registering.
|
|
102
|
+
*/
|
|
103
|
+
export function useMoldableGlobalShortcut(shortcut, handler) {
|
|
104
|
+
const [registered, setRegistered] = useState(false);
|
|
105
|
+
const [error, setError] = useState(null);
|
|
106
|
+
const [lastEvent, setLastEvent] = useState(null);
|
|
107
|
+
const handlerRef = useRef(handler);
|
|
108
|
+
handlerRef.current = handler;
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
setRegistered(false);
|
|
111
|
+
setError(null);
|
|
112
|
+
setLastEvent(null);
|
|
113
|
+
if (!shortcut)
|
|
114
|
+
return;
|
|
115
|
+
let live = true;
|
|
116
|
+
let registration = null;
|
|
117
|
+
registerMoldableGlobalShortcut(shortcut, (event) => {
|
|
118
|
+
if (!live)
|
|
119
|
+
return;
|
|
120
|
+
setLastEvent(event);
|
|
121
|
+
handlerRef.current?.(event);
|
|
122
|
+
})
|
|
123
|
+
.then((result) => {
|
|
124
|
+
if (!live) {
|
|
125
|
+
void result.unregister().catch(() => {
|
|
126
|
+
// Host teardown remains the final cleanup boundary.
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
registration = result;
|
|
131
|
+
setRegistered(true);
|
|
132
|
+
})
|
|
133
|
+
.catch((cause) => {
|
|
134
|
+
if (!live)
|
|
135
|
+
return;
|
|
136
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
137
|
+
});
|
|
138
|
+
return () => {
|
|
139
|
+
live = false;
|
|
140
|
+
if (registration) {
|
|
141
|
+
void registration.unregister().catch(() => {
|
|
142
|
+
// Host teardown remains the final cleanup boundary.
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}, [shortcut]);
|
|
147
|
+
return { registered, error, lastEvent };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Field-level global shortcut recorder: click to arm, press a modifier combo
|
|
151
|
+
* to capture it, and the accelerator registers system-wide. Registered chips
|
|
152
|
+
* pulse when the shortcut fires anywhere in the OS.
|
|
153
|
+
*/
|
|
154
|
+
function ShortcutRecorder({ value, defaultValue = null, onChange, onTrigger, disabled = false, className, ...props }) {
|
|
155
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
156
|
+
const shortcut = value !== undefined ? value : internalValue;
|
|
157
|
+
const [recording, setRecording] = useState(false);
|
|
158
|
+
const [pulse, setPulse] = useState(false);
|
|
159
|
+
const pulseTimeoutRef = useRef(null);
|
|
160
|
+
const valueRef = useRef(value);
|
|
161
|
+
valueRef.current = value;
|
|
162
|
+
const onChangeRef = useRef(onChange);
|
|
163
|
+
onChangeRef.current = onChange;
|
|
164
|
+
const onTriggerRef = useRef(onTrigger);
|
|
165
|
+
onTriggerRef.current = onTrigger;
|
|
166
|
+
const commit = useCallback((next) => {
|
|
167
|
+
if (valueRef.current === undefined)
|
|
168
|
+
setInternalValue(next);
|
|
169
|
+
onChangeRef.current?.(next);
|
|
170
|
+
}, []);
|
|
171
|
+
const handleShortcutEvent = useCallback((event) => {
|
|
172
|
+
if (event.state !== 'Pressed')
|
|
173
|
+
return;
|
|
174
|
+
setPulse(true);
|
|
175
|
+
if (pulseTimeoutRef.current !== null) {
|
|
176
|
+
window.clearTimeout(pulseTimeoutRef.current);
|
|
177
|
+
}
|
|
178
|
+
pulseTimeoutRef.current = window.setTimeout(() => setPulse(false), 200);
|
|
179
|
+
onTriggerRef.current?.(event);
|
|
180
|
+
}, []);
|
|
181
|
+
const { registered, error } = useMoldableGlobalShortcut(shortcut, handleShortcutEvent);
|
|
182
|
+
useEffect(() => () => {
|
|
183
|
+
if (pulseTimeoutRef.current !== null) {
|
|
184
|
+
window.clearTimeout(pulseTimeoutRef.current);
|
|
185
|
+
}
|
|
186
|
+
}, []);
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!recording)
|
|
189
|
+
return;
|
|
190
|
+
const handleKeyDown = (event) => {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
event.stopPropagation();
|
|
193
|
+
if (event.key === 'Escape') {
|
|
194
|
+
setRecording(false);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const key = acceleratorKeyFromEvent(event);
|
|
198
|
+
const modifiers = acceleratorModifiersFromEvent(event);
|
|
199
|
+
if (!key || modifiers.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
const accelerator = [...modifiers, key].join('+');
|
|
202
|
+
if (!isMoldableGlobalShortcut(accelerator))
|
|
203
|
+
return;
|
|
204
|
+
setRecording(false);
|
|
205
|
+
commit(accelerator);
|
|
206
|
+
};
|
|
207
|
+
window.addEventListener('keydown', handleKeyDown, true);
|
|
208
|
+
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
|
209
|
+
}, [recording, commit]);
|
|
210
|
+
const keys = shortcut ? formatShortcutKeys(shortcut) : [];
|
|
211
|
+
return (_jsxs("div", { "data-slot": "shortcut-recorder", "data-recording": recording || undefined, "data-registered": registered || undefined, className: cn('w-full min-w-0', className), ...props, children: [_jsxs("div", { className: "relative flex items-center", children: [_jsx("button", { type: "button", disabled: disabled, "aria-label": recording
|
|
212
|
+
? 'Recording shortcut, press keys'
|
|
213
|
+
: shortcut
|
|
214
|
+
? `Shortcut ${shortcut}, click to re-record`
|
|
215
|
+
: 'Record shortcut', onClick: () => setRecording(true), onBlur: () => setRecording(false), className: cn('border-input dark:bg-input/30 shadow-xs flex h-9 w-full min-w-0 items-center gap-2 rounded-md border bg-transparent px-3 text-left text-sm outline-none transition-[color,box-shadow]', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', recording && 'border-ring ring-ring/50 ring-[3px]', disabled && 'pointer-events-none cursor-not-allowed opacity-50', shortcut && !recording && 'pr-9'), children: recording ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "relative flex size-1.5 shrink-0", children: [_jsx("span", { className: "bg-destructive absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 motion-reduce:animate-none" }), _jsx("span", { className: "bg-destructive relative inline-flex size-1.5 rounded-full" })] }), _jsx("span", { className: "text-muted-foreground", children: "Press keys\u2026" })] })) : keys.length > 0 ? (_jsx(KbdGroup, { "data-state": pulse ? 'triggered' : 'idle', className: "origin-left transition-transform duration-150 data-[state=triggered]:scale-110 motion-reduce:transition-none", children: keys.map((key, index) => (_jsx(Kbd, { children: key }, `${index}-${key}`))) })) : (_jsx("span", { className: "text-muted-foreground", children: "Click to record" })) }), shortcut && !recording ? (_jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": "Clear shortcut", disabled: disabled, className: "absolute right-1.5", onClick: () => commit(null), children: _jsx(X, {}) })) : null] }), error ? (_jsx("p", { "data-slot": "shortcut-recorder-error", className: "text-status-error mt-1.5 text-xs", children: error.message })) : null] }));
|
|
216
|
+
}
|
|
217
|
+
export { ShortcutRecorder };
|