import { LuPlus, LuTrash2, LuPencil, LuSettings2, LuEye, LuEyeOff, LuSave, LuArrowBigUp, LuArrowBigDown, LuCircleX, LuTerminal } from "react-icons/lu"; import { useEffect, useMemo, useState } from "react"; import { Button } from "@components/Button"; import Card from "@components/Card"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { InputFieldWithLabel } from "@components/InputField"; import { useUiStore, useTerminalStore } from "@/hooks/stores"; import Checkbox from "@components/Checkbox"; import {SettingsItem} from "@components/SettingsItem"; /** ============== Types ============== */ interface QuickButton { id: string; // uuid-ish label: string; // shown on the button command: string; // raw command to send (without auto-terminator) terminator: {label: string, value: string}; // None/CR/LF/CRLF/LFCR sort: number; // for stable ordering } interface SerialSettings { baudRate: number; dataBits: number; stopBits: string; parity: string; terminator: {label: string, value: string}; // None/CR/LF/CRLF/LFCR hideSerialSettings: boolean; enableEcho: boolean; // future use normalizeMode: string; // future use normalizeLineEnd: string; // future use tabRender: string; // future use preserveANSI: boolean; // future use showNLTag: boolean; // future use buttons: QuickButton[]; } /** ============== Component ============== */ export function SerialConsole() { const { setTerminalType } = useUiStore(); const { setTerminator } = useTerminalStore(); const { send } = useJsonRpc(); // extension config (buttons + prefs) const [buttonConfig, setButtonConfig] = useState({ baudRate: 9600, dataBits: 8, stopBits: "1", parity: "none", terminator: {label: "LF (\\n)", value: "\n"}, hideSerialSettings: false, enableEcho: false, normalizeMode: "names", normalizeLineEnd: "keep", tabRender: "", preserveANSI: true, showNLTag: true, buttons: [], }); type NormalizeMode = "caret" | "names" | "hex"; // note: caret (not carret) const normalizeHelp: Record = { caret: "Caret notation: e.g. Ctrl+A as ^A, Esc as ^[", names: "Names: e.g. Ctrl+A as , Esc as ", hex: "Hex notation: e.g. Ctrl+A as 0x01, Esc as 0x1B", }; // editor modal state const [editorOpen, setEditorOpen] = useState(null); const [draftLabel, setDraftLabel] = useState(""); const [draftCmd, setDraftCmd] = useState(""); const [draftTerminator, setDraftTerminator] = useState({label: "LF (\\n)", value: "\n"}); // load serial settings like SerialConsole useEffect(() => { send("getSerialSettings", {}, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( `Failed to get button config: ${resp.error.data || "Unknown error"}`, ); return; } setButtonConfig(resp.result as SerialSettings); setTerminator((resp.result as SerialSettings).terminator.value); }); }, [send, setTerminator]); const handleSerialSettingsChange = (config: keyof SerialSettings, value: unknown) => { const newButtonConfig = { ...buttonConfig, [config]: value }; send("setSerialSettings", { settings: newButtonConfig }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error(`Failed to update serial settings: ${resp.error.data || "Unknown error"}`); return; } }); setButtonConfig(newButtonConfig); }; const onClickButton = (btn: QuickButton) => { const command = btn.command + btn.terminator.value; send("sendCustomCommand", { command }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( `Failed to send custom command: ${resp.error.data || "Unknown error"}`, ); } }); }; /** CRUD helpers */ const addNew = () => { setEditorOpen({ id: undefined }); setDraftLabel(""); setDraftCmd(""); setDraftTerminator({label: "LF (\\n)", value: "\n"}); }; const editBtn = (btn: QuickButton) => { setEditorOpen({ id: btn.id }); setDraftLabel(btn.label); setDraftCmd(btn.command); setDraftTerminator(btn.terminator); }; const removeBtn = (id: string) => { const nextButtons = buttonConfig.buttons.filter(b => b.id !== id).map((b, i) => ({ ...b, sort: i })) ; handleSerialSettingsChange("buttons", stableSort(nextButtons) ); setEditorOpen(null); }; const moveUpBtn = (id: string) => { // Make a copy so we don't mutate state directly const newButtons = [...buttonConfig.buttons]; // Find the index of the button to move const index = newButtons.findIndex(b => b.id === id); if (index > 0) { // Swap with the previous element [newButtons[index - 1], newButtons[index]] = [ newButtons[index], newButtons[index - 1], ]; } // Re-assign sort values const nextButtons = newButtons.map((b, i) => ({ ...b, sort: i })); handleSerialSettingsChange("buttons", stableSort(nextButtons) ); setEditorOpen(null); }; const moveDownBtn = (id: string) => { // Make a copy so we don't mutate state directly const newButtons = [...buttonConfig.buttons]; // Find the index of the button to move const index = newButtons.findIndex(b => b.id === id); if (index >= 0 && index < newButtons.length - 1) { // Swap with the next element [newButtons[index], newButtons[index + 1]] = [ newButtons[index + 1], newButtons[index], ]; } // Re-assign sort values const nextButtons = newButtons.map((b, i) => ({ ...b, sort: i })); handleSerialSettingsChange("buttons", stableSort(nextButtons) ); setEditorOpen(null); }; const saveDraft = () => { const label = draftLabel.trim() || "Unnamed"; const command = draftCmd; if (!command) { notifications.error("Command cannot be empty."); return; } const terminator = draftTerminator; console.log("Saving draft:", { label, command, terminator }); // if editing, get current id, otherwise undefined => new button const currentID = editorOpen?.id; // either update existing or add new // if new, assign next sort index // if existing, keep sort index const nextButtons = currentID ? buttonConfig.buttons.map(b => (b.id === currentID ? { ...b, label, command , terminator} : b)) : [...buttonConfig.buttons, { id: genId(), label, command, terminator, sort: buttonConfig.buttons.length }]; handleSerialSettingsChange("buttons", stableSort(nextButtons) ); setEditorOpen(null); }; /** simple reordering: alphabetical by sort, then label */ const sortedButtons = useMemo(() => buttonConfig.buttons, [buttonConfig.buttons]); return (
{/* Top actions */}

{/* Serial settings (collapsible) */} {!buttonConfig.hideSerialSettings && ( <>
handleSerialSettingsChange("baudRate", Number(e.target.value))} /> handleSerialSettingsChange("dataBits", Number(e.target.value))} /> handleSerialSettingsChange("stopBits", e.target.value)} /> handleSerialSettingsChange("parity", e.target.value)} />
{ handleSerialSettingsChange("terminator", {label: e.target.selectedOptions[0].text, value: e.target.value}) setTerminator(e.target.value); }} />
When sent, the selected line ending ({buttonConfig.terminator.label}) will be appended.
{ handleSerialSettingsChange("normalizeMode", e.target.value) }} />
{normalizeHelp[(buttonConfig.normalizeMode as NormalizeMode)]}
{ handleSerialSettingsChange("normalizeLineEnd", e.target.value) }} />
{ handleSerialSettingsChange("preserveANSI", e.target.value === "keep") }} />
tag", value: "hide" }, { label: "Show tag", value: "show" }, ]} value={buttonConfig.showNLTag ? "show" : "hide"} onChange={(e) => { handleSerialSettingsChange("showNLTag", e.target.value === "show") }} />
{ handleSerialSettingsChange("tabRender", e.target.value) }} />
Empty for no replacement
{ handleSerialSettingsChange("enableEcho", e.target.checked); }} />

)} {/* Buttons grid */}
{sortedButtons.map((btn) => (
))} {sortedButtons.length === 0 && (
No buttons yet. Click “Add Button”.
)}
{/* Editor drawer/modal (inline lightweight) */} {editorOpen && (
{editorOpen.id ? "Edit Button" : "New Button"}
{ setDraftLabel(e.target.value); }} />
{ setDraftCmd(e.target.value); }} /> {draftTerminator.value != "" && (
When sent, the selected line ending ({draftTerminator.label}) will be appended.
)}
setDraftTerminator({label: e.target.selectedOptions[0].text, value: e.target.value})} />
{editorOpen.id && ( <>
)}
); } /** ============== helpers ============== */ function genId() { return "b_" + Math.random().toString(36).slice(2, 10); } function stableSort(arr: QuickButton[]) { return [...arr].sort((a, b) => (a.sort - b.sort) || a.label.localeCompare(b.label)); }