Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Switchify PC

Switchify PC is the Rust/Tauri desktop companion for controlling Windows and macOS from the Switchify Android app. The React/TypeScript interface and Rust backend now live at the repository root. Platform adapters provide Bluetooth LE peripheral support, authenticated pairing, input injection, overlays, profiles, startup and tray behavior, diagnostics, and update checks.
Switchify PC is the Rust/Tauri desktop companion for controlling Windows and macOS from the Switchify mobile app. The React/TypeScript interface and Rust backend now live at the repository root. Platform adapters provide Bluetooth LE peripheral support, authenticated pairing, input injection, overlays, profiles, startup and tray behavior, diagnostics, and update checks.

The application uses the shipping product identity `Switchify PC` and bundle identifier `com.enaboapps.switchify.pc`. Only one application may advertise the Switchify Bluetooth service at a time.

Expand Down Expand Up @@ -29,9 +29,9 @@ The command idempotently creates a machine-local, ten-year code-signing identity

The first time, choose **Open Accessibility Settings**, enable **Switchify PC**, and return to the app. It silently updates to Ready when the window regains focus. If the row is already enabled but access remains required, select the stale row, click Remove, return to Switchify, reopen Accessibility Settings, and enable the newly added entry. The setup never resets TCC.

The signed macOS application stores Android pairing tokens in `pairing-tokens.json` in its application-data directory. The file is written atomically with user-only `0600` permissions, and its parent directory is restricted to `0700`. Windows uses its native credential store.
The signed macOS application stores mobile pairing tokens in `pairing-tokens.json` in its application-data directory. The file is written atomically with user-only `0600` permissions, and its parent directory is restricted to `0700`. Windows uses its native credential store.

The promoted identity starts with new settings, a new desktop ID, and no paired devices. Data, credentials, Accessibility approval, and certificates from earlier development builds are not migrated or removed. Recognized Switchify startup entries are migrated to the signed launcher without changing their enabled state; pair Android again after upgrading.
The promoted identity starts with new settings, a new desktop ID, and no paired devices. Data, credentials, Accessibility approval, and certificates from earlier development builds are not migrated or removed. Recognized Switchify startup entries are migrated to the signed launcher without changing their enabled state; pair your mobile device again after upgrading.

On a fresh unpaired installation, Switchify opens a five-step setup guide once. It checks Bluetooth and input access, links to Switchify on Google Play with a QR code, presents live secure-pairing approvals, and records explicit startup and anonymous-diagnostics choices. **Skip for now** dismisses the automatic prompt without marking setup complete; reopen it at any time from Home or Support. Existing paired users are never forced into the guide.

Expand Down
10 changes: 5 additions & 5 deletions docs/point-scan.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ The cursor overlay is hidden before a scan appears and stays hidden through paus

Point scan ports the Android line-only and grid-then-line techniques to Switchify PC. The reference source is `switchifyapp/switchify-android` commit `856720d8747e2f3d1724a572bf754ffae05df299`, especially `PointScanLineManager`, `PointScanBlockManager`, and `ContinuousLineSpeedUtils`.

Open **Settings → Switches** to add named keyboard switches and assign normal and hold actions. There is no on/off control: scanning is armed whenever the saved switches cover the current mode (Select for automatic scanning; Select, Next and Previous for manual) and the environment allows it. The runtime re-arms after a save, after key learning, after Escape, when an Android session ends, and retries a failed key reservation every two seconds. Fresh installs have no assignments; old point-scan keys migrate once. See [switch assignments](switches.md). Scanning arms at startup, so assigned keys are reserved from launch.
Open **Settings → Switches** to add named keyboard switches and assign normal and hold actions. There is no on/off control: scanning is armed whenever the saved switches cover the current mode (Select for automatic scanning; Select, Next and Previous for manual) and the environment allows it. The runtime re-arms after a save, after key learning, after Escape, when a mobile session ends, and retries a failed key reservation every two seconds. Fresh installs have no assignments; old point-scan keys migrate once. See [switch assignments](switches.md). Scanning arms at startup, so assigned keys are reserved from launch.

Focus the intended application and press Select to start. Line mode chooses X, then Y, and opens an action menu. Grid mode chooses a row, then a cell, before the same line sequence. Selecting happens on switch release; holding any switch freezes the position and repeat keydowns do not select again. The scan resets after a menu click or completed drag and waits for the next Select. Movement wraps at the selected region's edges. Automatic movement stops after three full passes of the current phase without a selection; the scan resets and waits for the next Select. Manual steps never trigger this limit, and each Select starts a fresh count for the next phase. Android's five speeds are 45, 75, 120, 180, and 270 logical units per second, with delayed ticks capped at 250 ms.

The scan uses the monitor under the pointer when it starts. Windows uses native physical coordinates and display scaling; macOS uses Core Graphics display units and converts overlay rectangles to AppKit coordinates. A monitor geometry change cancels scanning. Native overlay strips are topmost, click-through, and nonactivating. The pointer moves only when a menu action executes.

This first port uses switches attached to the computer. An Android connection pauses local scanning, which resumes when the connection ends. Existing authenticated Bluetooth commands and switch-forwarding profiles are unchanged. Input permission is still required on macOS. Point settings use a separate `point-scan.json` in the existing Tauri application configuration directory, so old settings and pairing schemas remain unchanged.
This first port uses switches attached to the computer. A mobile connection pauses local scanning, which resumes when the connection ends. Existing authenticated Bluetooth commands and switch-forwarding profiles are unchanged. Input permission is still required on macOS. Point settings use a separate `point-scan.json` in the existing Tauri application configuration directory, so old settings and pairing schemas remain unchanged.

Changes save automatically, including when you leave Settings, and apply straight away: saving pauses scanning and the runtime re-arms it with the new settings. A failed save keeps your edits and offers Retry save. Navigating away does not stop scanning.

Expand All @@ -34,7 +34,7 @@ Grid rows and cells use the shared pure Rust `scan_tree` navigator. After the la

The navigator supports nested branches and typed leaves for future item scanning. It removes empty branches and collapses single-child branches. Root traversal wraps without an escape slot. Grid leaf selection still begins the existing X/Y precision scan. No desktop accessibility discovery is included.

The shared frame carries optional label metadata, rendered by a separate nonactivating, click-through native host on the scan display. The technique label stays visible while a switch is held, until a hold-action prompt takes priority. Reset, Escape, configuration changes, Android connection and shutdown clear it through session cleanup. The desktop view adds the `rowEscape` phase; saved settings and Bluetooth interfaces are unchanged.
The shared frame carries optional label metadata, rendered by a separate nonactivating, click-through native host on the scan display. The technique label stays visible while a switch is held, until a hold-action prompt takes priority. Reset, Escape, configuration changes, mobile connection and shutdown clear it through session cleanup. The desktop view adds the `rowEscape` phase; saved settings and Bluetooth interfaces are unchanged.

Physical validation should include forward/reverse escape with a real switch, pause/hold behavior, the final automatic cycle, mixed-DPI label placement, focus retention, disconnect and exit on both Windows and macOS. Automated tests use fake input only.

Expand All @@ -48,13 +48,13 @@ Drag retains the source and scans a destination on the same display without hold

`point_workflow` separates point selection, menu navigation and typed action requests. Its current selection policy always opens the menu; a future auto-select policy can reuse `default_click` without changing the executor. No auto-select timer or setting exists. The reusable `scan_menu` defines stable action IDs and rows. Shared frames carry menu tiles, labels and highlight strips; all native windows remain click-through and nonactivating.

The desktop view adds menu, menuSuspended, dragDestination, dragConfirmation and executing phases. Existing point phases, saved settings and Bluetooth commands retain their shapes. Foreground changes, display changes, Android connections, switch editing/learning and shutdown cancel the workflow. Targets and foreground identity remain in memory and are never logged or emitted. Automated action tests use fake input only; physical Windows/macOS focus, scaling, target, scrolling and drag checks remain required for hardware qualification.
The desktop view adds menu, menuSuspended, dragDestination, dragConfirmation and executing phases. Existing point phases, saved settings and Bluetooth commands retain their shapes. Foreground changes, display changes, mobile connections, switch editing/learning and shutdown cancel the workflow. Targets and foreground identity remain in memory and are never logged or emitted. Automated action tests use fake input only; physical Windows/macOS focus, scaling, target, scrolling and drag checks remain required for hardware qualification.

The action menu uses a fixed grid of square icon tiles with labels beneath the artwork, on the same dark rounded panel chrome as the scanning keyboard (`TileRole::Panel`). A yellow border and amber background identify the current row or item. The same artwork is drawn on Windows and macOS. Menu titles stay above the shared panel when a scan opens a menu or changes submenus, without taking keyboard focus.

## Remote scanning

Select **Switchify scanning** on Switchify Remote's Android Forwarding screen. Remote switches live in the same list as keyboard switches under PC Settings → Switches: Add remote switch takes the next free number, and the number can be changed in the editor. Numbers match the switches shown on the Forwarding screen, up to eight. Defaults are Select, Next, Previous, Pause/resume, Reverse and Stop in switches one to six. Remote switches can be named and use the PC scan mode, colour, movement and hold timing; keyboard assignments are separate. Manual mode requires connected switches covering Select, Next and Previous.
Select **Switchify scanning** on Switchify Remote's mobile Forwarding screen. Remote switches live in the same list as keyboard switches under PC Settings → Switches: Add remote switch takes the next free number, and the number can be changed in the editor. Numbers match the switches shown on the Forwarding screen, up to eight. Defaults are Select, Next, Previous, Pause/resume, Reverse and Stop in switches one to six. Remote switches can be named and use the PC scan mode, colour, movement and hold timing; keyboard assignments are separate. Manual mode requires connected switches covering Select, Next and Previous.

Start forwarding, then press Select to begin. Local switch keys are inactive during the remote session; PC Escape remains an emergency stop. The PC owns hold-action timing: hold actions work exactly as for keyboard switches, and holding a remote switch past the PC emergency limit resets the scan without ending the session. Remote's own Forwarding hold-to-stop (3, 5 or 8 seconds, set on the phone) still ends the session without selecting, so keep hold lists short enough to finish inside it. Edits to remote switches, shared scanner settings or hold timing apply to a live session on the next press; a change that leaves the current mode without its required actions ends the session. The PC no longer rejects a stale profile revision, so Remote only needs to reload profiles to refresh its labels.

Expand Down
2 changes: 1 addition & 1 deletion docs/qwerty-keyboard.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# QWERTY scanning keyboard

Switchify has its own UK English keyboard on Windows and macOS. It uses the saved local switches or the existing Android remote scanning actions. It does not use the operating system's on-screen keyboard.
Switchify has its own UK English keyboard on Windows and macOS. It uses the saved local switches or the existing mobile remote scanning actions. It does not use the operating system's on-screen keyboard.

Select **Keyboard** in the action menu, or assign **Open keyboard** to a switch press or hold action in Settings → Switches. The switch opens the keyboard from idle, point scanning, menus, pauses and drags. Opening releases owned input and preserves the existing foreground focus without clicking. The keyboard does not take focus or accept mouse clicks.

Expand Down
2 changes: 1 addition & 1 deletion docs/switches.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The normal action runs on release. Hold actions are offered in their configured

Stop resets the scan and leaves the keys reserved. Select starts again. Pause preserves position. Reverse changes direction without stepping. Automatic scanning requires Select; manual scanning also requires Next and Previous. Hold assignments count toward those requirements.

Escape and the emergency hold reset the scan and release the keys; the runtime re-arms them on its next tick. The emergency hold timeout is at least 4 seconds and extends to (longest hold list + 2) × interval when hold actions exist. Settings shows the resulting duration. Heartbeat loss, capture failure, overflow, Android connection and shutdown cancel gestures without executing release actions.
Escape and the emergency hold reset the scan and release the keys; the runtime re-arms them on its next tick. The emergency hold timeout is at least 4 seconds and extends to (longest hold list + 2) × interval when hold actions exist. Settings shows the resulting duration. Heartbeat loss, capture failure, overflow, mobile connection and shutdown cancel gestures without executing release actions.

## Storage and integration

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "switchify-pc"
version = "1.0.0-rc.14"
description = "Switchify desktop companion for Android control"
description = "Switchify desktop companion for mobile control"
authors = ["Enabo Apps"]
edition = "2021"
rust-version = "1.97.1"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Switchify uses Bluetooth to receive authenticated control commands from your Android device.</string>
<string>Switchify uses Bluetooth to receive authenticated control commands from your mobile device.</string>
</dict>
</plist>
26 changes: 13 additions & 13 deletions src-tauri/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ pub struct DesktopCommandOutcome {
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AndroidTypingRoute {
pub struct MobileTypingRoute {
eligible: bool,
}

impl AndroidTypingRoute {
impl MobileTypingRoute {
pub fn for_text(text: &str) -> Self {
Self {
eligible: !text.is_empty(),
Expand Down Expand Up @@ -1991,7 +1991,7 @@ mod tests {
}

#[test]
fn android_typing_commands_report_only_actual_keyboard_injection() {
fn mobile_typing_commands_report_only_actual_keyboard_injection() {
let mut input = DesktopInput::new(FakeInjector::default());

for (command_type, payload) in [
Expand Down Expand Up @@ -2068,7 +2068,7 @@ mod tests {
}

#[test]
fn failed_android_typing_does_not_report_injection() {
fn failed_mobile_typing_does_not_report_injection() {
let mut input = DesktopInput::new(FakeInjector {
fail_key_down: true,
..FakeInjector::default()
Expand All @@ -2094,7 +2094,7 @@ mod tests {
"keyboard.textStream.key",
] {
assert!(
AndroidTypingRoute::for_command(command_type).is_eligible(),
MobileTypingRoute::for_command(command_type).is_eligible(),
"{command_type}"
);
}
Expand All @@ -2107,18 +2107,18 @@ mod tests {
"window.control",
] {
assert!(
!AndroidTypingRoute::for_command(command_type).is_eligible(),
!MobileTypingRoute::for_command(command_type).is_eligible(),
"{command_type}"
);
}
assert!(AndroidTypingRoute::for_text("a").is_eligible());
assert!(!AndroidTypingRoute::for_text("").is_eligible());
assert!(MobileTypingRoute::for_text("a").is_eligible());
assert!(!MobileTypingRoute::for_text("").is_eligible());
}

#[test]
fn typing_route_orders_prepare_and_successful_hide_effects() {
let events = Mutex::new(Vec::new());
let route = AndroidTypingRoute::for_text("Hello");
let route = MobileTypingRoute::for_text("Hello");
route.prepare(
|| events.lock().unwrap().push("cancel dwell"),
|| events.lock().unwrap().push("stop repeats"),
Expand All @@ -2130,13 +2130,13 @@ mod tests {
);

let events = Mutex::new(Vec::new());
AndroidTypingRoute::for_text("").prepare(
MobileTypingRoute::for_text("").prepare(
|| events.lock().unwrap().push("cancel dwell"),
|| events.lock().unwrap().push("stop repeats"),
);
AndroidTypingRoute::for_text("")
MobileTypingRoute::for_text("")
.finish(true, || events.lock().unwrap().push("hide overlay"));
AndroidTypingRoute::for_text("Hello")
MobileTypingRoute::for_text("Hello")
.finish(false, || events.lock().unwrap().push("hide overlay"));
assert!(events.lock().unwrap().is_empty());
}
Expand Down Expand Up @@ -2268,7 +2268,7 @@ mod tests {
}

#[test]
fn android_pointer_speed_changes_persist_and_restore() {
fn mobile_pointer_speed_changes_persist_and_restore() {
let root =
std::env::temp_dir().join(format!("switchify-pointer-speed-{}", uuid::Uuid::new_v4()));
let state_path = root.join("state.json");
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1423,7 +1423,7 @@ fn point_scan_ready(app: &AppHandle) -> Result<(), String> {
}
let state = app.state::<AppModel>().snapshot();
if state.bluetooth == state::BluetoothState::Connected && !remote_scan::active(app) {
return Err("Local scanning pauses while Android is connected.".into());
return Err("Local scanning pauses while a mobile device is connected.".into());
}
if state.accessibility != state::AccessibilityState::Granted {
return Err("Grant input access before using point scan.".into());
Expand Down
Loading
Loading