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.
{view === "android" && void perform(api.disconnectAll)} />}
diff --git a/src/Switches.test.tsx b/src/Switches.test.tsx
index 11c0d502..cc4bb043 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("A connection alone does not verify a physical switch press.");
+ 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..f40575a5 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."} A connection alone does not verify a physical switch press.