• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atomic14 / web-serial-plotter / 17381877161

01 Sep 2025 03:29PM UTC coverage: 63.077% (+0.1%) from 62.932%
17381877161

push

github

cgreening
Fix build

409 of 519 branches covered (78.81%)

Branch coverage included in aggregate %.

1969 of 3251 relevant lines covered (60.57%)

36.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/SerialConsole.tsx
1
import { useCallback, useState } from 'react'
×
2
import { useConsoleStore } from '../hooks/useConsoleStore'
×
3
import ConsoleLog from './ConsoleLog'
×
4
import ConsoleInput from './ConsoleInput'
×
5
import { exportMessages, type ExportFormat } from '../utils/consoleExport'
×
6

7
interface SerialConsoleProps {
8
  isConnected: boolean
9
  onSendMessage: (message: string) => Promise<void>
10
}
11

12
export default function SerialConsole({ isConnected, onSendMessage }: SerialConsoleProps) {
×
13
  const { messages, addOutgoing, clear, getCapacity, setCapacity } = useConsoleStore()
×
14
  const [showSettings, setShowSettings] = useState(false)
×
15
  const [capacityInput, setCapacityInput] = useState(getCapacity().toString())
×
16
  const [exportFormat, setExportFormat] = useState<ExportFormat>('txt')
×
17

18
  const handleSendMessage = useCallback(async (message: string) => {
×
19
    try {
×
20
      // Add newline to message if not present
21
      const messageToSend = message.endsWith('\n') ? message : message + '\n'
×
22
      
23
      // Log outgoing message
24
      addOutgoing(message)
×
25
      
26
      // Send via serial
27
      await onSendMessage(messageToSend)
×
28
    } catch (error) {
×
29
      // Log error
30
      addOutgoing(`Error: ${error instanceof Error ? error.message : 'Failed to send'}`, 'error')
×
31
    }
×
32
  }, [onSendMessage, addOutgoing])
×
33

34
  const handleCapacityChange = useCallback(() => {
×
35
    const newCapacity = parseInt(capacityInput, 10)
×
36
    if (Number.isInteger(newCapacity) && newCapacity >= 10 && newCapacity <= 10000) {
×
37
      setCapacity(newCapacity)
×
38
      setShowSettings(false)
×
39
    }
×
40
  }, [capacityInput, setCapacity])
×
41

42
  const handleCapacityKeyDown = useCallback((e: React.KeyboardEvent) => {
×
43
    if (e.key === 'Enter') {
×
44
      handleCapacityChange()
×
45
    } else if (e.key === 'Escape') {
×
46
      setCapacityInput(getCapacity().toString())
×
47
      setShowSettings(false)
×
48
    }
×
49
  }, [handleCapacityChange, getCapacity])
×
50

51
  const handleDownload = useCallback(() => {
×
52
    if (messages.length === 0) return
×
53
    exportMessages(messages, exportFormat)
×
54
  }, [messages, exportFormat])
×
55

56
  return (
×
57
    <div className="flex flex-col h-full">
×
58
      {/* Header with connection status and controls */}
59
      <div className="border-b border-gray-200 dark:border-neutral-800 bg-gray-50 dark:bg-neutral-900">
×
60
        <div className="flex items-center justify-between p-3">
×
61
          <div className="flex items-center gap-2">
×
62
            <div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-gray-400'}`} />
×
63
            <span className="text-sm font-medium text-gray-700 dark:text-neutral-300">
×
64
              {isConnected ? 'Connected' : 'Disconnected'}
×
65
            </span>
×
66
            <span className="text-xs text-gray-500 dark:text-neutral-400">
×
67
              ({messages.length}/{getCapacity()} messages)
×
68
            </span>
×
69
          </div>
×
70
          <div className="flex items-center gap-2">
×
71
            <button
×
72
              onClick={handleDownload}
×
73
              disabled={messages.length === 0}
×
74
              className="px-3 py-1 text-xs font-medium text-gray-600 dark:text-neutral-400 hover:text-gray-800 dark:hover:text-neutral-200 border border-gray-300 dark:border-neutral-700 rounded hover:border-gray-400 dark:hover:border-neutral-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
×
75
              title={`Download as ${exportFormat.toUpperCase()}`}
×
76
            >
×
77
              ⬇️ Download
78
            </button>
×
79
            <button
×
80
              onClick={() => setShowSettings(!showSettings)}
×
81
              className="px-2 py-1 text-xs font-medium text-gray-600 dark:text-neutral-400 hover:text-gray-800 dark:hover:text-neutral-200 border border-gray-300 dark:border-neutral-700 rounded hover:border-gray-400 dark:hover:border-neutral-600 transition-colors"
×
82
              title="Settings"
×
83
            >
×
84
              ⚙️
85
            </button>
×
86
            <button
×
87
              onClick={clear}
×
88
              disabled={messages.length === 0}
×
89
              className="px-3 py-1 text-xs font-medium text-gray-600 dark:text-neutral-400 hover:text-gray-800 dark:hover:text-neutral-200 border border-gray-300 dark:border-neutral-700 rounded hover:border-gray-400 dark:hover:border-neutral-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
×
90
            >
×
91
              Clear
92
            </button>
×
93
          </div>
×
94
        </div>
×
95
        
96
        {/* Settings panel */}
97
        {showSettings && (
×
98
          <div className="px-3 pb-3 border-t border-gray-200 dark:border-neutral-800 space-y-3">
×
99
            {/* Message history setting */}
100
            <div className="flex items-center gap-2 pt-2">
×
101
              <label className="text-xs font-medium text-gray-700 dark:text-neutral-300">
×
102
                Max Messages:
103
              </label>
×
104
              <input
×
105
                type="number"
×
106
                value={capacityInput}
×
107
                onChange={(e) => setCapacityInput(e.target.value)}
×
108
                onKeyDown={handleCapacityKeyDown}
×
109
                min="10"
×
110
                max="10000"
×
111
                className="w-20 px-2 py-1 text-xs border border-gray-300 dark:border-neutral-700 rounded bg-white dark:bg-neutral-900 text-gray-900 dark:text-neutral-100"
×
112
              />
×
113
              <button
×
114
                onClick={handleCapacityChange}
×
115
                className="px-2 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded transition-colors"
×
116
              >
×
117
                Apply
118
              </button>
×
119
              <button
×
120
                onClick={() => {
×
121
                  setCapacityInput(getCapacity().toString())
×
122
                  setShowSettings(false)
×
123
                }}
×
124
                className="px-2 py-1 text-xs font-medium text-gray-600 dark:text-neutral-400 hover:text-gray-800 dark:hover:text-neutral-200 border border-gray-300 dark:border-neutral-700 rounded hover:border-gray-400 dark:hover:border-neutral-600 transition-colors"
×
125
              >
×
126
                Cancel
127
              </button>
×
128
              <span className="text-xs text-gray-500 dark:text-neutral-400">
×
129
                (10-10000)
130
              </span>
×
131
            </div>
×
132
            
133
            {/* Export format setting */}
134
            <div className="flex items-center gap-2">
×
135
              <label className="text-xs font-medium text-gray-700 dark:text-neutral-300">
×
136
                Export Format:
137
              </label>
×
138
              <select
×
139
                value={exportFormat}
×
140
                onChange={(e) => setExportFormat(e.target.value as ExportFormat)}
×
141
                className="px-2 py-1 text-xs border border-gray-300 dark:border-neutral-700 rounded bg-white dark:bg-neutral-900 text-gray-900 dark:text-neutral-100"
×
142
              >
143
                <option value="txt">Text (.txt)</option>
×
144
                <option value="csv">CSV (.csv)</option>
×
145
                <option value="json">JSON (.json)</option>
×
146
              </select>
×
147
              <span className="text-xs text-gray-500 dark:text-neutral-400">
×
148
                Choose download format
149
              </span>
×
150
            </div>
×
151
          </div>
×
152
        )}
153
      </div>
×
154

155
      {/* Console log */}
156
      <div className="flex-1 min-h-0 flex flex-col p-3">
×
157
        <ConsoleLog messages={messages} />
×
158
      </div>
×
159

160
      {/* Input area */}
161
      <ConsoleInput 
×
162
        onSend={handleSendMessage}
×
163
        disabled={!isConnected}
×
164
      />
×
165
    </div>
×
166
  )
167
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc