import { useEffect, useState } from 'react'; import { Radio, Square, Send, Plug, Power, RefreshCw, X, ChevronUp, ChevronDown } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; export interface WKMacro { label: string; text: string } export interface WKStatus { connected: boolean; busy: boolean; wpm: number; version: number; port: string; error?: string; } interface Props { status: WKStatus; ports: string[]; port: string; wpm: number; macros: WKMacro[]; sent: string; // text echoed back by the keyer as it transmits onSelectPort: (p: string) => void; onRefreshPorts: () => void; onConnect: () => void; onDisconnect: () => void; onSetSpeed: (wpm: number) => void; onSend: (text: string) => void; // raw text (App resolves variables) onSendMacro: (index: number) => void; // App resolves the macro + sends onStop: () => void; onClose: () => void; // disable the keyer (hide the panel) sendOnType: boolean; onToggleSendOnType: (on: boolean) => void; onSendRaw: (chars: string) => void; // key typed chars as-is (no variables) onBackspace: () => void; // remove last not-yet-keyed char } // WinkeyerPanel — Log4OM-style CW keyer operating window. Lives in the // reserved space to the right of the F1-F5 tabs. Sends Morse via the WinKeyer // hardware: free-text CW, one-click macros (F1…), live speed, and abort. export function WinkeyerPanel({ status, ports, port, wpm, macros, sent, onSelectPort, onRefreshPorts, onConnect, onDisconnect, onSetSpeed, onSend, onSendMacro, onStop, onClose, sendOnType, onToggleSendOnType, onSendRaw, onBackspace, }: Props) { const [cwText, setCwText] = useState(''); const [speed, setSpeed] = useState(wpm); // Step the speed (compact +/- control replaces the old slider). const changeSpeed = (delta: number) => { const w = Math.max(5, Math.min(50, speed + delta)); setSpeed(w); onSetSpeed(w); }; // Keep the local speed in sync when the device/config changes it. useEffect(() => { setSpeed(status.connected ? status.wpm || wpm : wpm); }, [status.wpm, status.connected, wpm]); const connected = status.connected; function sendText() { const t = cwText.trim(); if (t && !sendOnType) onSend(t); // in send-on-type the text already went out setCwText(''); } // In "send on type" mode, key each newly-typed char immediately, and send a // WinKeyer backspace for each deleted char (removes it from the buffer if it // hasn't been keyed yet). Only end-of-string edits are mirrored live. function onCwChange(v: string) { if (sendOnType && connected) { const old = cwText; if (v.length > old.length && v.startsWith(old)) { onSendRaw(v.slice(old.length)); } else if (v.length < old.length && old.startsWith(v)) { for (let i = 0; i < old.length - v.length; i++) onBackspace(); } } setCwText(v); } return (
{/* Header / connection bar */}
WinKeyer
{!connected ? ( <> ) : ( )}
{/* Live transmitted text (echoed by the keyer) + compact speed stepper. */}
{sent || } {status.busy && }
{/* Speed: number + up/down arrows (replaces the slider, saves height). */}
{speed} wpm
{/* CW text */}
onCwChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); sendText(); } }} placeholder={sendOnType ? 'Type — sent live…' : 'Type and press Enter to send…'} disabled={!connected} className="font-mono uppercase" />
{/* Macro buttons F1… — single-line (F-key + label) to keep the panel short. */}
{macros.map((m, i) => ( ))}
{status.error &&
{status.error}
}
); }