/** * Serial Port Connect Modal * Allows users to configure and connect to a serial port */ import { ChevronDown, ChevronUp, Cpu, RefreshCw, Save, Usb } from 'lucide-react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useI18n } from '../application/i18n/I18nProvider'; import { useTerminalBackend } from '../application/state/useTerminalBackend'; import type { Host, SerialConfig, SerialFlowControl, SerialParity } from '../domain/models'; import { prepareSerialConfigForSavedHost } from '../domain/serialBackspace'; import { cn } from '../lib/utils'; import { Button } from './ui/button'; import { Combobox, type ComboboxOption } from './ui/combobox'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from './ui/dialog'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'; interface SerialPort { path: string; manufacturer: string; serialNumber: string; vendorId: string; productId: string; pnpId: string; type?: 'hardware' | 'pseudo' | 'custom'; } interface SerialConnectModalProps { open: boolean; onClose: () => void; onConnect: (config: SerialConfig, options?: { charset?: string }) => void; onSaveHost?: (host: Host) => void; } const BAUD_RATES = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]; const DATA_BITS: Array<5 | 6 | 7 | 8> = [5, 6, 7, 8]; const STOP_BITS: Array<1 | 1.5 | 2> = [1, 1.5, 2]; const PARITY_OPTIONS: SerialParity[] = ['none', 'even', 'odd', 'mark', 'space']; const FLOW_CONTROL_OPTIONS: SerialFlowControl[] = ['none', 'xon/xoff', 'rts/cts']; export const SerialConnectModal: React.FC = ({ open, onClose, onConnect, onSaveHost, }) => { const { t } = useI18n(); const [ports, setPorts] = useState([]); const [isLoadingPorts, setIsLoadingPorts] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); // Form state const [selectedPort, setSelectedPort] = useState(''); const [baudRate, setBaudRate] = useState(115200); const [dataBits, setDataBits] = useState<5 | 6 | 7 | 8>(8); const [stopBits, setStopBits] = useState<1 | 1.5 | 2>(1); const [parity, setParity] = useState('none'); const [flowControl, setFlowControl] = useState('none'); const [localEcho, setLocalEcho] = useState(false); const [lineMode, setLineMode] = useState(false); const [backspaceBehavior, setBackspaceBehavior] = useState('default'); const [charset, setCharset] = useState('UTF-8'); // Save configuration state const [saveConfig, setSaveConfig] = useState(false); const [configLabel, setConfigLabel] = useState(''); const terminalBackend = useTerminalBackend(); const loadPorts = useCallback(async () => { setIsLoadingPorts(true); try { const result = await terminalBackend.listSerialPorts(); setPorts(result); // Auto-select first port if available and no port is selected if (result.length > 0) { setSelectedPort((prev) => prev || result[0].path); } } catch (err) { console.error('[Serial] Failed to list ports:', err); } finally { setIsLoadingPorts(false); } }, [terminalBackend]); useEffect(() => { if (open) { loadPorts(); } }, [open]); // eslint-disable-line react-hooks/exhaustive-deps // Generate a default label when port is selected useEffect(() => { if (selectedPort && !configLabel) { const portName = selectedPort.split('/').pop() || selectedPort; setConfigLabel(`Serial: ${portName}`); } }, [selectedPort, configLabel]); const handleConnect = () => { if (!selectedPort) return; const config: SerialConfig = { path: selectedPort, baudRate, dataBits, stopBits, parity, flowControl, localEcho, lineMode, backspaceBehavior, }; // Save as host if checkbox is checked and onSaveHost is provided if (saveConfig && onSaveHost) { const portName = selectedPort.split('/').pop() || selectedPort; const host: Host = { id: `serial-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, label: configLabel.trim() || `Serial: ${portName}`, hostname: selectedPort, // For serial hosts, port field stores baud rate as a numeric identifier. // The full configuration is stored in serialConfig for actual connection. port: baudRate, username: '', os: 'linux', tags: ['serial'], protocol: 'serial', createdAt: Date.now(), charset, serialConfig: prepareSerialConfigForSavedHost(config), }; onSaveHost(host); } onConnect(config, { charset }); onClose(); }; // Convert ports to Combobox options const portOptions: ComboboxOption[] = useMemo(() => { return ports.map((port) => ({ value: port.path, label: port.path, sublabel: port.manufacturer || undefined, })); }, [ports]); // Validate: port path must start with /dev/ (Unix/macOS) or COM/\\.\COM (Windows) const trimmedPort = selectedPort.trim(); const isPortValid = trimmedPort.startsWith('/dev/') || /^COM\d+$/i.test(trimmedPort) || /^\\\\\.\\COM\d+$/i.test(trimmedPort); // Allow custom baud rates as long as they are positive integers const isBaudRateValid = Number.isInteger(baudRate) && baudRate > 0; // Check if using 1.5 stop bits (limited Windows support) const isStopBits15 = stopBits === 1.5; const isValid = isPortValid && isBaudRateValid; return ( !isOpen && onClose()}> {t('serial.modal.title')} {t('serial.modal.desc')}
{/* Serial Port Selection */}
{/* Combobox for port selection with manual input support */} } /> {!isPortValid && selectedPort && (

{t('serial.field.customPortPlaceholder')}

)}
{/* Baud Rate */}
({ value: String(rate), label: String(rate), }))} value={String(baudRate)} onValueChange={(val) => { const parsed = parseInt(val, 10); if (!isNaN(parsed) && parsed > 0) { setBaudRate(parsed); } }} placeholder={t('serial.field.baudRatePlaceholder')} emptyText={t('serial.field.baudRateEmpty')} allowCreate createText={t('common.use')} /> {baudRate > 0 && !BAUD_RATES.includes(baudRate) && (

{t('serial.field.customBaudRate')}

)}
{/* Advanced Options */} {/* Data Bits */}
{/* Stop Bits */}
{isStopBits15 && (

{t('serial.field.stopBits15Warning')}

)}
{/* Parity */}
{/* Flow Control */}
{/* Terminal Options */}

{t('serial.field.backspaceBehaviorDesc')}

{t('serial.field.localEchoDesc')}

setLocalEcho(e.target.checked)} className="h-4 w-4 rounded border-input" />

{t('serial.field.lineModeDesc')}

setLineMode(e.target.checked)} className="h-4 w-4 rounded border-input" />
{/* Charset */}
setCharset(e.target.value)} className="h-9" />
{/* Save Configuration */} {onSaveHost && (

{t('serial.field.saveConfigDesc')}

setSaveConfig(e.target.checked)} className="h-4 w-4 rounded border-input" />
{saveConfig && (
setConfigLabel(e.target.value)} placeholder={t('serial.field.configLabelPlaceholder')} />
)}
)}
); }; export default SerialConnectModal;