From 91b4c87a70391ab5cafc43148d746bc1c14053f0 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Mon, 21 Sep 2026 15:24:17 +0100 Subject: [PATCH 1/2] Clarify local switch configuration and remote presets (#825) --- src/App.tsx | 4 ++-- src/Switches.test.tsx | 34 ++++++++++++++++++++++++++++++-- src/settings/SwitchesSection.tsx | 15 +++++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b0852801..38c12636 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -382,7 +382,7 @@ function SetupGuide({ state, switches, suspended, busy, error, skip, finish, acc {step === 0 &&

Input access

{state.accessibility === "required" && }
} - {step === 1 && } + {step === 1 && } {step === 2 &&

Use your switches

Focus the application you want to use, then press and release Select to start scanning. Select chooses a point and opens the action menu.

Automatic scanning moves the highlight for you. Use Auto scan rate for grid and menu timing, and Line speed for scanning lines. Holding a switch freezes movement; release runs the action shown.

Escape disables switch control. Assign Stop scanning or Pause / resume in Switches. Holding any switch for {switches.state ? switches.state.escapeHoldMs / 1000 : 4}s disables switch control.

Open Scanning after setup to adjust movement. Android connection is optional.

} {step === 3 &&

Choose startup behavior

Switchify can start quietly when you sign in, ready for your switches.

} {step === 4 &&

Choose whether to share diagnostics

Optional anonymous app health and sanitized errors help improve Switchify. Typed text, commands, pairing secrets, device names, and full paths are never included.

Privacy policy
} @@ -752,7 +752,7 @@ export function App() {

{updateNotice}

{view === "home" && void perform(api.disconnectAll)} onAccessibility={() => void perform(() => api.checkAccessibility(true))} />} - {view === "switches" &&

Switches

} + {view === "switches" &&

Switches

} {view === "scanning" &&

Scanning

} {(view === "android" || view === "devices" || view === "profiles") &&

Android connection

Optional Android control and switch forwarding

{view === "android" && void perform(api.disconnectAll)} />} diff --git a/src/Switches.test.tsx b/src/Switches.test.tsx index 11c0d502..0eebdb18 100644 --- a/src/Switches.test.tsx +++ b/src/Switches.test.tsx @@ -51,12 +51,12 @@ const scan = { message: "Scanning is off.", supported: true, }; -function Shell({ visible = true }: { visible?: boolean }) { +function Shell({ visible = true, androidConnected = false }: { visible?: boolean; androidConnected?: boolean }) { const switches = useSwitches(); const scanning = useScanning(); return ( <> - {visible && } + {visible && } ); @@ -400,3 +400,33 @@ it("removes a remote switch and surfaces a failed remote save with retry", async await waitFor(() => expect(remote.slots[0].pressAction).toBeNull()); expect(screen.queryByRole("heading", { name: "Remote switch 1" })).toBeNull(); }); + +it("separates an empty local configuration from the six remote presets", async () => { + current.settings.bindings = []; + remote.slots = ["select", "next", "back", "pause", "reverse", "stop", null, null] + .map((pressAction) => ({ pressAction, holdActions: [] })); + render(); + await screen.findByRole("heading", { name: "Remote switch 6" }); + expect(screen.getByText(/No local switches configured/)).toBeInTheDocument(); + expect(screen.getByText(/not detected physical switches/)).toBeInTheDocument(); + expect(screen.getByText(/No Android device connected/)).toBeInTheDocument(); + expect(screen.getAllByText(/Remote forwarding slot/)).toHaveLength(6); + expect(mocks.invoke.mock.calls.some(([command]) => command.startsWith("save_"))).toBe(false); +}); +it("keeps source identity after renaming and separates connection from tested input", async () => { + const view = render(); + await screen.findByRole("heading", { name: "Remote switch 1" }); + open("Remote switch 1"); + fireEvent.change(screen.getByLabelText("Name for Remote 1"), { target: { value: "Chin" } }); + await waitFor(() => expect(remote.slots[0].name).toBe("Chin")); + const row = screen.getByRole("heading", { name: "Chin" }).closest("article")!; + expect(within(row).getByText("Remote forwarding slot 1")).toBeInTheDocument(); + expect(within(row).getByText("Remote 1")).toBeInTheDocument(); + const localRow = screen.getByRole("heading", { name: "Head switch" }).closest("article")!; + expect(within(localRow).getByText("Local keyboard input")).toBeInTheDocument(); + view.rerender(); + expect(screen.getByText(/Android device connected. This does not confirm/)).toHaveTextContent("Input reception is not tested on this page."); + expect(screen.queryByText(/No Android device connected/)).not.toBeInTheDocument(); + view.rerender(); + expect(screen.getByText(/No Android device connected/)).toBeInTheDocument(); +}); diff --git a/src/settings/SwitchesSection.tsx b/src/settings/SwitchesSection.tsx index 98270f6c..f520b7e4 100644 --- a/src/settings/SwitchesSection.tsx +++ b/src/settings/SwitchesSection.tsx @@ -329,7 +329,7 @@ function CaptureDialog({ name, onCancel }: { name: string; onCancel: () => void ); } -export function SwitchesSection({ controller, onDraftChange, suspended = false }: { controller: SwitchController; onDraftChange?: (draft: boolean) => void; suspended?: boolean }) { +export function SwitchesSection({ controller, onDraftChange, suspended = false, androidConnected }: { controller: SwitchController; onDraftChange?: (draft: boolean) => void; suspended?: boolean; androidConnected?: boolean }) { const { settings, state, pending, unsaved } = controller; // A refused capture sets both the general error and the capture error; the // key field already shows the latter, so the band only carries save errors. @@ -556,6 +556,17 @@ export function SwitchesSection({ controller, onDraftChange, suspended = false } title="Switches" description="Add keyboard switches or switches forwarded from Switchify Remote, and choose what each one does." > +
+

{!state ? "Loading local switch configuration..." : settings.bindings.length === 0 + ? "No local switches configured. Use Add switch to learn a keyboard key." + : `${settings.bindings.length} local keyboard ${settings.bindings.length === 1 ? "switch configured" : "switches configured"}.`}

+

Remote assignments are numbered forwarding slots in Switchify Remote, not detected physical switches. The supplied presets can be changed or removed.

+

{androidConnected === true + ? "Android device connected. This does not confirm that remote assignments match your switches." + : androidConnected === false + ? "No Android device connected. Remote assignments are available for a future connection." + : "Android connection status is unavailable here."} Input reception is not tested on this page.

+