diff --git a/README.md b/README.md index e251746e..11c04192 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/docs/point-scan.md b/docs/point-scan.md index 9c7dbe2f..3acec488 100644 --- a/docs/point-scan.md +++ b/docs/point-scan.md @@ -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. @@ -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. @@ -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. diff --git a/docs/qwerty-keyboard.md b/docs/qwerty-keyboard.md index 02f61e12..61833877 100644 --- a/docs/qwerty-keyboard.md +++ b/docs/qwerty-keyboard.md @@ -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. diff --git a/docs/switches.md b/docs/switches.md index 713eb9ab..c763083f 100644 --- a/docs/switches.md +++ b/docs/switches.md @@ -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 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4dd3cf16..28cb383f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist index 1285e1eb..bd98a256 100644 --- a/src-tauri/Info.plist +++ b/src-tauri/Info.plist @@ -3,6 +3,6 @@ NSBluetoothAlwaysUsageDescription - Switchify uses Bluetooth to receive authenticated control commands from your Android device. + Switchify uses Bluetooth to receive authenticated control commands from your mobile device. diff --git a/src-tauri/src/input.rs b/src-tauri/src/input.rs index 3df76983..4b2a1471 100644 --- a/src-tauri/src/input.rs +++ b/src-tauri/src/input.rs @@ -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(), @@ -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 [ @@ -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() @@ -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}" ); } @@ -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"), @@ -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()); } @@ -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"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ebadb099..90f5f26f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1423,7 +1423,7 @@ fn point_scan_ready(app: &AppHandle) -> Result<(), String> { } let state = app.state::().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()); diff --git a/src-tauri/src/macos.rs b/src-tauri/src/macos.rs index d981e0e7..d9a077b1 100644 --- a/src-tauri/src/macos.rs +++ b/src-tauri/src/macos.rs @@ -20,8 +20,8 @@ use crate::ble_lifecycle::{RecoveryCoordinator, RECOVERY_DELAYS}; use crate::display_navigation::{self, NavigationError}; use crate::dwell::DwellController; use crate::input::{ - execute_desktop_command, execute_dwell_click, AndroidTypingRoute, DesktopCommandOutcome, - DesktopInput, PointerFeedback, + execute_desktop_command, execute_dwell_click, DesktopCommandOutcome, DesktopInput, + MobileTypingRoute, PointerFeedback, }; use crate::modifier_overlay::ModifierOverlay; use crate::mouse_repeat::{MouseRepeatController, RepeatCommand, MOVE_TICK_INTERVAL_MS}; @@ -891,7 +891,7 @@ impl MacRuntime { set_activity( &self.shared, ActivityKind::Info, - "Advertising to nearby Switchify Android devices.", + "Advertising to nearby Switchify mobile devices.", ); } else { self.reset_gatt(); @@ -920,7 +920,7 @@ impl MacRuntime { &self.shared, ActivityKind::Info, format!( - "Android connected. Notification limit: {maximum_notification_bytes} bytes." + "Mobile device connected. Notification limit: {maximum_notification_bytes} bytes." ), ); self.flush_outbound()?; @@ -1227,7 +1227,7 @@ impl MacRuntime { } fn handle_text(&mut self, command: TextCommand) { - let typing_route = AndroidTypingRoute::for_text(&command.text); + let typing_route = MobileTypingRoute::for_text(&command.text); let app = self.app.clone(); typing_route.prepare( || app.state::().cancel(&app), @@ -1337,7 +1337,7 @@ impl MacRuntime { ) { self.app.state::().cancel(&self.app); } - let typing_route = AndroidTypingRoute::for_command(&command.command_type); + let typing_route = MobileTypingRoute::for_command(&command.command_type); let profiles = self .shared .lock() @@ -2001,7 +2001,7 @@ impl MacRuntime { .values() .copied() .min() - .ok_or_else(|| "No Android device is subscribed for notifications.".to_string())?; + .ok_or_else(|| "No mobile device is subscribed for notifications.".to_string())?; self.outbound.push_notification_message( message, MAX_QUEUED_NOTIFICATIONS, @@ -2241,7 +2241,7 @@ mod tests { #[test] fn mac_typing_route_orders_cleanup_before_successful_overlay_hiding() { 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"), @@ -2253,7 +2253,7 @@ mod tests { ); let events = Mutex::new(Vec::new()); - let failed = AndroidTypingRoute::for_text("Hello"); + let failed = MobileTypingRoute::for_text("Hello"); failed.prepare( || events.lock().unwrap().push("cancel dwell"), || events.lock().unwrap().push("stop repeats"), @@ -2262,7 +2262,7 @@ mod tests { assert_eq!(*events.lock().unwrap(), ["cancel dwell", "stop repeats"]); let events = Mutex::new(Vec::new()); - let empty = AndroidTypingRoute::for_text(""); + let empty = MobileTypingRoute::for_text(""); empty.prepare( || events.lock().unwrap().push("cancel dwell"), || events.lock().unwrap().push("stop repeats"), diff --git a/src-tauri/src/point_scan.rs b/src-tauri/src/point_scan.rs index 9acc5da4..9a119e61 100644 --- a/src-tauri/src/point_scan.rs +++ b/src-tauri/src/point_scan.rs @@ -1,4 +1,4 @@ -//! Android point scanning, with desktop coordinates and no OS input in the engine. +//! Mobile point scanning, with desktop coordinates and no OS input in the engine. use crate::scan_tree::{Navigator, Node, Selection}; use crate::scanning::{ Action, Frame, FrameLabel, Interval, Rect, SwitchSettings, Technique, TICK_MS, diff --git a/src-tauri/src/scanning_runtime.rs b/src-tauri/src/scanning_runtime.rs index 2c79d491..c2041730 100644 --- a/src-tauri/src/scanning_runtime.rs +++ b/src-tauri/src/scanning_runtime.rs @@ -59,7 +59,7 @@ thread_local! {static COUNTDOWN: RefCell<(Option, Option { @@ -784,7 +784,7 @@ fn tick(app: &AppHandle) { == crate::state::BluetoothState::Connected && !crate::remote_scan::active(app) { - disable::(app, "Android connected. Local scanning stopped."); + disable::(app, "Mobile device connected. Local scanning stopped."); return; } let result = (|| -> Result<(), String> { diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index f4a3cbd9..3d8fdf6a 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -411,7 +411,7 @@ impl AppModel { discarded_legacy_pairing.then(|| Activity { kind: ActivityKind::Info, message: - "Pair Android again once to finish the secure-storage upgrade." + "Pair your mobile device again once to finish the secure-storage upgrade." .into(), }) }), diff --git a/src-tauri/src/switch_gestures.rs b/src-tauri/src/switch_gestures.rs index c708f685..bd7c7879 100644 --- a/src-tauri/src/switch_gestures.rs +++ b/src-tauri/src/switch_gestures.rs @@ -1,4 +1,4 @@ -//! Pure Android-style release/hold selection. Cancellation never becomes an action. +//! Pure Mobile-style release/hold selection. Cancellation never becomes an action. use crate::{ scanning::Action, switches::{Binding, Settings}, @@ -168,7 +168,7 @@ mod tests { assert_eq!(gestures.released("two", 30), None); } #[test] - fn boundary_and_last_action_match_android() { + fn boundary_and_last_action_match_mobile() { for (duration, expected) in [ (999, Action::Select), (1000, Action::Next), diff --git a/src-tauri/src/switch_practice.rs b/src-tauri/src/switch_practice.rs index 6d72fd24..474ccf11 100644 --- a/src-tauri/src/switch_practice.rs +++ b/src-tauri/src/switch_practice.rs @@ -277,7 +277,7 @@ pub fn begin(app: &AppHandle, remote: bool) -> Result { } if remote && !crate::remote_scan::active(app) { return Err( - "No remote forwarding session. Connect Android and start switch forwarding first." + "No remote forwarding session. Connect your mobile device and start switch forwarding first." .into(), ); } @@ -287,7 +287,7 @@ pub fn begin(app: &AppHandle, remote: bool) -> Result { let (settings, generation) = if remote { let Some((g, s, _, _)) = remote_state else { return Err( - "No remote forwarding session. Connect Android and start switch forwarding first." + "No remote forwarding session. Connect your mobile device and start switch forwarding first." .into(), ); }; diff --git a/src-tauri/src/windows_runtime.rs b/src-tauri/src/windows_runtime.rs index fd34f7f0..494385cf 100644 --- a/src-tauri/src/windows_runtime.rs +++ b/src-tauri/src/windows_runtime.rs @@ -24,8 +24,8 @@ use crate::ble_lifecycle::{RecoveryCoordinator, RECOVERY_DELAYS}; use crate::display_navigation::{self, NavigationError}; use crate::dwell::DwellController; use crate::input::{ - execute_desktop_command, execute_dwell_click, AndroidTypingRoute, DesktopCommandOutcome, - DesktopInput, PointerFeedback, + execute_desktop_command, execute_dwell_click, DesktopCommandOutcome, DesktopInput, + MobileTypingRoute, PointerFeedback, }; use crate::modifier_overlay::ModifierOverlay; use crate::mouse_repeat::{MouseRepeatController, RepeatCommand, MOVE_TICK_INTERVAL_MS}; @@ -155,7 +155,7 @@ impl NotificationDispatcher { .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); if !state.subscribed { - return Err("No Android device is subscribed for notifications.".into()); + return Err("No mobile device is subscribed for notifications.".into()); } (state.generation, state.maximum_encoded_bytes) }; @@ -179,7 +179,7 @@ impl NotificationDispatcher { .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); if !state.subscribed { - return Err("No Android device is subscribed for notifications.".into()); + return Err("No mobile device is subscribed for notifications.".into()); } if expected_delivery.is_some_and(|(generation, maximum_encoded_bytes)| { state.generation != generation || state.maximum_encoded_bytes != maximum_encoded_bytes @@ -937,9 +937,9 @@ async fn start_gatt( if cancelled > 0 { "Pairing request cancelled." } else if connected { - "Android device connected." + "Mobile device connected." } else { - "Android device disconnected." + "Mobile device disconnected." }, ); if !connected { @@ -1263,7 +1263,7 @@ fn update_advertisement_status( ( BluetoothState::Advertising, ActivityKind::Info, - "Advertising to nearby Switchify Android devices.".to_string(), + "Advertising to nearby Switchify mobile devices.".to_string(), ) } else if status == GattServiceProviderAdvertisementStatus::StartedWithoutAllAdvertisementData { ( @@ -1560,7 +1560,7 @@ fn complete_mouse_click( ) } fn complete_text(app: &AppHandle, shared: &SharedModel, command: TextCommand) -> Option { - let typing_route = AndroidTypingRoute::for_text(&command.text); + let typing_route = MobileTypingRoute::for_text(&command.text); typing_route.prepare( || app.state::().cancel(app), || stop_all_repeats(app), @@ -1636,7 +1636,7 @@ fn complete_desktop( ) { app.state::().cancel(app); } - let typing_route = AndroidTypingRoute::for_command(&command.command_type); + let typing_route = MobileTypingRoute::for_command(&command.command_type); let profiles = shared .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) @@ -2341,7 +2341,7 @@ mod tests { NOTIFICATION_BYTES, NOTIFICATION_DELIVERY_ERROR, }; use crate::display_navigation::Display; - use crate::input::AndroidTypingRoute; + use crate::input::MobileTypingRoute; use crate::protocol::{ create_notification_frames, pointer_profile_response, BluetoothFrame, EngineEvent, FrameReassembler, PointerProfile, ProtocolEngine, @@ -2398,7 +2398,7 @@ mod tests { #[test] fn windows_typing_route_orders_cleanup_before_successful_overlay_hiding() { 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"), @@ -2410,7 +2410,7 @@ mod tests { ); let events = Mutex::new(Vec::new()); - let failed = AndroidTypingRoute::for_text("Hello"); + let failed = MobileTypingRoute::for_text("Hello"); failed.prepare( || events.lock().unwrap().push("cancel dwell"), || events.lock().unwrap().push("stop repeats"), @@ -2419,7 +2419,7 @@ mod tests { assert_eq!(*events.lock().unwrap(), ["cancel dwell", "stop repeats"]); let events = Mutex::new(Vec::new()); - let empty = AndroidTypingRoute::for_text(""); + let empty = MobileTypingRoute::for_text(""); empty.prepare( || events.lock().unwrap().push("cancel dwell"), || events.lock().unwrap().push("stop repeats"), @@ -2547,7 +2547,7 @@ mod tests { let dispatcher = NotificationDispatcher::default(); assert_eq!( dispatcher.enqueue("response"), - Err("No Android device is subscribed for notifications.".into()) + Err("No mobile device is subscribed for notifications.".into()) ); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7e63f5a2..914e6f1e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -44,8 +44,8 @@ "icons/icon.icns", "icons/icon.ico" ], - "shortDescription": "Android control for Windows and macOS", - "longDescription": "Cross-platform Switchify desktop companion for Android control.", + "shortDescription": "Mobile control for Windows and macOS", + "longDescription": "Cross-platform Switchify desktop companion for mobile control.", "windows": { "nsis": { "installMode": "perMachine" } }, diff --git a/src/App.test.tsx b/src/App.test.tsx index 90e6671c..61374cff 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -34,7 +34,7 @@ describe("Switchify PC shell", () => { return { settings, state: { settings, capture: { active: false, key: null, error: null }, supported: true, error: null, escapeHoldMs: 4000, unavailableKeys: [] }, pending: 0, error: null, capturing: false, unsaved: false, update: vi.fn(), remove: vi.fn(), capture: vi.fn(), cancelCapture: vi.fn().mockResolvedValue(undefined), setKeyboardEntry: vi.fn().mockResolvedValue(true), retry: vi.fn() }; } - it("shows local readiness without Bluetooth and explains Android suspension", async () => { + it("shows local readiness without Bluetooth and explains Mobile suspension", async () => { browserState.accessibility = "granted"; browserState.bluetooth = "unsupported"; vi.spyOn(switchHooks, "useSwitches").mockReturnValue(localSwitches()); @@ -48,7 +48,7 @@ describe("Switchify PC shell", () => { expect(screen.getByText("Manual scanning · Line only")).toBeInTheDocument(); scanning.state = { ...scanning.state!, paused: true }; act(() => receive?.({ ...structuredClone(browserState), bluetooth: "connected" })); - expect(screen.getByText("Local scanning is paused while an Android device is connected.")).toBeInTheDocument(); + expect(screen.getByText("Local scanning is paused while a mobile device is connected.")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Disconnect" })).toBeInTheDocument(); }); @@ -181,17 +181,17 @@ describe("Switchify PC shell", () => { expect(within(footer as HTMLElement).queryByRole("button")).not.toBeInTheDocument(); }); - it("distinguishes the Android install QR from pairing for existing users", async () => { + it("distinguishes the Mobile install QR from pairing for existing users", async () => { render(); await screen.findByRole("heading", { name: "Switchify PC" }); - fireEvent.click(screen.getByRole("button", { name: "Android connection" })); - expect(screen.getByRole("heading", { name: "Install Switchify for Android" })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Mobile connection" })); + expect(screen.getByRole("heading", { name: "Install Switchify for mobile" })).toBeInTheDocument(); const qr = screen.getByRole("img", { name: "QR code for Switchify on Google Play" }); expect(qr.closest("figure")).toHaveTextContent("Scan to open Google Play and install Switchify."); expect(screen.getByRole("link", { name: "Open Google Play" })).toHaveAttribute("href", expect.stringContaining("play.google.com")); const pairing = screen.getByRole("heading", { name: "Already installed? Connect to this computer" }).closest("section")!; expect(within(pairing).getAllByRole("listitem").map((item) => item.textContent)).toEqual([ - "Open Switchify on your Android device and select this computer.", + "Open Switchify on your mobile device and select this computer.", "Compare the pairing codes shown in both apps.", "Approve the pairing request on this computer only if the codes match.", ]); @@ -206,7 +206,7 @@ describe("Switchify PC shell", () => { render(); await screen.findByRole("heading", { name: "Switchify PC" }); - fireEvent.click(screen.getByRole("button", { name: "Android connection" })); + fireEvent.click(screen.getByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Paired devices" })); expect(screen.getByText(`Last connected ${new Date(connectedAt).toLocaleString()}`)).toBeInTheDocument(); @@ -239,15 +239,15 @@ describe("Switchify PC shell", () => { it.each([ ["initializing", "Stale tablet", "Preparing this computer for nearby devices."], - ["advertising", "Stale tablet", "Waiting for a nearby Android device."], - ["connected", null, "Android device connected."], + ["advertising", "Stale tablet", "Waiting for a nearby mobile device."], + ["connected", null, "Mobile device connected."], ["connected", "Pixel Tablet", "Pixel Tablet"], - ["poweredOff", "Stale tablet", "Turn on Bluetooth to connect an Android device."], + ["poweredOff", "Stale tablet", "Turn on Bluetooth to connect a mobile device."], ["unauthorized", "Stale tablet", "Allow Bluetooth access in System Settings to connect."], ["conflict", "Stale tablet", "Quit the other Switchify PC instance, then reopen this app."], ["unsupported", "Stale tablet", "This computer does not support the required Bluetooth features."], ["error", "Stale tablet", "Bluetooth could not start. Try restarting Switchify PC."], - ] satisfies Array<[BluetoothState, string | null, string]>)('uses recovery-appropriate Android connection copy for Bluetooth state "%s"', async (bluetooth, deviceName, description) => { + ] satisfies Array<[BluetoothState, string | null, string]>)('uses recovery-appropriate Mobile connection copy for Bluetooth state "%s"', async (bluetooth, deviceName, description) => { browserState.bluetooth = bluetooth; browserState.connectedDeviceName = deviceName; browserState.lastActivity = { kind: "error", message: "Internal runtime activity" }; @@ -255,7 +255,7 @@ describe("Switchify PC shell", () => { render(); await screen.findByRole("heading", { name: "Switchify PC" }); - fireEvent.click(screen.getByRole("button", { name: "Android connection" })); + fireEvent.click(screen.getByRole("button", { name: "Mobile connection" })); expect(screen.getByText(description)).toBeInTheDocument(); if (bluetooth !== "connected") expect(screen.queryByText("Stale tablet")).not.toBeInTheDocument(); expect(screen.queryByText("Internal runtime activity")).not.toBeInTheDocument(); @@ -385,7 +385,7 @@ describe("Switchify PC shell", () => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); - it("completes switch setup without Android or Bluetooth and requires explicit choices", async () => { + it("completes switch setup without Mobile or Bluetooth and requires explicit choices", async () => { browserState.pairedDevices = []; browserState.bluetooth = "unsupported"; const complete = vi.spyOn(api, "completeSetup").mockImplementation(async (startWithSystem, shareDiagnostics) => ({ @@ -490,7 +490,7 @@ describe("Switchify PC shell", () => { return [profile]; }); render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); const newProfile = await screen.findByRole("button", { name: "New profile" }); expect(newProfile).toHaveClass("primary"); @@ -511,7 +511,7 @@ describe("Switchify PC shell", () => { }; vi.spyOn(api, "listProfiles").mockResolvedValue([source, { ...source, id: crypto.randomUUID(), name: "Generic keyboard copy", builtIn: false }]); render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); fireEvent.click(await screen.findByRole("button", { name: /Generic keyboard.*Built in/ })); expect(screen.getByRole("textbox", { name: "Profile name" })).toBeDisabled(); @@ -526,7 +526,7 @@ describe("Switchify PC shell", () => { it("identifies duplicate names and bindings at their fields", async () => { render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); fireEvent.click(await screen.findByRole("button", { name: "New profile" })); fireEvent.change(screen.getByRole("textbox", { name: "Profile name" }), { target: { value: "Generic keyboard" } }); @@ -545,7 +545,7 @@ describe("Switchify PC shell", () => { it("confirms dirty editor dismissal and restores focus", async () => { render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); const opener = await screen.findByRole("button", { name: "New profile" }); fireEvent.click(opener); @@ -560,7 +560,7 @@ describe("Switchify PC shell", () => { it("closes pristine new and duplicated profiles without a discard prompt", async () => { render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); const newProfile = await screen.findByRole("button", { name: "New profile" }); fireEvent.click(newProfile); @@ -578,7 +578,7 @@ describe("Switchify PC shell", () => { it("prevents navigation from discarding a modified profile", async () => { const confirm = vi.spyOn(window, "confirm").mockReturnValue(false); render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); fireEvent.click(await screen.findByRole("button", { name: "New profile" })); fireEvent.change(screen.getByRole("textbox", { name: "Profile name" }), { target: { value: "Modified controls" } }); @@ -601,7 +601,7 @@ describe("Switchify PC shell", () => { return () => undefined; }); render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); fireEvent.click(await screen.findByRole("button", { name: "New profile" })); fireEvent.change(screen.getByRole("textbox", { name: "Profile name" }), { target: { value: "Unsaved controls" } }); @@ -627,7 +627,7 @@ describe("Switchify PC shell", () => { vi.spyOn(api, "listProfiles").mockResolvedValue([custom]); vi.spyOn(api, "deleteProfile").mockResolvedValue([]); render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); fireEvent.click(await screen.findByRole("button", { name: /Scanning controls.*Custom/ })); fireEvent.change(screen.getByRole("textbox", { name: "Profile name" }), { target: { value: "Changed controls" } }); @@ -642,7 +642,7 @@ describe("Switchify PC shell", () => { it("keeps the editor open and focused when saving fails", async () => { vi.spyOn(api, "saveProfile").mockRejectedValue(new Error("Profile storage unavailable")); render(); - fireEvent.click(await screen.findByRole("button", { name: "Android connection" })); + fireEvent.click(await screen.findByRole("button", { name: "Mobile connection" })); fireEvent.click(screen.getByRole("tab", { name: "Switch Forwarding" })); fireEvent.click(await screen.findByRole("button", { name: "New profile" })); fireEvent.click(screen.getByRole("button", { name: "Save profile" })); diff --git a/src/App.tsx b/src/App.tsx index e4f5b8ce..1f96ddb0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,11 +17,11 @@ import { areaOptions } from "./scanning/preferences"; import { SwitchesSection } from "./settings/SwitchesSection"; import { ScanningSection } from "./settings/ScanningSection"; -type View = "switches" | "scanning" | "android" | "home" | "devices" | "profiles" | "settings" | "support"; +type View = "switches" | "scanning" | "mobile" | "home" | "devices" | "profiles" | "settings" | "support"; const brandIconUrl = new URL("../src-tauri/icons/icon.png", import.meta.url).href; -const androidQrUrl = new URL("./assets/android-download-qr.png", import.meta.url).href; -const androidDownloadUrl = "https://play.google.com/store/apps/details?id=com.enaboapps.switchify"; +const mobileQrUrl = new URL("./assets/mobile-download-qr.png", import.meta.url).href; +const mobileDownloadUrl = "https://play.google.com/store/apps/details?id=com.enaboapps.switchify"; const bluetoothLabels: Record = { initializing: "Starting Bluetooth...", advertising: "Ready to connect", connected: "Device connected", @@ -31,9 +31,9 @@ const bluetoothLabels: Record = { const bluetoothDescriptions: Record = { initializing: "Preparing this computer for nearby devices.", - advertising: "Waiting for a nearby Android device.", - connected: "Android device connected.", - poweredOff: "Turn on Bluetooth to connect an Android device.", + advertising: "Waiting for a nearby mobile device.", + connected: "Mobile device connected.", + poweredOff: "Turn on Bluetooth to connect a mobile device.", unauthorized: "Allow Bluetooth access in System Settings to connect.", conflict: "Quit the other Switchify PC instance, then reopen this app.", unsupported: "This computer does not support the required Bluetooth features.", @@ -62,7 +62,7 @@ function HomeView({ state, switches, scanning, navigate, onDisconnect, onAccessi const saved = switches.state?.settings.bindings ?? []; const hasSelect = saved.some((binding) => binding.pressAction === "select" || binding.holdActions.includes("select")); const ready = state.accessibility === "granted" && hasSelect && !switches.error && !scanning.error && state.bluetooth !== "connected" && scanning.state?.supported && scanning.state.enabled && !scanning.state.paused; - const message = switches.error ?? scanning.error ?? (state.accessibility !== "granted" ? "Allow input access to control this computer." : !scanning.state ? "Loading switch control..." : !scanning.state.supported ? scanning.state.message : scanning.state.remote ? "Remote controls scanning. Use the switches in Remote; PC Escape stops the session." : state.bluetooth === "connected" ? "Local scanning is paused while an Android device is connected." : !hasSelect ? "Add a switch with the Select action to begin scanning." : scanning.state.paused ? "Scanning is paused. Use your Pause / resume switch to continue." : scanning.state.enabled ? "Focus the application you want to use, then press and release your Select switch." : scanning.state.message); + const message = switches.error ?? scanning.error ?? (state.accessibility !== "granted" ? "Allow input access to control this computer." : !scanning.state ? "Loading switch control..." : !scanning.state.supported ? scanning.state.message : scanning.state.remote ? "Remote controls scanning. Use the switches in Remote; PC Escape stops the session." : state.bluetooth === "connected" ? "Local scanning is paused while a mobile device is connected." : !hasSelect ? "Add a switch with the Select action to begin scanning." : scanning.state.paused ? "Scanning is paused. Use your Pause / resume switch to continue." : scanning.state.enabled ? "Focus the application you want to use, then press and release your Select switch." : scanning.state.message); return

Switchify PC

Control your computer with switches.

@@ -75,29 +75,29 @@ function HomeView({ state, switches, scanning, navigate, onDisconnect, onAccessi

Configured switches

{saved.length} saved · {hasSelect ? "Select assigned" : "Select action needed"}

Scanning

{areaOptions(scanning.config, "point").automatic ? "Automatic scanning" : "Manual scanning"} · {scanning.config.mode === "line" ? "Line only" : "Grid then line"}

-

Android connection

{state.bluetooth === "connected" ? state.connectedDeviceName ?? bluetoothLabels.connected : bluetoothLabels[state.bluetooth]}

{state.bluetooth === "connected" && }
+

Mobile connection

{state.bluetooth === "connected" ? state.connectedDeviceName ?? bluetoothLabels.connected : bluetoothLabels[state.bluetooth]}

{state.bluetooth === "connected" && }
; } -function AndroidConnection({ state, onDisconnect }: { state: AppState; onDisconnect: () => void }) { +function MobileConnection({ state, onDisconnect }: { state: AppState; onDisconnect: () => void }) { const bluetoothOk = state.bluetooth === "advertising" || state.bluetooth === "connected"; return

{bluetoothLabels[state.bluetooth]}

{state.bluetooth === "connected" ? state.connectedDeviceName ?? bluetoothDescriptions.connected : bluetoothDescriptions[state.bluetooth]}

{state.bluetooth === "connected" && }
-

Android connection is optional. Bluetooth advertises automatically; local switches do not need an Android device. Connecting an Android device pauses local scanning until it disconnects.

-
- -
QR code for Switchify on Google Play
Scan to open Google Play and install Switchify.
+

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.

+
+

Install Switchify for mobile

Need the mobile app? Scan the QR code or open Google Play to install Switchify.

Open Google Play
+
QR code for Switchify on Google Play
Scan to open Google Play and install Switchify.
-

Already installed? Connect to this computer

  1. Open Switchify on your Android device and select this computer.
  2. Compare the pairing codes shown in both apps.
  3. Approve the pairing request on this computer only if the codes match.
+

Already installed? Connect to this computer

  1. Open Switchify on your mobile device and select this computer.
  2. Compare the pairing codes shown in both apps.
  3. Approve the pairing request on this computer only if the codes match.
; } function DevicesView({ state, forget }: { state: AppState; forget: (id: string) => void }) { - return

Paired devices

Android devices trusted by this computer

- {state.pairedDevices.length === 0 ?

No paired devices

Pair from Switchify Android while this computer is advertising.

: + return

Paired devices

Mobile devices trusted by this computer

+ {state.pairedDevices.length === 0 ?

No paired devices

Open Switchify on your mobile device to pair while this computer is advertising.

:
{state.pairedDevices.map((device) =>

{device.deviceName}

{device.lastSeenAt !== null ? `Last connected ${new Date(device.lastSeenAt).toLocaleString()}` : "Not connected yet"}

)}
}
; } @@ -333,7 +333,7 @@ function SupportView({ state, switches, busy, perform, openSetup, openUpdates }: {tab === "setup" ? <>

Input access

{state.accessibility === "required" && }

Local switches

{switches.state?.settings.bindings.length ?? 0} configured. Open Switches to assign Select, then open Scanning to adjust movement.

-

Optional Android connection

{bluetoothLabels[state.bluetooth]}. Open Android connection for pairing and forwarding profiles.

+

Optional mobile connection

{bluetoothLabels[state.bluetooth]}. Open Mobile connection for pairing and forwarding profiles.

:

Bluetooth connection

{bluetoothLabels[state.bluetooth]}

Input access

{state.accessibility === "required" ? : }
@@ -404,8 +404,8 @@ function SetupGuide({ state, switches, suspended, busy, error, skip, finish, acc {step === 0 &&

Input access

{state.accessibility === "required" && }
} - {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 === 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. 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.

Privacy policy
}
@@ -463,7 +463,7 @@ function PairingDialog({ requests, connectedDeviceName, busy, error, approve, re }; return
-

Pairing requests

Confirm each code matches Switchify for Android.

{connectedDeviceName &&

Connected to {connectedDeviceName}

}
{requests.length}
+

Pairing requests

Confirm each code matches Switchify for mobile.

{connectedDeviceName &&

Connected to {connectedDeviceName}

}
{requests.length}
{error &&

{error}

}
{requests.map((request, index) => { @@ -720,7 +720,7 @@ export function App() { useEffect(() => { if (view === "profiles") void api.listProfiles().then(setProfiles).catch((reason) => setError(String(reason))); }, [view]); const nav = useMemo(() => [ - ["home", "Home", ], ["switches", "Switches", ], ["scanning", "Scanning", ], ["android", "Android connection", ], ["settings", "Settings", ], + ["home", "Home", ], ["switches", "Switches", ], ["scanning", "Scanning", ], ["mobile", "Mobile connection", ], ["settings", "Settings", ], ["support", "Support", ], ] as const, []); @@ -769,16 +769,16 @@ export function App() { if (!state || !settings) return
Starting Switchify PC...
; return
- +
0}> {error &&
{error}
}

{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)} />} + {(view === "mobile" || view === "devices" || view === "profiles") &&

Mobile connection

Optional mobile control and switch forwarding

+ {view === "mobile" && void perform(api.disconnectAll)} />} {view === "devices" && void perform(() => api.forgetDevice(id))} />} {view === "profiles" && { profileEditorDirty.current = dirty; }} nativeExitRequest={profileExitRequest} onConfirmNativeExit={confirmProfileExit} onCancelNativeExit={cancelProfileExit} />}
} diff --git a/src/PointScan.test.tsx b/src/PointScan.test.tsx index df818839..0db1eb37 100644 --- a/src/PointScan.test.tsx +++ b/src/PointScan.test.tsx @@ -176,7 +176,7 @@ it("keeps a newer runtime event when an older save response arrives", async () = mocks.listen.mock.calls[0][1]({ payload: { ...initial, - message: "Android connected. Local scanning stopped.", + message: "Mobile device connected. Local scanning stopped.", }, }), ); @@ -184,7 +184,7 @@ it("keeps a newer runtime event when an older save response arrives", async () = resolveSave({ ...initial, enabled: true, message: "Ready." }), ); expect( - screen.getByText("Android connected. Local scanning stopped."), + screen.getByText("Mobile device connected. Local scanning stopped."), ).toBeInTheDocument(); }); diff --git a/src/Switches.test.tsx b/src/Switches.test.tsx index a306d25b..f7e4d7af 100644 --- a/src/Switches.test.tsx +++ b/src/Switches.test.tsx @@ -52,12 +52,12 @@ const scan = { message: "Scanning is off.", supported: true, }; -function Shell({ visible = true, androidConnected = false }: { visible?: boolean; androidConnected?: boolean }) { +function Shell({ visible = true, mobileConnected = false }: { visible?: boolean; mobileConnected?: boolean }) { const switches = useSwitches(); const scanning = useScanning(); return ( <> - {visible && } + {visible && } ); @@ -608,7 +608,7 @@ it("separates an empty local configuration from the six remote presets", async ( 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.getByText(/No mobile device connected/)).toBeInTheDocument(); expect(screen.getAllByText(/Remote forwarding slot/)).toHaveLength(6); expect(mocks.invoke.mock.calls.some(([command]) => command.startsWith("save_"))).toBe(false); }); @@ -623,9 +623,9 @@ it("keeps source identity after renaming and separates connection from tested in expect(within(row).getByText("Remote 1")).toBeInTheDocument(); const localRow = screen.getByRole("heading", { name: "Head switch" }).closest("article")!; expect(within(localRow).getByText("On this computer")).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(/Mobile device connected. This does not confirm/)).toHaveTextContent("A connection alone does not verify a physical switch press."); + expect(screen.queryByText(/No mobile device connected/)).not.toBeInTheDocument(); view.rerender(); - expect(screen.getByText(/No Android device connected/)).toBeInTheDocument(); + expect(screen.getByText(/No mobile device connected/)).toBeInTheDocument(); }); diff --git a/src/assets/android-download-qr.png b/src/assets/mobile-download-qr.png similarity index 100% rename from src/assets/android-download-qr.png rename to src/assets/mobile-download-qr.png diff --git a/src/settings/CursorSection.tsx b/src/settings/CursorSection.tsx index eb28bbf4..3d7f5708 100644 --- a/src/settings/CursorSection.tsx +++ b/src/settings/CursorSection.tsx @@ -5,7 +5,7 @@ import { } from "./controls"; export function CursorSection({ settings, update }: { settings: AppSettings; update: SettingsUpdate }) { - return + return update("cursorOverlayEnabled", value)} />
legend="Overlay visibility" disabled={!settings.cursorOverlayEnabled} options={overlayVisibilityOptions} value={settings.cursorOverlayVisibility} onChange={(next) => update("cursorOverlayVisibility", next)} diff --git a/src/settings/PointerSection.tsx b/src/settings/PointerSection.tsx index 4d42e271..b5eab0a6 100644 --- a/src/settings/PointerSection.tsx +++ b/src/settings/PointerSection.tsx @@ -47,7 +47,7 @@ export function PointerSection({ settings, update }: { settings: AppSettings; up 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 Android pointer movement stops, a countdown appears and performs one left click. Move again to rearm it." }} /> + note={{ summary: "After mobile pointer movement stops, a countdown appears and performs one left click. Move again to rearm it." }} />
; diff --git a/src/settings/ScanningSection.tsx b/src/settings/ScanningSection.tsx index f4d4cab2..4b635ece 100644 --- a/src/settings/ScanningSection.tsx +++ b/src/settings/ScanningSection.tsx @@ -32,7 +32,7 @@ export function ScanningSection({ controller }: { controller: ScanningController
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.

-

Assigned keys stay reserved while Switchify runs. Escape resets the scan. Android connections pause local scanning until they end.

+

Assigned keys stay reserved while Switchify runs. Escape resets the scan. Mobile connections pause local scanning until they end.

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 37fd8a22..faae3cc4 100644 --- a/src/settings/SwitchesSection.tsx +++ b/src/settings/SwitchesSection.tsx @@ -380,7 +380,7 @@ function SwitchConfirmation({ title, action, busy, error, cancel, confirm }: { t
, document.body); } -export function SwitchesSection({ controller, onDraftChange, suspended = false, androidConnected }: { controller: SwitchController; onDraftChange?: (draft: boolean) => void; suspended?: boolean; androidConnected?: boolean }) { +export function SwitchesSection({ controller, onDraftChange, suspended = false, mobileConnected }: { controller: SwitchController; onDraftChange?: (draft: boolean) => void; suspended?: boolean; mobileConnected?: 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. @@ -679,11 +679,11 @@ export function SwitchesSection({ controller, onDraftChange, suspended = false, ? "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.

-

{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.

+

{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.

{ expect(within(dwellDelay).getByRole("button", { name: "1.5s" })).toHaveAttribute("aria-pressed", "true"); // Dwell's whole explanation fits one note, so it gets no disclosure, and // the group is described by it. - const dwellNote = screen.getByText(/After Android pointer movement stops/); + const dwellNote = screen.getByText(/After mobile pointer movement stops/); expect(screen.queryByRole("button", { name: /about dwell/ })).not.toBeInTheDocument(); expect(dwellDelay).toHaveAttribute("aria-describedby", dwellNote.id); }); diff --git a/src/styles.css b/src/styles.css index b56f6b12..fe45b80a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -186,13 +186,13 @@ button:disabled { cursor: default; opacity: 0.55; } .setup-statuses { border-top: 1px solid var(--border); } .setup-statuses article { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; min-height: 92px; border-bottom: 1px solid var(--border); padding: 14px 4px; } .setup-statuses h3 { font-size: 14px; } -.android-download { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 36px; } -.android-download figure { margin: 0; max-width: 210px; } -.android-download figcaption { margin-top: 8px; color: var(--muted); font-size: 13px; } -.android-pairing { margin-top: 28px; } -.android-pairing ol { padding-left: 24px; color: var(--muted); line-height: 1.6; } -.android-download a { width: max-content; margin-top: 22px; text-decoration: none; } -.android-download img { width: 190px; height: 190px; border: 1px solid var(--border); border-radius: 9px; padding: 10px; background: white; image-rendering: crisp-edges; } +.mobile-download { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 36px; } +.mobile-download figure { margin: 0; max-width: 210px; } +.mobile-download figcaption { margin-top: 8px; color: var(--muted); font-size: 13px; } +.mobile-pairing { margin-top: 28px; } +.mobile-pairing ol { padding-left: 24px; color: var(--muted); line-height: 1.6; } +.mobile-download a { width: max-content; margin-top: 22px; text-decoration: none; } +.mobile-download img { width: 190px; height: 190px; border: 1px solid var(--border); border-radius: 9px; padding: 10px; background: white; image-rendering: crisp-edges; } .setup-pairings { margin-top: 20px; border-top: 1px solid var(--border); } .setup-pairings article { display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 16px; min-height: 82px; border-bottom: 1px solid var(--border); } .setup-pairings article > div:first-child { display: grid; gap: 4px; } @@ -266,8 +266,8 @@ button:disabled { cursor: default; opacity: 0.55; } .task-list article > button { grid-column: 1 / -1; } .setup-dialog { min-height: 0; padding: 20px; } .setup-content { padding: 28px 0 20px; } - .android-download { grid-template-columns: 1fr; } - .android-download img { width: 150px; height: 150px; } + .mobile-download { grid-template-columns: 1fr; } + .mobile-download img { width: 150px; height: 150px; } .setup-statuses article, .setup-pairings article { grid-template-columns: auto 1fr; } .setup-statuses article > button, .setup-pairings article > div:last-child { grid-column: 1 / -1; } }