- Switch keys are reserved while enabled. Escape stops scanning and
- releases them. Disconnect Android before enabling local point scan.
+ Scanning is on whenever a switch has the Select action, and its keys
+ stay reserved while Switchify runs. Escape resets the scan. Android
+ connections pause local scanning until they end.
On Windows, use switch keys without Shift, Ctrl, Alt or Windows held.
@@ -60,18 +51,6 @@ export function ScanningSection({
: (state?.message ?? "Loading point scan...")}
-
{pending
? "Saving scanning settings..."
@@ -118,7 +97,7 @@ export function ScanningSection({
legend="Mode"
@@ -173,7 +152,7 @@ export function ScanningSection({
>
)}
- Settings are saved, but scanning stays off when the app restarts.
+ Settings are saved and scanning resumes when the app restarts.
>
);
diff --git a/src/settings/SettingsView.tsx b/src/settings/SettingsView.tsx
index eea2b84e..22abae50 100644
--- a/src/settings/SettingsView.tsx
+++ b/src/settings/SettingsView.tsx
@@ -62,7 +62,7 @@ export function SettingsView({ switches, scanning, state, settings, onChange, ch
{active === "general" && }
{active === "pointer" && }
- {active === "switches" && }
+ {active === "switches" && }
{active === "scanning" && }
{active === "cursor" && }
{active === "privacy" && }
diff --git a/src/settings/SwitchesSection.tsx b/src/settings/SwitchesSection.tsx
index 2551455b..c81cc38f 100644
--- a/src/settings/SwitchesSection.tsx
+++ b/src/settings/SwitchesSection.tsx
@@ -307,13 +307,7 @@ function CaptureDialog({ name, onCancel }: { name: string; onCancel: () => void
);
}
-export function SwitchesSection({
- controller,
- locked,
-}: {
- controller: SwitchController;
- locked: boolean;
-}) {
+export function SwitchesSection({ controller }: { controller: SwitchController }) {
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.
@@ -387,7 +381,7 @@ export function SwitchesSection({
if (state.capture.error) setRowError({ id: target, message: state.capture.error });
setTarget(null);
}, [capturing, target, state]);
- const disabled = locked || !state?.supported || !!state.error || capturing;
+ const disabled = !state?.supported || !!state.error || capturing;
// A learned key arrives while the editor is disabled; focus the name once the
// fieldset is enabled again so a new switch can be named straight away.
useEffect(() => {
@@ -483,9 +477,7 @@ export function SwitchesSection({
? "Saving switches..."
: unsaved
? "Switch assignments have unsaved changes."
- : locked
- ? "Disable scanning to change switches."
- : "Changes save automatically."}
+ : "Changes save automatically and apply straight away."}
{(error || state?.error) && (
From de79b9e60c52f80df33e204e6771960ccda54f1a Mon Sep 17 00:00:00 2001
From: Owen McGirr
Date: Sun, 13 Sep 2026 08:52:52 +0100
Subject: [PATCH 2/2] Re-check the environment every tick instead of backing
off
An Android connection or missing input access failed inside arm() and
started the two-second retry timer, so scanning stayed off for up to
two seconds after the condition cleared. The adapter now exposes a
pure ready() check that the reconciler polls each tick; only a failed
key reservation or overlay start backs off.
Co-Authored-By: Claude Fable 5.1
---
src-tauri/src/lib.rs | 10 ++++++++--
src-tauri/src/point_scan_runtime.rs | 3 +++
src-tauri/src/scanning_runtime.rs | 9 +++++++--
3 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 929fcf2a..23d6784d 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1315,14 +1315,20 @@ async fn configure_point_scan(
.map_err(|_| "Point scan configuration was cancelled.".to_string())?
}
-fn point_scan_prepare(app: &AppHandle) -> Result<(), String> {
+/// Pure environment check, safe to call every tick while scanning is off.
+fn point_scan_ready(app: &AppHandle) -> Result<(), String> {
let state = app.state::().snapshot();
if state.bluetooth == state::BluetoothState::Connected {
- return Err("Disconnect Android before using local point scan.".into());
+ return Err("Local scanning pauses while Android is connected.".into());
}
if state.accessibility != state::AccessibilityState::Granted {
return Err("Grant input access before using point scan.".into());
}
+ Ok(())
+}
+
+fn point_scan_prepare(app: &AppHandle) -> Result<(), String> {
+ point_scan_ready(app)?;
app.state::().cancel(app);
platform_stop_mouse_repeat(app);
Ok(())
diff --git a/src-tauri/src/point_scan_runtime.rs b/src-tauri/src/point_scan_runtime.rs
index f4c61c1e..50b5a61f 100644
--- a/src-tauri/src/point_scan_runtime.rs
+++ b/src-tauri/src/point_scan_runtime.rs
@@ -40,6 +40,9 @@ impl Adapter for PointScan {
fn validate_environment(app: &AppHandle, display: Option<&Display>) -> Result<(), String> {
validate_display(app, display)
}
+ fn ready(app: &AppHandle) -> Result<(), String> {
+ crate::point_scan_ready(app)
+ }
fn prepare(app: &AppHandle) -> Result<(), String> {
crate::point_scan_prepare(app)
}
diff --git a/src-tauri/src/scanning_runtime.rs b/src-tauri/src/scanning_runtime.rs
index ee7d53ad..b779e967 100644
--- a/src-tauri/src/scanning_runtime.rs
+++ b/src-tauri/src/scanning_runtime.rs
@@ -20,6 +20,9 @@ pub trait Adapter: Send + Sync + 'static {
app: &AppHandle,
environment: Option<&Self::Environment>,
) -> Result<(), String>;
+ /// Pure check that the environment allows scanning, polled while it is off.
+ fn ready(app: &AppHandle) -> Result<(), String>;
+ /// Side effects needed right before scanning uses the desktop.
fn prepare(app: &AppHandle) -> Result<(), String>;
fn activate(
app: &AppHandle,
@@ -236,10 +239,12 @@ fn wanted(app: &AppHandle, config: &A::Config) -> Result<(), String>
}
switches
.settings
- .validate_actions(A::switches(config).automatic)
+ .validate_actions(A::switches(config).automatic)?;
+ A::ready(app)
}
/// Reserves the switch keys and readies the overlay. Leaves nothing behind on
-/// failure: the broker only enables after every key registered.
+/// failure: the broker only enables after every key registered. Only failures
+/// here start the retry backoff; environment conditions are re-read each tick.
fn arm(app: &AppHandle, config: &A::Config) -> Result<(), String> {
A::prepare(app)?;
HOST.with(|slot| {