mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
15b16ab7b2
* fix: serial backspace leaves hidden chars for multi-byte CJK input
Serial terminals process backspace at the byte level: one 0x7F removes
one byte from the device input buffer. For multi-byte CJK characters
(UTF-8: 3 bytes, GB18030: 2 bytes), a single backspace leaves orphan
bytes that render as hidden or garbled characters.
Fixes three layers of the problem:
1. Character mode (createXTermRuntime.ts): when backspace is received
and the command-buffer's last character is wider than one byte,
send as many backspace bytes as the character occupies on the wire
so the device deletes the whole character.
2. Local echo (serialLocalEcho.ts): formatSerialLocalEcho now accepts
a backspaceCells parameter so CJK ideographs (2 terminal cells)
erase both cells instead of just one.
3. Line mode (serialLineInput.ts, Terminal.tsx): backspace local echo
now erases the correct number of cells based on the deleted
character's display width.
Adds serialCharMetrics.ts with getCharDisplayWidth (East Asian Width)
and getCharByteLength (UTF-8 / GB18030) utilities.
39 new and updated tests all pass.
* fix: address codex review — surrogate pairs, effective encoding, cursor guard, broadcast
Address all 5 P2 Codex review comments on PR #3325:
1. GB18030 4-byte supplementary chars: CJK Extension B+ (cp > 0xFFFF)
now returns 4 bytes instead of 2, matching iconv-lite's GB18030
encoder.
2. Effective session encoding: add currentEncodingRef to runtime
context so toolbar encoding changes propagate to backspace byte
length calculation (not just the saved host.charset).
3. Cursor-movement guard: track lastInputWasPrintable so backspace
byte expansion only fires when the cursor is at the end of the
typed buffer, not after arrow-key escape sequences.
4. Surrogate-pair-safe slicing: getLastChar/removeLastChar helpers
handle UTF-16 surrogate pairs (emoji, CJK Extension B) so slice(0, -1)
never leaves an orphan high surrogate.
5. Broadcast expansion: when backspace is expanded for the source
serial session, the same expanded data is sent to broadcast peers
instead of the original single-byte backspace.
Adds isPrintableInput, getLastChar, removeLastChar helpers.
56 tests pass.
* fix: address codex review — scope, bracketed paste, cursor confidence
Address 3 new Codex review comments on PR #3325:
1. P1: isExpanded/outData were declared inside the character-mode else
block but referenced after it (ReferenceError in serial line mode).
Moved declarations before the if/else so both branches share scope.
2. P2: Bracketed paste (\x1b[200~...\x1b[201~) starts with ESC so
isPrintableInput returned false, losing tail confidence. Now detected
and lastInputWasPrintable set to true.
3. P2: Printable input after a cursor movement reset lastInputWasPrintable
to true even though the cursor was not at the buffer end. Now the flag
is kept as-is when printable text is typed — only escape sequences
reset it to false.
* fix: establish cursor-tail confidence for initial and post-submission typing
Address Codex review: lastInputWasPrintable started false, so the first
printable input after session start or line submission would keep it
false, defeating the backspace byte expansion guard.
- Initialize to true: cursor is at the end of an empty buffer at
session start and after each line submission.
- Reset to true after Enter (handledSubmittedInput), Ctrl+C, and
Ctrl+U — all three clear the command buffer, placing the cursor
at the beginning of a fresh line.
This restores the original fix for the common case: type 你, press
Backspace, device receives 3 bytes and deletes the whole character.
* fix: address codex review — broadcast safety, paste cursor state, hibernation wake
Address 3 new Codex review comments on PR #3325:
1. Broadcast safety: stop forwarding expanded backspaces to broadcast
peers. Each peer has its own protocol, encoding, and command buffer,
so the source's repeated DELs can over-delete on non-serial peers
(SSH character-level) or on serial peers with different encodings
(GB18030 needs 2 DELs, not 3). The source session still receives
the expanded data; broadcast peers get the original single DEL.
2. Bracketed paste cursor state: no longer unconditionally set
lastInputWasPrintable=true on paste. After a cursor movement the
paste inserts at the moved cursor, not the buffer tail, so the
flag is now preserved as-is (false stays false).
3. Hibernation wake: initialize lastInputWasPrintable conservatively
when commandBufferRef is nonempty. A hibernated session retains
its buffer but the pre-hibernation cursor may have been moved,
so we start with false instead of true.
* fix: grapheme-safe backspace + gate expansion behind byteOrientedBackspace
Address 2 new Codex review comments on PR #3325:
1. Remove the whole grapheme on backspace (P2):
getLastChar/removeLastChar only handled UTF-16 surrogate pairs, so a
decomposed grapheme like e + U+0301 returned only the combining mark
and left the base. Rewrite both with Intl.Segmenter (grapheme
granularity) so surrogate pairs, combining marks, variation
selectors, and ZWJ sequences are removed as one unit. getCharByteLength
and getCharDisplayWidth already compose correctly for graphemes.
2. Gate byte expansion on byte-oriented line editing (P2):
Character-aware serial endpoints (e.g. a UTF-8 Linux console with
readline) delete one whole character per DEL, so repeating the
backspace 3x for a 3-byte char over-deletes. Add an explicit
serialConfig.byteOrientedBackspace flag (default true, preserving the
original fix) that users can disable for character-aware editors.
Wired through the serial connect modal and host details panel, with
i18n in en/zh-CN/zh-TW/ru/es, vault host validation, and session
restore/snapshot preservation.
170 tests pass.
* fix: xterm grapheme width for local echo + always compute cell width
Address 2 new Codex review comments on PR #3325:
1. Use xterm's grapheme width for local-echo erasure (P2):
getCharDisplayWidth returned 1 for emoji-presentation graphemes
(e.g. ❤️, 1️⃣, ©️) whose base characters fall outside the standard
emoji range. The 15-graphemes Unicode provider active in
createXTermRuntime renders these as 2 cells. Now import
stringCellWidth from terminalStringCellWidth.ts (which uses
_core.unicodeService.getStringCellWidth when available) in both
character mode (createXTermRuntime) and line mode (Terminal.tsx,
serialLineInput). Add VS16 (U+FE0F) detection to both the xterm
fallback and the domain getCharDisplayWidth so emoji-presentation
graphemes are correctly 2 cells even without xterm.
2. Compute local-echo width when byte deletion is disabled (P2):
When byteOrientedBackspace is false, the entire backspace block was
skipped, leaving backspaceCells at 1 and erasing only half of a
2-cell CJK/emoji glyph in local echo. Restructured the condition:
display width is now always computed when backspacing a non-empty
buffer, while wire-byte expansion is independently gated on
byteOrientedBackspace.
182 tests pass.
* fix: read byteOrientedBackspace from session snapshot, not live host
Address Codex review (P2): a restored or hibernated session's
snapshotted serialConfig.byteOrientedBackspace can differ from the
current vault host (e.g. session saved false, host later changed to
true). The runtime read the live host instead of the snapshot, so
after recreation a character-aware session could start expanding DEL
and over-delete again.
Add serialByteOrientedBackspace to the runtime context (mirroring
serialLineMode and serialLocalEcho) and pass it from the session
snapshot in useTerminalEffects. Default true preserves the existing
fix for byte-oriented devices.
182 tests pass.
* fix: carry serialByteOrientedBackspace and currentEncodingRef into hibernation wake context
Address Codex review (P2): xTermRuntimeContextRef.current in Terminal.tsx
omitted serialByteOrientedBackspace and currentEncodingRef, so when
hibernateHiddenTabs woke a serial tab the runtime fell back to default
values (byte-oriented=true, host charset). A session snapshot with
byteOrientedBackspace=false would re-enable byte expansion after waking,
and a toolbar-selected encoding would be lost.
Added both fields to the wake context alongside serialLocalEcho and
serialLineMode, mirroring the useTerminalEffects mount path.
182 tests pass.
* fix: tail confidence after line submission, GB18030 4-byte BMP, free-form encodings
Address 3 Codex review comments (all P2):
1. Preserve tail confidence after line submission:
After Enter submission sets lastInputWasPrintable = true, the same
\r/\n went through the generic control-char branch and immediately
reset it to false. This disabled byte expansion and correct local-echo
width for every subsequent command. Now skip the reset when
handledSubmittedInput is true.
2. Measure GB18030 bytes with the backend encoder:
getCharByteLength assumed every BMP non-ASCII character is 2 bytes.
But BMP characters outside the GBK character set (e.g. U+0100 'Ā')
encode to 4 bytes in GB18030. Added isGbkCharacter() to check the
GBK character set ranges; non-GBK BMP characters now return 4.
3. Use the backend charset for free-form serial encodings:
currentEncodingRef is now always present (hibernation wake fix), so
the ?? fallback to host.charset never triggered. For a Shift_JIS host
with no toolbar override, currentEncodingRef.current is 'utf-8' (the
default), causing UTF-8 byte lengths (3 for あ) instead of Shift_JIS
(2). Now prefer host.charset when it's non-UTF-8 and the toolbar
encoding is the default. Also added Shift_JIS support to
resolveWireEncoding and getCharByteLength (1 byte ASCII, 2 bytes
non-ASCII).
182 tests pass.
* fix: honor remembered UTF-8 when counting serial bytes
Address Codex review (P2): the previous fix treated every
toolbarEnc === 'utf-8' as an unselected default and overrode it with
the host charset. But a GB18030 host with a remembered UTF-8 preference
has its backend session synced to UTF-8 (via shouldSyncTerminalEncodingOnAttach),
so using the host's GB18030 charset for byte counting is wrong.
Now use resolveTerminalEncodingFromCharset to check if the host charset
is recognized by the toolbar system (UTF-8 or GB18030). For recognized
charsets, the toolbar encoding is authoritative (it may reflect a
remembered override). For unrecognized charsets (e.g. Shift_JIS), the
toolbar encoding falls back to the default (utf-8), so the host charset
is the correct wire encoding.
193 tests pass (including terminalEncodingPreference.test.ts).
* fix: honor toolbar changes for free-form charsets + GB18030 4-byte CJK Ext A
Address 2 Codex review comments (both P2):
1. Honor toolbar changes for free-form serial charsets:
When a Shift_JIS session starts and the user later selects UTF-8 in
the toolbar, setSessionEncoding switches the backend to UTF-8, but
the runtime continued using host.charset because it is outside the
two-option resolver. Now pass userPickedEncodingRef to the runtime
context (mirroring existing use in shouldSyncTerminalEncodingOnAttach).
The effective encoding logic:
- Non-utf-8 toolbar → trust toolbar (explicit selection)
- utf-8 toolbar + userPickedEncoding → trust toolbar (explicit utf-8)
- utf-8 toolbar + recognized charset → trust toolbar (remembered override)
- utf-8 toolbar + unrecognized charset → use host.charset (backend default)
2. GB18030 4-byte for CJK Extension A:
isGbkCharacter classified U+3400–U+4DBF (CJK Extension A) as 2-byte
GBK characters, but iconv-lite encodes this block as 4 bytes in
GB18030. Removed the range; these characters now correctly return 4.
193 tests pass.
* fix: simplify effective encoding logic with backendSynced flag
Follow-up to previous fix: the 4-level decision tree had an edge case
where a Shift_JIS host with remembered UTF-8 would use host.charset
instead of the synced toolbar encoding. Simplified to a 3-level logic
using backendSynced = userPicked || hasRemembered:
- backendSynced OR recognized charset → trust toolbar encoding
- NOT synced AND unrecognized charset → use host.charset (backend default)
Added hasRememberedEncodingRef to runtime context (alongside existing
userPickedEncodingRef) so the logic mirrors shouldSyncTerminalEncodingOnAttach.
193 tests pass.
* fix: sum grapheme code points, remove Latin-1 from GBK, generic multi-byte fallback
Address 3 Codex review comments (all P2):
1. Sum every code point in non-UTF-8 graphemes:
GB18030 and Shift_JIS returned after the first non-ASCII code point
instead of counting the complete grapheme that getLastChar returns.
❤️ (U+2764 U+FE0F) is 8 GB18030 bytes, but the old code returned 4.
Now accumulate byte lengths for all code points, like the UTF-8 path.
2. Stop classifying Latin-1 as two-byte GBK:
U+00A1–U+00FE was in isGbkCharacter, but iconv-lite encodes these
as 4 bytes in GB18030. Removed the range; these characters now
correctly return 4 bytes.
3. Count bytes for every backend-supported serial charset:
Unknown encodings fell back to UTF-8 byte length, but the backend
uses iconv-lite for Big5, EUC-JP, Johab, etc. (2 bytes for CJK).
Changed fallback to 1 byte ASCII + 2 bytes non-ASCII, matching
iconv-lite's multi-byte encoding for common serial charsets.
Comment 4 (remembered override for free-form charsets) was already
fixed in commit fca4e7c0: backendSynced = userPicked || hasRemembered.
193 tests pass.
* fix: Shift_JIS half-width Katakana as 1 byte (iconv-lite 0xA1-0xDF)
Address Codex review (P2): half-width Katakana characters like ア
(U+FF71) are encoded as single bytes (0xB1) in Shift_JIS by iconv-lite,
but the previous code counted every non-ASCII character as 2 bytes.
Backspace sent 2 DELs, deleting the character and one preceding byte.
Now handle the three 1-byte non-ASCII cases in Shift_JIS:
- Half-width Katakana (U+FF61-U+FF9F, mapped to 0xA1-0xDF) → 1 byte
- ¥ (U+00A5, mapped to 0x5C) → 1 byte
- All other non-ASCII → 2 bytes
Self-review: traced all 4 encoding paths (UTF-8, GB18030, Shift_JIS,
unknown fallback) × all edge cases (single/multi-code-point graphemes,
ASCII/BMP/supplementary, GBK/non-GBK ranges). All paths verified correct.
193 tests pass.
* fix: deduplicate terminal width import after main merge
* test: retain input safety checks with serial erase metadata
* fix: restore serial editing after paste and document byte mode
* fix: keep pending serial input on its original encoding
* fix: restore serial backspace after urgent interruption
* fix: retain editable serial snippet input for backspace
* fix(serial): synchronize pending snippet tail state
* fix(serial): unify empty-line input state across text sources
* fix(serial): track editable startup snippets
* fix(serial): share pending-tail tracking for multiline input
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
500 lines
19 KiB
TypeScript
500 lines
19 KiB
TypeScript
/**
|
|
* 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<SerialConnectModalProps> = ({
|
|
open,
|
|
onClose,
|
|
onConnect,
|
|
onSaveHost,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
const [ports, setPorts] = useState<SerialPort[]>([]);
|
|
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<SerialParity>('none');
|
|
const [flowControl, setFlowControl] = useState<SerialFlowControl>('none');
|
|
const [localEcho, setLocalEcho] = useState(false);
|
|
const [lineMode, setLineMode] = useState(false);
|
|
const [backspaceBehavior, setBackspaceBehavior] = useState<SerialConfig['backspaceBehavior']>('default');
|
|
const [byteOrientedBackspace, setByteOrientedBackspace] = useState(false);
|
|
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,
|
|
byteOrientedBackspace,
|
|
};
|
|
|
|
// 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 (
|
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
<DialogContent className="max-w-md max-h-[85vh] flex flex-col">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Usb size={18} />
|
|
{t('serial.modal.title')}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{t('serial.modal.desc')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-2 overflow-y-auto flex-1 min-h-0">
|
|
{/* Serial Port Selection */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="serial-port">{t('serial.field.port')}</Label>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={loadPorts}
|
|
disabled={isLoadingPorts}
|
|
className="h-6 px-2 text-xs"
|
|
>
|
|
<RefreshCw size={12} className={cn("mr-1", isLoadingPorts && "animate-spin")} />
|
|
{t('common.refresh')}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Combobox for port selection with manual input support */}
|
|
<Combobox
|
|
options={portOptions}
|
|
value={selectedPort}
|
|
onValueChange={setSelectedPort}
|
|
placeholder={t('serial.field.selectPort')}
|
|
emptyText={t('serial.noPorts')}
|
|
allowCreate
|
|
createText={t('common.use')}
|
|
icon={<Usb size={14} className="text-muted-foreground" />}
|
|
/>
|
|
|
|
{!isPortValid && selectedPort && (
|
|
<p className="text-xs text-destructive">
|
|
{t('serial.field.customPortPlaceholder')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Baud Rate */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="baud-rate">{t('serial.field.baudRate')}</Label>
|
|
<Combobox
|
|
options={BAUD_RATES.map((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) && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.customBaudRate')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Advanced Options */}
|
|
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-between h-9 px-0 hover:bg-transparent"
|
|
>
|
|
<span className="text-sm font-medium text-muted-foreground">
|
|
{t('common.advanced')}
|
|
</span>
|
|
{showAdvanced ? (
|
|
<ChevronUp size={14} className="text-muted-foreground" />
|
|
) : (
|
|
<ChevronDown size={14} className="text-muted-foreground" />
|
|
)}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="space-y-4 pt-2">
|
|
{/* Data Bits */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="data-bits">{t('serial.field.dataBits')}</Label>
|
|
<Select
|
|
value={String(dataBits)}
|
|
onValueChange={(v) => setDataBits(parseInt(v, 10) as 5 | 6 | 7 | 8)}
|
|
>
|
|
<SelectTrigger id="data-bits">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DATA_BITS.map((bits) => (
|
|
<SelectItem key={bits} value={String(bits)}>
|
|
{bits}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Stop Bits */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="stop-bits">{t('serial.field.stopBits')}</Label>
|
|
<Select
|
|
value={String(stopBits)}
|
|
onValueChange={(v) => setStopBits(parseFloat(v) as 1 | 1.5 | 2)}
|
|
>
|
|
<SelectTrigger id="stop-bits">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{STOP_BITS.map((bits) => (
|
|
<SelectItem key={bits} value={String(bits)}>
|
|
{bits}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{isStopBits15 && (
|
|
<p className="text-xs text-yellow-500">
|
|
{t('serial.field.stopBits15Warning')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Parity */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="parity">{t('serial.field.parity')}</Label>
|
|
<Select
|
|
value={parity}
|
|
onValueChange={(v) => setParity(v as SerialParity)}
|
|
>
|
|
<SelectTrigger id="parity">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{PARITY_OPTIONS.map((option) => (
|
|
<SelectItem key={option} value={option}>
|
|
{t(`serial.parity.${option}`)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Flow Control */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="flow-control">{t('serial.field.flowControl')}</Label>
|
|
<Select
|
|
value={flowControl}
|
|
onValueChange={(v) => setFlowControl(v as SerialFlowControl)}
|
|
>
|
|
<SelectTrigger id="flow-control">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{FLOW_CONTROL_OPTIONS.map((option) => (
|
|
<SelectItem key={option} value={option}>
|
|
{t(`serial.flowControl.${option}`)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Terminal Options */}
|
|
<div className="space-y-3 pt-2 border-t border-border/60">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="serial-backspace">{t('serial.field.backspaceBehavior')}</Label>
|
|
<Select
|
|
value={backspaceBehavior}
|
|
onValueChange={(value) => setBackspaceBehavior(value === 'ctrl-h' ? 'ctrl-h' : 'default')}
|
|
>
|
|
<SelectTrigger id="serial-backspace">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="default">{t('serial.backspace.default')}</SelectItem>
|
|
<SelectItem value="ctrl-h">{t('serial.backspace.ctrlH')}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.backspaceBehaviorDesc')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="byte-oriented-backspace" className="text-sm font-medium cursor-pointer">
|
|
{t('serial.field.byteOrientedBackspace')}
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.byteOrientedBackspaceDesc')}
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
id="byte-oriented-backspace"
|
|
checked={byteOrientedBackspace}
|
|
onChange={(e) => setByteOrientedBackspace(e.target.checked)}
|
|
className="h-4 w-4 rounded border-input"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="local-echo" className="text-sm font-medium cursor-pointer">
|
|
{t('serial.field.localEcho')}
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.localEchoDesc')}
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
id="local-echo"
|
|
checked={localEcho}
|
|
onChange={(e) => setLocalEcho(e.target.checked)}
|
|
className="h-4 w-4 rounded border-input"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="line-mode" className="text-sm font-medium cursor-pointer">
|
|
{t('serial.field.lineMode')}
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.lineModeDesc')}
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
id="line-mode"
|
|
checked={lineMode}
|
|
onChange={(e) => setLineMode(e.target.checked)}
|
|
className="h-4 w-4 rounded border-input"
|
|
/>
|
|
</div>
|
|
|
|
{/* Charset */}
|
|
<div className="space-y-1">
|
|
<Label htmlFor="serial-charset" className="text-sm font-medium">
|
|
{t('serial.field.charset')}
|
|
</Label>
|
|
<Input
|
|
id="serial-charset"
|
|
placeholder={t("hostDetails.charset.placeholder")}
|
|
value={charset}
|
|
onChange={(e) => setCharset(e.target.value)}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* Save Configuration */}
|
|
{onSaveHost && (
|
|
<div className="space-y-3 pt-2 border-t border-border/60">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="save-config" className="text-sm font-medium cursor-pointer">
|
|
{t('serial.field.saveConfig')}
|
|
</Label>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('serial.field.saveConfigDesc')}
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
id="save-config"
|
|
checked={saveConfig}
|
|
onChange={(e) => setSaveConfig(e.target.checked)}
|
|
className="h-4 w-4 rounded border-input"
|
|
/>
|
|
</div>
|
|
{saveConfig && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="config-label">{t('serial.field.configLabel')}</Label>
|
|
<Input
|
|
id="config-label"
|
|
value={configLabel}
|
|
onChange={(e) => setConfigLabel(e.target.value)}
|
|
placeholder={t('serial.field.configLabelPlaceholder')}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={onClose}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button onClick={handleConnect} disabled={!isValid}>
|
|
{saveConfig ? (
|
|
<Save size={14} className="mr-2" />
|
|
) : (
|
|
<Cpu size={14} className="mr-2" />
|
|
)}
|
|
{saveConfig ? t('serial.connectAndSave') : t('common.connect')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default SerialConnectModal;
|