Mobile connection is optional. Bluetooth advertises automatically; local switches do not need a mobile device. Connecting a mobile device pauses local scanning until it disconnects.
+
Optional. Connecting a mobile device pauses local scanning.
-
Install Switchify for mobile
Need the mobile app? Scan the QR code or open Google Play to install Switchify.
{switches.state?.settings.bindings.length ?? 0} configured. Open Switches to assign Select, then open Scanning to adjust movement.
Optional mobile connection
{bluetoothLabels[state.bluetooth]}. Open Mobile connection for pairing and forwarding profiles.
> :
-
Bluetooth connection
{bluetoothLabels[state.bluetooth]}
-
Input access
{state.accessibility === "required" ? : }
-
Application update
Switchify PC {state.version}
-
Diagnostics
Export sanitized health, capability, and recent event data
+
Bluetooth connection
{bluetoothLabels[state.bluetooth]}
+
Input access
{state.accessibility === "required" ? : }
+
Application update
Switchify PC {state.version}
+
Diagnostics
Export sanitized health, capability, and recent event data
Recent Bluetooth changes
{state.diagnostics.recentBluetooth.length > 0 ? state.diagnostics.recentBluetooth.map((event) => event.status).join(" → ") : "No Bluetooth changes recorded yet"}
Last disconnect
{state.diagnostics.lastDisconnect ? `${state.diagnostics.lastDisconnect.detail ?? state.diagnostics.lastDisconnect.status}` : "No disconnect recorded yet"}
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. Mobile 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.
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.
;
}
diff --git a/src/help/demonstrations.css b/src/help/demonstrations.css
index 34211840..f1b309bf 100644
--- a/src/help/demonstrations.css
+++ b/src/help/demonstrations.css
@@ -1,3 +1,4 @@
+@layer components {
.teaching-disclosure { margin: 12px 0; }
.teaching-disclosure > button { text-align: left; white-space: normal; }
.teaching-demo { margin-top: 12px; padding: 16px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); max-width: 760px; }
@@ -8,9 +9,11 @@
.teaching-art svg text[fill="#fff"] { fill: #fff; }
.teaching-art svg g, .teaching-art svg circle, .teaching-art svg rect { transition: transform 500ms ease, opacity 500ms ease, cy 500ms ease, cx 500ms ease, fill 500ms ease; }
.teaching-caption { line-height: 1.5; }
-.teaching-controls { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
-.teaching-steps { padding-left: 24px; font-size: 13px; line-height: 1.6; }
+.teaching-controls { display: flex; flex-wrap: wrap; gap: 12px; margin: 12px 0; }
+.teaching-steps { padding-left: 24px; font-size: 16px; line-height: 1.6; }
.teaching-steps li { margin: 8px 0; }
.teaching-steps li[aria-current="step"] { font-weight: 600; }
.teaching-demo[data-moving="false"] svg *, .teaching-demo[data-reduced="true"] svg * { transition: none; }
@media (prefers-reduced-motion: reduce) { .teaching-art svg * { transition: none; } }
+
+}
diff --git a/src/help/demonstrations.ts b/src/help/demonstrations.ts
index ae7ab1de..7965aae0 100644
--- a/src/help/demonstrations.ts
+++ b/src/help/demonstrations.ts
@@ -15,5 +15,17 @@ export function demonstration(kind: DemonstrationKind, platform: DemoPlatform) {
pair: { title: "Pair a mobile device", steps: ["Open Switchify on your mobile device and choose this computer while it is advertising.", "Compare the six-digit codes in both apps. These are example codes, not a live pairing request.", "Approve on this computer only if the codes match. If they differ or you did not request pairing, reject the request.", "A successful pairing trusts the mobile device. Check the real connection status in the app."] },
startup: { title: "Understand startup behavior", steps: ["Start with system is optional. Choose whether Switchify should launch when you sign in.", `When enabled, Switchify starts quietly in the ${location}.`, `Open Switchify from the ${location} when you need its window. Start manually means opening the app yourself.`] },
};
- return examples[kind];
+ const captions: Record = {
+ jack: ["Check the connector.", "Plug in your switch.", "Connect the interface. Then add your switch."],
+ usb: ["Your switch sends a key.", "Plug it into your computer.", "Add your switch and learn its key."],
+ learn: ["Choose Add switch, then Learn key.", "Press and release your switch.", "Check the key shown."],
+ select: ["Open Press and release.", "Choose Select.", "Save your switch."],
+ grid: ["Press Select to start.", "Select a row.", "Select a cell.", "Select the horizontal position.", "Select the vertical position.", "Auto select: wait to click, or press for actions.", "Without Auto select: choose an action."],
+ line: ["Press Select to start.", "Select the horizontal position.", "Select the vertical position.", "Auto select: wait to click, or press for actions.", "Without Auto select: choose an action."],
+ hold: ["Press to freeze movement.", "Release to choose.", "Hold to see more actions.", "Wait for the action you want.", "Release to run it."],
+ access: ["Open Accessibility Settings.", "Find Switchify PC.", "Turn it on, then return here."],
+ pair: ["Choose this computer on your mobile.", "Compare the codes in both apps.", "Approve only if the codes match.", "Check the connection status."],
+ startup: ["Choose whether to start at sign-in.", `Switchify starts in the ${location}.`, `Open its window from the ${location}.`],
+ };
+ return { ...examples[kind], captions: captions[kind] };
}
diff --git a/src/scanning/SwitchPractice.tsx b/src/scanning/SwitchPractice.tsx
index c209fe1c..420e60cb 100644
--- a/src/scanning/SwitchPractice.tsx
+++ b/src/scanning/SwitchPractice.tsx
@@ -1,3 +1,4 @@
+import { Button, Select } from "../ui/controls";
import { createPortal } from "react-dom";
import { useEffect, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
@@ -56,9 +57,9 @@ export function SwitchPractice({ disabled = false }: { disabled?: boolean }) {
};
const highlighted = view?.blocks.filter(block => block.selected).map(block => block.label).join(", ");
return
-
Test your saved switches safely with a simple set of practice blocks. Select starts a scan; Next and Previous move the highlight.
legend="Overlay visibility" disabled={!settings.cursorOverlayEnabled} options={overlayVisibilityOptions} value={settings.cursorOverlayVisibility} onChange={(next) => update("cursorOverlayVisibility", next)}
note={{ about: "overlay visibility", summary: "Choose when the overlay stays on screen.", detail: "On input hides shortly after pointer activity stops. While controlling stays visible until the session ends." }} />
- legend="Overlay size" columns="three" disabled={!settings.cursorOverlayEnabled} options={overlaySizeOptions} value={settings.cursorOverlaySize} onChange={(next) => update("cursorOverlaySize", next)} />
+ legend="Cursor size" columns="three" disabled={!settings.cursorOverlayEnabled} options={overlaySizeOptions} value={settings.cursorOverlaySize} onChange={(next) => update("cursorOverlaySize", next)} />
update("cursorCrosshairs", value)} />
diff --git a/src/settings/PointerSection.tsx b/src/settings/PointerSection.tsx
index b5eab0a6..4a309485 100644
--- a/src/settings/PointerSection.tsx
+++ b/src/settings/PointerSection.tsx
@@ -1,3 +1,4 @@
+import { Button, Select, MoreOptions } from "../ui/controls";
import { useEffect, useId, useState } from "react";
import type { AppSettings } from "../types";
import {
@@ -14,20 +15,27 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up
const [showExact, setShowExact] = useState(!isPreset);
useEffect(() => { if (!isPreset) setShowExact(true); }, [isPreset]);
const exactSpeedId = useId();
- return
+ return
-
+
+ update("dwellClickEnabled", value)} />
+
+ legend="Wait before clicking" columns="five" disabled={!settings.dwellClickEnabled} options={secondsOptions(dwellDelayOptions)} value={settings.dwellClickDelayMs} onChange={(next) => update("dwellClickDelayMs", next)}
+ note={{ summary: "After mobile pointer movement stops, a countdown appears and performs one left click. Move again to rearm it." }} />
+
+
+
update("mouseRepeatEnabled", value)} />
legend="Movement interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.moveRepeatIntervalMs} onChange={(next) => update("moveRepeatIntervalMs", next)} />
@@ -43,12 +51,6 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up
note={{ about: "key repeat", summary: "Held navigation keys repeat, like on a keyboard.", detail: "Applies to the arrow keys, Tab, Backspace, Delete, Page Up, and Page Down." }} />
-
- update("dwellClickEnabled", value)} />
-
- legend="Dwell delay" columns="five" disabled={!settings.dwellClickEnabled} options={secondsOptions(dwellDelayOptions)} value={settings.dwellClickDelayMs} onChange={(next) => update("dwellClickDelayMs", next)}
- note={{ summary: "After mobile pointer movement stops, a countdown appears and performs one left click. Move again to rearm it." }} />
-
{state.telemetry.available ? state.telemetry.consent === "undecided" ? "No choice recorded yet. Nothing is sent unless you choose Share diagnostics." : state.telemetry.consent === "enabled" ? "Consent recorded. You can turn this off at any time to delete queued reports." : "Opted out. No diagnostic reports are stored or sent." : "Diagnostic reporting is unavailable in this build."} Privacy policy
{state.telemetry.available ? state.telemetry.consent === "undecided" ? "No choice recorded yet. Nothing is sent unless you choose Share diagnostics." : state.telemetry.consent === "enabled" ? "Consent recorded. You can turn this off at any time to delete queued reports." : "Opted out. No diagnostic reports are stored or sent." : "Diagnostic reporting is unavailable in this build."} Privacy policy
;
}
diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx
index f23486c3..18260d5f 100644
--- a/src/settings/ScannerPreferences.tsx
+++ b/src/settings/ScannerPreferences.tsx
@@ -1,3 +1,4 @@
+import { Button, Input, Select, MoreOptions } from "../ui/controls";
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react';
import type { ScanningController, ScannerColor, PointScanConfig } from '../scanning/useScanning';
import { areaOptions, sharedOptions, defaultScanPreferences, type ScanArea, type ScanOptions } from '../scanning/preferences';
@@ -50,37 +51,23 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl
const custom = area !== 'shared' && settings[area][key] != null;
return
{effective.automatic ? 'Time each row or item stays highlighted. Lower values scan faster. Line speed is separate.' : 'Used in automatic scanning. Your rate is kept while scanning manually.'}
+
{effective.automatic ? 'Seconds per highlight. Less time means faster scanning.' : 'Used in automatic scanning. Your rate is kept while scanning manually.'}
>)}
+
+
{field('direction', 'Initial direction', locked => legend="Initial direction" disabled={locked} value={effective.direction} options={[{ value: 'forward', label: 'Forward' }, { value: 'reverse', label: 'Reverse' }]} onChange={value => change('direction', value)} />)}
{field('passLimit', 'Pass limit', locked => legend="Pass limit" disabled={locked} value={effective.passLimit} options={[1, 2, 3, 5, 0].map(value => ({ value, label: value ? `${value} ${value === 1 ? 'pass' : 'passes'}` : 'Unlimited' }))} onChange={value => change('passLimit', value)}
note={{ summary: 'After this many automatic passes, scanning waits for Select. Unlimited keeps scanning until you pause or stop.' }} />)}
{area !== 'point' && field('pattern', 'Item scanning', locked => legend="Item scanning" disabled={locked} value={effective.pattern} options={[{ value: 'grouped', label: 'Groups, then items' }, { value: 'linear', label: 'One item at a time' }]} onChange={value => change('pattern', value)}
note={{ summary: 'Grouped scanning chooses a row or group before an item. Linear scanning visits each item directly. Point scanning uses its own line and grid modes.' }} />)}
-
+
{field('color', 'Scanner colour', locked => )}
{field('thickness', 'Highlight thickness', locked => legend="Highlight thickness" disabled={locked} value={effective.thickness} options={(['thin', 'standard', 'thick'] as const).map(value => ({ value, label: value[0].toUpperCase() + value.slice(1) }))} onChange={value => change('thickness', value)} />)}
Each switch action needs a different key. Escape is reserved for cancel.
}
+ How scanning works
Focus the application you want to use, then press Select to scan the display under the pointer. Choose a point, then use the action menu for clicks, scrolling and dragging.
Assign switch actions in the Switches page. Actions run on release; holding a switch freezes movement. Manual scanning needs Select, Next and Previous. After clicking or reaching the pass limit, use Select to start again.
On Windows, assigned keys remain switches with Shift, Ctrl, Alt or Windows held, even in the background. Other keys and Switchify-generated shortcuts still work normally.
-
;
}
diff --git a/src/settings/SwitchesSection.tsx b/src/settings/SwitchesSection.tsx
index 61a1a14a..ef000331 100644
--- a/src/settings/SwitchesSection.tsx
+++ b/src/settings/SwitchesSection.tsx
@@ -1,3 +1,4 @@
+import { Button, Input, Select, MoreOptions } from "../ui/controls";
import { Demonstration, SwitchDemonstrations } from "../help/Demonstration";
import { SwitchPractice } from "../scanning/SwitchPractice";
import { createPortal } from "react-dom";
@@ -51,7 +52,7 @@ function ActionSelect({
onChange: (value: SwitchAction) => void;
}) {
return (
- onChange(e.target.value as SwitchAction)}
@@ -61,7 +62,7 @@ function ActionSelect({
{label}
))}
-
+
);
}
@@ -130,7 +131,7 @@ function SwitchEditor({
{!remote && }
{remote ? (
) : (
Key
-
{binding.key ? "Change key" : "Learn key"}
-
+
{(keyError || unavailable) && (
@@ -223,7 +224,7 @@ function SwitchEditor({
})
}
/>
- move(index, -1)}
>
-
-
+ move(index, 1)}
>
-
-
+
-
+
))}
@@ -267,7 +268,7 @@ function SwitchEditor({
.join(", ")}. Release to run the action shown.`
: "Holding only freezes movement. Add actions to offer them one by one while held."}
- = 32}
@@ -277,25 +278,25 @@ function SwitchEditor({
}
>
Add hold action
-
+
Keyboard typing is on. Switch scanning is paused.
- { void toggleEntry(false); }}>Resume switch control
+ { void toggleEntry(false); }}>Resume switch control
{!state ? "Loading local switch configuration..." : settings.bindings.length === 0
- ? "No local switches configured. Use Add switch to learn a switch."
- : `${settings.bindings.length} local ${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.
-
{mobileConnected === true
- ? "Mobile device connected. This does not confirm that remote assignments match your switches."
- : mobileConnected === false
- ? "No mobile device connected. Remote assignments are available for a future connection."
- : "Mobile connection status is unavailable here."} A connection alone does not verify a physical switch press.
-
-
{pending || remote.pending
? "Saving switches..."
: unsaved || remote.unsaved
? "Switch assignments have unsaved changes."
- : "Changes save automatically and apply straight away."}
+ : "Saved automatically."}
{!state ? "Loading local switch configuration..." : settings.bindings.length === 0
+ ? "No local switches configured. Use Add switch to learn a switch."
+ : `${settings.bindings.length} local ${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.
+
{mobileConnected === true
+ ? "Mobile device connected. This does not confirm that remote assignments match your switches."
+ : mobileConnected === false
+ ? "No mobile device connected. Remote assignments are available for a future connection."
+ : "Mobile connection status is unavailable here."} A connection alone does not verify a physical switch press.
+
+
+
+
>
);
}
diff --git a/src/settings/UpdatesSection.tsx b/src/settings/UpdatesSection.tsx
index e06237b1..ed57591d 100644
--- a/src/settings/UpdatesSection.tsx
+++ b/src/settings/UpdatesSection.tsx
@@ -1,3 +1,4 @@
+import { Button } from "../ui/controls";
import { Download, RefreshCw, X } from "lucide-react";
import type { Ref } from "react";
import type { AppState, UpdateState, UpdateStatus } from "../types";
@@ -64,7 +65,7 @@ export function UpdateControls({ update, run, cancel }: { update: UpdateState; r
{updateProgress(update)}
>}
-