import { LuPlus, LuTrash2, LuPencil, LuSettings2, LuEye, LuEyeOff, LuSave } 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"; /** ============== Types ============== */ interface SerialSettings { baudRate: string; dataBits: string; stopBits: string; parity: string; } interface QuickButton { id: string; // uuid-ish label: string; // shown on the button command: string; // raw command to send (without auto-terminator) sort: number; // for stable ordering } interface ButtonConfig { buttons: QuickButton[]; terminator: string; // CR/CRLF/None hideSerialSettings: boolean; } /** ============== Component ============== */ export function SerialButtons() { const { send } = useJsonRpc(); // serial settings (same as SerialConsole) const [serialSettings, setSerialSettings] = useState({ baudRate: "9600", dataBits: "8", stopBits: "1", parity: "none", }); // extension config (buttons + prefs) const [buttonConfig, setButtonConfig] = useState({ buttons: [], terminator: "", hideSerialSettings: false, }); // editor modal state const [editorOpen, setEditorOpen] = useState(null); const [draftLabel, setDraftLabel] = useState(""); const [draftCmd, setDraftCmd] = useState(""); // load serial settings like SerialConsole useEffect(() => { send("getSerialSettings", {}, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( `Failed to get serial settings: ${resp.error.data || "Unknown error"}`, ); return; } setSerialSettings(resp.result as SerialSettings); }); send("getSerialButtonConfig", {}, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( `Failed to get button config: ${resp.error.data || "Unknown error"}`, ); return; } setButtonConfig(resp.result as ButtonConfig); }); }); const handleSerialSettingChange = (setting: keyof SerialSettings, value: string) => { const newSettings = { ...serialSettings, [setting]: value }; send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error(`Failed to update serial settings: ${resp.error.data || "Unknown error"}`); return; } setSerialSettings(newSettings); }); }; const handleSerialButtonConfigChange = (config: keyof ButtonConfig, value: unknown) => { const newButtonConfig = { ...buttonConfig, [config]: value }; send("setSerialButtonConfig", { config: newButtonConfig }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error(`Failed to update button config: ${resp.error.data || "Unknown error"}`); return; } setButtonConfig(newButtonConfig); }); }; const onClickButton = (btn: QuickButton) => { /** build final string to send: * if the user's button command already contains a terminator, we don't append the selected terminator safely */ const raw = btn.command; const t = buttonConfig.terminator ?? ""; const command = raw.endsWith("\r") || raw.endsWith("\n") ? raw : raw + t; send("sendCustomCommand", { command }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( `Failed to send ATX power action: ${resp.error.data || "Unknown error"}`, ); } }); }; /** CRUD helpers */ const addNew = () => { setEditorOpen({ id: undefined }); setDraftLabel(""); setDraftCmd(""); }; const editBtn = (btn: QuickButton) => { setEditorOpen({ id: btn.id }); setDraftLabel(btn.label); setDraftCmd(btn.command); }; const removeBtn = (id: string) => { const nextButtons = buttonConfig.buttons.filter(b => b.id !== id).map((b, i) => ({ ...b, sort: i })) ; handleSerialButtonConfigChange("buttons", stableSort(nextButtons) ); setEditorOpen(null); }; const saveDraft = () => { const label = draftLabel.trim() || "Unnamed"; const command = draftCmd.trim(); if (!command) { notifications.error("Command cannot be empty."); return; } const isEdit = editorOpen?.id; const nextButtons = isEdit ? buttonConfig.buttons.map(b => (b.id === isEdit ? { ...b, label, command } : b)) : [...buttonConfig.buttons, { id: genId(), label, command, sort: buttonConfig.buttons.length }]; handleSerialButtonConfigChange("buttons", stableSort(nextButtons) ); setEditorOpen(null); }; /** simple reordering: alphabetical by sort, then label */ const sortedButtons = useMemo(() => stableSort(buttonConfig.buttons), [buttonConfig.buttons]); return (
{/* Top actions */}
{/* Serial settings (collapsible) */} {!buttonConfig.hideSerialSettings && ( <>
handleSerialSettingChange("baudRate", e.target.value)} /> handleSerialSettingChange("dataBits", e.target.value)} /> handleSerialSettingChange("stopBits", e.target.value)} /> handleSerialSettingChange("parity", e.target.value)} />
handleSerialButtonConfigChange("terminator", e.target.value)} />
)} {/* 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); }} />
The selected line ending ({pretty(buttonConfig.terminator)}) will be appended when sent.
)}
); } /** ============== helpers ============== */ function pretty(s: string) { return s.replace(/\r/g, "\\r").replace(/\n/g, "\\n"); } 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)); }