Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions src/components/ConsoleInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,29 @@ export default function ConsoleInput({ onSend, disabled = false }: ConsoleInputP
setHistoryIndex(-1)
}, [input, disabled, onSend, history])

const sendControlChar = useCallback((char: 'C' | 'D') => {
if (disabled) return

// Send control character (Ctrl-C = 0x03, Ctrl-D = 0x04)
const code = char === 'C' ? 3 : 4
const controlChar = String.fromCharCode(code)
onSend(controlChar)
}, [disabled, onSend])

const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
// Handle Ctrl-C and Ctrl-D
if (e.ctrlKey || e.metaKey) {
if (e.key === 'c' || e.key === 'C') {
e.preventDefault()
sendControlChar('C')
return
} else if (e.key === 'd' || e.key === 'D') {
e.preventDefault()
sendControlChar('D')
return
}
}

if (e.key === 'Enter') {
e.preventDefault()
handleSend()
Expand All @@ -51,7 +73,7 @@ export default function ConsoleInput({ onSend, disabled = false }: ConsoleInputP
}
}
}
}, [handleSend, history, historyIndex])
}, [handleSend, history, historyIndex, sendControlChar])

return (
<div className="flex gap-2 p-3 border-t border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-950">
Expand All @@ -61,10 +83,26 @@ export default function ConsoleInput({ onSend, disabled = false }: ConsoleInputP
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={disabled ? "Not connected" : "Type command and press Enter..."}
placeholder={disabled ? "Not connected" : "Type command and press Enter (Ctrl-C, Ctrl-D supported)..."}
disabled={disabled}
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-900 text-gray-900 dark:text-neutral-100 placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 disabled:opacity-50 disabled:cursor-not-allowed"
/>
<button
onClick={() => sendControlChar('C')}
disabled={disabled}
title="Send Ctrl-C (interrupt)"
className="px-3 py-2 text-sm font-medium text-white bg-orange-600 hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-600 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-orange-600 dark:disabled:hover:bg-orange-500"
>
^C
</button>
<button
onClick={() => sendControlChar('D')}
disabled={disabled}
title="Send Ctrl-D (EOF)"
className="px-3 py-2 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 dark:bg-purple-500 dark:hover:bg-purple-600 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-purple-600 dark:disabled:hover:bg-purple-500"
>
^D
</button>
<button
onClick={handleSend}
disabled={!input.trim() || disabled}
Expand Down
17 changes: 13 additions & 4 deletions src/components/SerialConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,20 @@ export default function SerialConsole({ isConnected, onSendMessage }: SerialCons

const handleSendMessage = useCallback(async (message: string) => {
try {
// Add newline to message if not present
const messageToSend = message.endsWith('\n') ? message : message + '\n'
// Check if message is a control character (ASCII 0-31)
const isControlChar = message.length === 1 && message.charCodeAt(0) < 32

// Log outgoing message
addOutgoing(message)
// Add newline to message if not present (unless it's a control character)
const messageToSend = isControlChar ? message : (message.endsWith('\n') ? message : message + '\n')

// Log outgoing message with readable name for control chars
if (isControlChar) {
const code = message.charCodeAt(0)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The control character name generation logic uses a magic number (64) without explanation. Consider adding a comment explaining that this converts ASCII control codes to their corresponding caret notation (e.g., code 3 becomes '^C' via String.fromCharCode(64 + 3) = '@' + 3 = 'C').

Suggested change
const code = message.charCodeAt(0)
const code = message.charCodeAt(0)
// The magic number 64 converts ASCII control codes to caret notation (e.g., code 3 becomes '^C' via String.fromCharCode(64 + 3) = 'C')

Copilot uses AI. Check for mistakes.
const ctrlName = code === 3 ? '^C (ETX)' : code === 4 ? '^D (EOT)' : `^${String.fromCharCode(64 + code)}`
addOutgoing(ctrlName)
} else {
addOutgoing(message)
}

// Send via serial
await onSendMessage(messageToSend)
Expand Down