Skip to content

Commit 2467ece

Browse files
committed
MTP: Add settings toggle to disable MTP
- Add `fileOperations.mtpEnabled` setting (Settings > General > File operations) to enable/disable Android device support - `MTP_ENABLED` `AtomicBool` gate in `watcher.rs` — watcher loop keeps running but skips auto-connect when disabled - `set_mtp_enabled()` async: disconnects all devices, clears `KNOWN_DEVICES`, restores `ptpcamerad` (macOS) - `set_mtp_enabled_flag()` sync: sets the flag at startup before `start_mtp_watcher()` - Moved `load_settings()` earlier in `lib.rs` so the flag is set before the watcher auto-connects - Frontend: registry entry, `SettingSwitch` in `FileOperationsSection`, `settings-applier.ts` dispatch - Takes effect instantly — no restart needed
1 parent d161f9b commit 2467ece

File tree

18 files changed

+370
-44
lines changed

18 files changed

+370
-44
lines changed

apps/desktop/src-tauri/src/commands/mtp.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ pub struct MtpScanResult {
2222
pub total_bytes: u64,
2323
}
2424

25+
/// Enables or disables MTP support at runtime.
26+
///
27+
/// When disabled: disconnects all devices, stops auto-connecting, and restores
28+
/// ptpcamerad (macOS). When enabled: resumes auto-connecting and checks for
29+
/// already-plugged-in devices.
30+
#[tauri::command]
31+
pub async fn set_mtp_enabled(enabled: bool) {
32+
mtp::set_mtp_enabled(enabled).await;
33+
}
34+
2535
/// Lists all connected MTP devices.
2636
///
2737
/// This returns devices detected via USB that support MTP protocol.

apps/desktop/src-tauri/src/lib.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,13 @@ pub fn run() {
329329
#[cfg(target_os = "macos")]
330330
mtp::macos_workaround::ensure_ptpcamerad_enabled();
331331

332+
// Load persisted settings early so MTP enabled flag is set before the watcher starts
333+
let saved_settings = settings::load_settings(app.handle());
334+
335+
// Apply MTP enabled setting (default: true) before starting the watcher
336+
#[cfg(any(target_os = "macos", target_os = "linux"))]
337+
mtp::set_mtp_enabled_flag(saved_settings.mtp_enabled.unwrap_or(true));
338+
332339
// Start MTP device hotplug watcher (Android device support)
333340
// This also auto-connects any devices already plugged in at startup
334341
#[cfg(any(target_os = "macos", target_os = "linux"))]
@@ -355,9 +362,6 @@ pub fn run() {
355362
// Initialize font metrics for default font (system font at 12px)
356363
font_metrics::init_font_metrics(app.handle(), "system-400-12");
357364

358-
// Load persisted settings to initialize menu with correct state
359-
let saved_settings = settings::load_settings(app.handle());
360-
361365
// Apply direct SMB connection setting (default: true)
362366
file_system::set_direct_smb_enabled(saved_settings.direct_smb_connection.unwrap_or(true));
363367

@@ -691,6 +695,8 @@ pub fn run() {
691695
commands::sync_status::get_sync_status,
692696
// MTP commands (macOS + Linux - Android device support)
693697
#[cfg(any(target_os = "macos", target_os = "linux"))]
698+
commands::mtp::set_mtp_enabled,
699+
#[cfg(any(target_os = "macos", target_os = "linux"))]
694700
commands::mtp::list_mtp_devices,
695701
#[cfg(any(target_os = "macos", target_os = "linux"))]
696702
commands::mtp::connect_mtp_device,
@@ -725,6 +731,8 @@ pub fn run() {
725731
#[cfg(feature = "virtual-mtp")]
726732
commands::mtp::resume_virtual_mtp_watcher,
727733
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
734+
stubs::mtp::set_mtp_enabled,
735+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
728736
stubs::mtp::list_mtp_devices,
729737
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
730738
stubs::mtp::connect_mtp_device,

apps/desktop/src-tauri/src/mtp/CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ On Linux, users may need udev rules for USB device permissions (see `resources/9
2929
USB plug-in
3030
→ nusb hotplug event (watcher.rs)
3131
→ 500 ms delay
32+
→ check MTP_ENABLED gate — skip if disabled
3233
→ list_mtp_devices() (discovery.rs)
3334
→ auto_connect_device() (watcher.rs)
3435
→ MtpConnectionManager::connect()
@@ -52,6 +53,15 @@ Event loop (event_loop.rs)
5253
→ emit directory-diff (same format as local file watching)
5354
```
5455

56+
### MTP enabled/disabled toggle
57+
58+
`MTP_ENABLED` (`AtomicBool`, default `true`) in `watcher.rs` gates all auto-connect behavior. The watcher loop always runs (it's `OnceLock`-based, no shutdown channel), but `check_for_device_changes()` returns early when disabled.
59+
60+
- **`set_mtp_enabled_flag(bool)`** — Sets the flag without side effects. Called at startup from `lib.rs` before `start_mtp_watcher()` so the initial auto-connect respects the persisted setting.
61+
- **`set_mtp_enabled(bool, app)`** — Async. Called at runtime via the `set_mtp_enabled` Tauri command. When disabling: disconnects all devices, clears `KNOWN_DEVICES`, restores ptpcamerad (macOS). When enabling: calls `check_for_device_changes()` to pick up already-plugged devices.
62+
- **Setting key**: `fileOperations.mtpEnabled` in `settings.json`, read by `settings/loader.rs` at startup.
63+
- **Interaction with ptpcamerad**: disabling MTP calls `restore_ptpcamerad_unconditionally()`. Re-enabling triggers auto-connect, which re-suppresses ptpcamerad if devices are found.
64+
5565
The frontend is a passive consumer: it subscribes to `volumes-changed` (for the volume picker)
5666
and `mtp-device-connected`/`mtp-device-disconnected` (for device connection state tracking).
5767
It never orchestrates MTP connections.

apps/desktop/src-tauri/src/mtp/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub mod watcher;
3030
pub use connection::{ConnectedDeviceInfo, MtpConnectionError, MtpObjectInfo, MtpOperationResult, connection_manager};
3131
pub use discovery::list_mtp_devices;
3232
pub use types::{MtpDeviceInfo, MtpStorageInfo};
33-
pub use watcher::start_mtp_watcher;
33+
pub use watcher::{set_mtp_enabled, set_mtp_enabled_flag, start_mtp_watcher};
3434

3535
/// The Terminal command that users can run to work around ptpcamerad on macOS.
3636
/// Returns an empty string on non-macOS platforms (ptpcamerad doesn't exist there).

apps/desktop/src-tauri/src/mtp/watcher.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use log::{debug, error, info, warn};
99
use nusb::hotplug::HotplugEvent;
1010
use std::collections::HashSet;
11+
use std::sync::atomic::{AtomicBool, Ordering};
1112
use std::sync::{Mutex, OnceLock};
1213
use tauri::AppHandle;
1314

@@ -20,6 +21,55 @@ static KNOWN_DEVICES: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
2021
/// Flag to indicate watcher has been started
2122
static WATCHER_STARTED: OnceLock<()> = OnceLock::new();
2223

24+
/// Whether MTP support is enabled. When false, the watcher loop still runs
25+
/// but `check_for_device_changes()` returns early and no auto-connects happen.
26+
static MTP_ENABLED: AtomicBool = AtomicBool::new(true);
27+
28+
/// Sets the MTP enabled flag without side effects. Used at startup before the
29+
/// watcher starts, so the initial auto-connect respects the persisted setting.
30+
pub fn set_mtp_enabled_flag(enabled: bool) {
31+
MTP_ENABLED.store(enabled, Ordering::SeqCst);
32+
debug!("MTP enabled flag set to {}", enabled);
33+
}
34+
35+
/// Enables or disables MTP support at runtime.
36+
///
37+
/// When disabling: disconnects all connected devices, clears known devices,
38+
/// and restores ptpcamerad (macOS). When enabling: re-checks for plugged-in
39+
/// devices so they get auto-connected.
40+
pub async fn set_mtp_enabled(enabled: bool) {
41+
let was_enabled = MTP_ENABLED.swap(enabled, Ordering::SeqCst);
42+
if was_enabled == enabled {
43+
debug!("MTP enabled unchanged ({})", enabled);
44+
return;
45+
}
46+
47+
info!("MTP support {}", if enabled { "enabled" } else { "disabled" });
48+
49+
if enabled {
50+
check_for_device_changes();
51+
} else {
52+
// Disconnect all connected devices
53+
let cm = super::connection_manager();
54+
let connected = cm.get_all_connected_devices().await;
55+
for device in &connected {
56+
let device_id = device.device.id.clone();
57+
auto_disconnect_device(device_id);
58+
}
59+
60+
// Clear known devices so re-enable detects everything as new
61+
if let Some(known) = KNOWN_DEVICES.get()
62+
&& let Ok(mut guard) = known.lock()
63+
{
64+
guard.clear();
65+
}
66+
67+
// Restore ptpcamerad on macOS
68+
#[cfg(target_os = "macos")]
69+
restore_ptpcamerad_unconditionally();
70+
}
71+
}
72+
2373
/// Gets the current set of MTP devices using mtp-rs discovery.
2474
fn get_current_mtp_devices() -> HashSet<String> {
2575
let devices = super::list_mtp_devices();
@@ -28,7 +78,12 @@ fn get_current_mtp_devices() -> HashSet<String> {
2878

2979
/// Checks for MTP device changes by comparing current state with known state.
3080
/// Auto-connects newly detected devices and disconnects removed ones.
81+
/// Returns early if MTP is disabled.
3182
fn check_for_device_changes() {
83+
if !MTP_ENABLED.load(Ordering::SeqCst) {
84+
return;
85+
}
86+
3287
let current_devices = get_current_mtp_devices();
3388

3489
let known = match KNOWN_DEVICES.get() {
@@ -133,8 +188,8 @@ pub fn start_mtp_watcher(app: &AppHandle) {
133188
initial_devices.len()
134189
);
135190

136-
// Auto-connect any devices already plugged in at startup
137-
if !initial_devices.is_empty() {
191+
// Auto-connect any devices already plugged in at startup (skip if MTP is disabled)
192+
if !initial_devices.is_empty() && MTP_ENABLED.load(Ordering::SeqCst) {
138193
#[cfg(target_os = "macos")]
139194
suppress_ptpcamerad_if_needed();
140195

@@ -214,6 +269,24 @@ fn suppress_ptpcamerad_if_needed() {
214269
}
215270
}
216271

272+
/// Restores ptpcamerad unconditionally (used when MTP is disabled).
273+
/// Emits `mtp-ptpcamerad-restored` event on success.
274+
#[cfg(target_os = "macos")]
275+
fn restore_ptpcamerad_unconditionally() {
276+
use tauri::Emitter;
277+
278+
match super::macos_workaround::restore_ptpcamerad() {
279+
Ok(true) => {
280+
info!("Restored ptpcamerad (MTP disabled)");
281+
if let Some(app) = APP_HANDLE.get() {
282+
let _ = app.emit("mtp-ptpcamerad-restored", ());
283+
}
284+
}
285+
Ok(false) => {} // Wasn't suppressed
286+
Err(e) => warn!("Failed to restore ptpcamerad: {}", e),
287+
}
288+
}
289+
217290
/// Restores ptpcamerad when no MTP devices remain connected.
218291
/// Emits `mtp-ptpcamerad-restored` event on success.
219292
#[cfg(target_os = "macos")]
@@ -243,9 +316,28 @@ mod tests {
243316

244317
#[test]
245318
fn test_get_current_mtp_devices() {
246-
// This test just verifies the function runs without panicking
319+
// This test verifies the function runs without panicking
247320
let devices = get_current_mtp_devices();
248321
// The function should complete without error (even if empty)
249322
assert!(devices.is_empty() || !devices.is_empty());
250323
}
324+
325+
#[test]
326+
fn test_mtp_enabled_flag_defaults_to_true() {
327+
assert!(MTP_ENABLED.load(Ordering::SeqCst));
328+
}
329+
330+
#[test]
331+
fn test_set_mtp_enabled_flag() {
332+
let original = MTP_ENABLED.load(Ordering::SeqCst);
333+
334+
set_mtp_enabled_flag(false);
335+
assert!(!MTP_ENABLED.load(Ordering::SeqCst));
336+
337+
set_mtp_enabled_flag(true);
338+
assert!(MTP_ENABLED.load(Ordering::SeqCst));
339+
340+
// Restore original state
341+
MTP_ENABLED.store(original, Ordering::SeqCst);
342+
}
251343
}

apps/desktop/src-tauri/src/settings/CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Settings {
2121
crash_reports_enabled: Option<bool>, // from "updates.crashReports"
2222
ai_provider: Option<String>, // from "ai.provider", for crash reports
2323
verbose_logging: Option<bool>, // from "developer.verboseLogging", for crash reports
24+
direct_smb_connection: Option<bool>, // from "network.directSmbConnection"
25+
mtp_enabled: Option<bool>, // from "fileOperations.mtpEnabled"
2426
}
2527
```
2628

apps/desktop/src-tauri/src/settings/loader.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ pub struct Settings {
5151
pub verbose_logging: Option<bool>,
5252
#[serde(alias = "network.directSmbConnection", default)]
5353
pub direct_smb_connection: Option<bool>,
54+
#[serde(alias = "fileOperations.mtpEnabled", default)]
55+
pub mtp_enabled: Option<bool>,
5456
}
5557

5658
fn default_show_hidden() -> bool {
@@ -69,6 +71,7 @@ impl Default for Settings {
6971
ai_provider: None,
7072
verbose_logging: None,
7173
direct_smb_connection: None,
74+
mtp_enabled: None,
7275
}
7376
}
7477
}
@@ -116,6 +119,7 @@ fn parse_settings(contents: &str) -> Result<Settings, serde_json::Error> {
116119
let ai_provider = json.get("ai.provider").and_then(|v| v.as_str()).map(String::from);
117120
let verbose_logging = json.get("developer.verboseLogging").and_then(|v| v.as_bool());
118121
let direct_smb_connection = json.get("network.directSmbConnection").and_then(|v| v.as_bool());
122+
let mtp_enabled = json.get("fileOperations.mtpEnabled").and_then(|v| v.as_bool());
119123

120124
Ok(Settings {
121125
show_hidden_files,
@@ -127,5 +131,6 @@ fn parse_settings(contents: &str) -> Result<Settings, serde_json::Error> {
127131
ai_provider,
128132
verbose_logging,
129133
direct_smb_connection,
134+
mtp_enabled,
130135
})
131136
}

apps/desktop/src-tauri/src/stubs/mtp.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ impl std::fmt::Display for MtpConnectionError {
5959

6060
impl std::error::Error for MtpConnectionError {}
6161

62+
/// Enables or disables MTP support (stub - no-op).
63+
#[tauri::command]
64+
pub async fn set_mtp_enabled(_enabled: bool) {}
65+
6266
/// Lists connected MTP devices (stub - always returns empty).
6367
#[tauri::command]
6468
pub fn list_mtp_devices() -> Vec<MtpDeviceInfo> {

apps/desktop/src/lib/file-explorer/navigation/CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,14 @@ keyboard navigation (ArrowRight to open, ArrowLeft/Escape to close, Enter to act
125125
These patterns emerged during the volume picker implementation and should be followed in future dropdown/submenu work:
126126

127127
- **CSS triangles for arrows/chevrons**, not font characters. Font-based arrows (``, ``) render at inconsistent sizes
128-
across fonts and OS versions. Use the CSS border trick (`border-left: 4px solid transparent; border-right: 4px solid
129-
transparent; border-top: 5px solid currentcolor`) for pixel-perfect control.
128+
across fonts and OS versions. Use the CSS border trick
129+
(`border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid currentcolor`) for
130+
pixel-perfect control.
130131
- **Single cursor rule.** When a submenu opens, suppress the main menu highlight. Exactly one cursor should be visible
131132
at all times. Use a state flag (like `submenuVolumeId`) to conditionally remove the `is-focused-and-under-cursor`
132133
class from the main menu.
133-
- **Elements with independent actions must be outside their parent's click area.** If a button inside another button
134-
has a different action (like "Volume options" inside "Volume selector"), it must be a sibling, not a child. Otherwise
134+
- **Elements with independent actions must be outside their parent's click area.** If a button inside another button has
135+
a different action (like "Volume options" inside "Volume selector"), it must be a sibling, not a child. Otherwise
135136
`stopPropagation` fights with the parent's click handler.
136137
- **Fixed positioning for submenus inside scrollable containers.** A submenu inside a `overflow-y: auto` dropdown gets
137138
clipped. Use `position: fixed` with coordinates calculated from `getBoundingClientRect()` of the trigger element.

apps/desktop/src/lib/mtp/CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ Multiple storages (Internal + SD Card) become separate volumes in UI. Each has d
2424
Listen to `mtp-directory-changed` events from backend. When device emits MTP `ObjectAdded/Removed/Changed`, backend
2525
sends event with `deviceId`. Frontend re-fetches current directory if viewing that device.
2626

27+
### Settings toggle (`fileOperations.mtpEnabled`)
28+
29+
MTP support can be disabled entirely from Settings > General > File operations. The toggle calls `setMtpEnabled()`
30+
(wired through `settings-applier.ts`), which invokes the `set_mtp_enabled` Tauri command. When disabled, all devices
31+
disconnect and hotplug events are ignored. The frontend is passive — it reacts to `volumes-changed` events as usual.
32+
2733
### Automatic ptpcamerad suppression (macOS)
2834

2935
On macOS, `ptpcamerad` daemon auto-claims MTP/PTP devices. The backend now handles this automatically:

0 commit comments

Comments
 (0)