Skip to content

Commit 8f78596

Browse files
committed
Wire up all settings to actual functionality
- Add cross-window settings sync via Tauri events (settings:changed) - Create reactive-settings.svelte.ts for immediate UI updates - Wire UI density to row heights in BriefList/FullList - Wire date/time format with live formatting updates - Wire file size format (binary/SI) throughout the app - Wire network timeout settings to SMB client operations - Wire developer settings (verbose logging, MCP config) - Wire advanced settings (virtualization buffer, debounce, timeouts) - Fix Reset feature not clearing modified indicators - Fix app icons toggle causing missing icons (cache refresh) - Fix BriefList bottom bar to use reactive file size format - Add format-utils.ts for pure formatting functions - Add network-settings.ts helpers for timeout calculations - Add settings-applier.ts for CSS variable and backend sync
1 parent a854da9 commit 8f78596

46 files changed

Lines changed: 916 additions & 201 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/coverage-allowlist.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"settings/components/SettingRadioGroup.svelte": { "reason": "UI component, logic is simple" },
3535
"settings/components/SectionSummary.svelte": { "reason": "UI component, simple navigation cards" },
3636
"settings/format-utils.ts": { "reason": "Simple date formatting, low complexity" },
37+
"settings/network-settings.ts": { "reason": "Thin wrapper over getSetting, requires mocking settings store" },
3738
"settings/reactive-settings.svelte.ts": { "reason": "Svelte reactive state, depends on settings store" },
3839
"settings/settings-applier.ts": { "reason": "Depends on DOM APIs (document.documentElement.style)" },
3940
"settings/components/SettingRow.svelte": { "reason": "UI component, layout only" },

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ pub fn cancel_write_operation(operation_id: String, rollback: bool) {
392392
/// the actual copy operation.
393393
///
394394
/// # Events emitted
395-
/// * `scan-preview-progress` - Every 100ms with current counts
395+
/// * `scan-preview-progress` - Based on progress_interval_ms setting
396396
/// * `scan-preview-complete` - When scanning finishes
397397
/// * `scan-preview-error` - On error
398398
/// * `scan-preview-cancelled` - If cancelled
@@ -402,15 +402,18 @@ pub fn cancel_write_operation(operation_id: String, rollback: bool) {
402402
/// * `sources` - List of source file/directory paths. Supports tilde expansion (~).
403403
/// * `sort_column` - Column to sort files by.
404404
/// * `sort_order` - Sort order (ascending/descending).
405+
/// * `progress_interval_ms` - Progress update interval in milliseconds (default: 500).
405406
#[tauri::command]
406407
pub fn start_scan_preview(
407408
app: tauri::AppHandle,
408409
sources: Vec<String>,
409410
sort_column: SortColumn,
410411
sort_order: SortOrder,
412+
progress_interval_ms: Option<u64>,
411413
) -> ScanPreviewStartResult {
412414
let sources: Vec<PathBuf> = sources.iter().map(|s| PathBuf::from(expand_tilde(s))).collect();
413-
ops_start_scan_preview(app, sources, sort_column, sort_order)
415+
let progress_interval = progress_interval_ms.unwrap_or(500);
416+
ops_start_scan_preview(app, sources, sort_column, sort_order, progress_interval)
414417
}
415418

416419
/// Cancels a running scan preview.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod icons;
77
pub mod licensing;
88
#[cfg(target_os = "macos")]
99
pub mod network;
10+
pub mod settings;
1011
pub mod sync_status; // Has both macOS and non-macOS implementations
1112
pub mod ui;
1213
#[cfg(target_os = "macos")]

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

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,60 @@ pub async fn resolve_host(host_id: String) -> Option<NetworkHost> {
5959

6060
/// Lists shares available on a network host.
6161
///
62-
/// Returns cached results if available (30 second TTL), otherwise queries the host.
62+
/// Returns cached results if available, otherwise queries the host.
6363
/// Attempts guest access first; returns an error if authentication is required.
6464
///
6565
/// # Arguments
6666
/// * `host_id` - Unique identifier for the host (used for caching)
6767
/// * `hostname` - Hostname to connect to (for example, "TEST_SERVER.local")
6868
/// * `ip_address` - Optional resolved IP address (preferred over hostname for reliability)
6969
/// * `port` - SMB port (default 445, but Docker containers may use different ports)
70+
/// * `timeout_ms` - Optional timeout in milliseconds (default: 15000)
71+
/// * `cache_ttl_ms` - Optional cache TTL in milliseconds (default: 30000)
7072
#[tauri::command]
7173
pub async fn list_shares_on_host(
7274
host_id: String,
7375
hostname: String,
7476
ip_address: Option<String>,
7577
port: u16,
78+
timeout_ms: Option<u64>,
79+
cache_ttl_ms: Option<u64>,
7680
) -> Result<ShareListResult, ShareListError> {
77-
smb_client::list_shares(&host_id, &hostname, ip_address.as_deref(), port, None).await
81+
smb_client::list_shares(
82+
&host_id,
83+
&hostname,
84+
ip_address.as_deref(),
85+
port,
86+
None,
87+
timeout_ms,
88+
cache_ttl_ms,
89+
)
90+
.await
7891
}
7992

8093
/// Prefetches shares for a host (for example, on hover).
8194
/// Same as list_shares_on_host but designed for prefetching - errors are silently ignored.
8295
/// Returns immediately if shares are already cached.
8396
#[tauri::command]
84-
pub async fn prefetch_shares(host_id: String, hostname: String, ip_address: Option<String>, port: u16) {
97+
pub async fn prefetch_shares(
98+
host_id: String,
99+
hostname: String,
100+
ip_address: Option<String>,
101+
port: u16,
102+
timeout_ms: Option<u64>,
103+
cache_ttl_ms: Option<u64>,
104+
) {
85105
// Fire and forget - we don't care about the result for prefetching
86-
let _ = smb_client::list_shares(&host_id, &hostname, ip_address.as_deref(), port, None).await;
106+
let _ = smb_client::list_shares(
107+
&host_id,
108+
&hostname,
109+
ip_address.as_deref(),
110+
port,
111+
None,
112+
timeout_ms,
113+
cache_ttl_ms,
114+
)
115+
.await;
87116
}
88117

89118
/// Gets auth mode detected for a host (from cached share list if available).
@@ -189,14 +218,22 @@ pub fn delete_smb_credentials(server: String, share: Option<String>) -> Result<(
189218
/// * `port` - SMB port
190219
/// * `username` - Username for authentication (or None for guest)
191220
/// * `password` - Password for authentication (or None for guest)
221+
/// * `timeout_ms` - Optional timeout in milliseconds (default: 15000)
222+
/// * `cache_ttl_ms` - Optional cache TTL in milliseconds (default: 30000)
192223
#[tauri::command]
224+
#[allow(
225+
clippy::too_many_arguments,
226+
reason = "Tauri command requires all parameters to be top-level"
227+
)]
193228
pub async fn list_shares_with_credentials(
194229
host_id: String,
195230
hostname: String,
196231
ip_address: Option<String>,
197232
port: u16,
198233
username: Option<String>,
199234
password: Option<String>,
235+
timeout_ms: Option<u64>,
236+
cache_ttl_ms: Option<u64>,
200237
) -> Result<ShareListResult, ShareListError> {
201238
let credentials = match (username, password) {
202239
(Some(u), Some(p)) => Some((u, p)),
@@ -209,6 +246,8 @@ pub async fn list_shares_with_credentials(
209246
ip_address.as_deref(),
210247
port,
211248
credentials.as_ref().map(|(u, p)| (u.as_str(), p.as_str())),
249+
timeout_ms,
250+
cache_ttl_ms,
212251
)
213252
.await
214253
}
@@ -228,6 +267,7 @@ use crate::network::mount::{self, MountError, MountResult};
228267
/// * `share` - Name of the share to mount
229268
/// * `username` - Optional username for authentication
230269
/// * `password` - Optional password for authentication
270+
/// * `timeout_ms` - Optional timeout in milliseconds (default: 20000)
231271
///
232272
/// # Returns
233273
/// * `Ok(MountResult)` - Mount successful, with path to mount point
@@ -238,6 +278,7 @@ pub async fn mount_network_share(
238278
share: String,
239279
username: Option<String>,
240280
password: Option<String>,
281+
timeout_ms: Option<u64>,
241282
) -> Result<MountResult, MountError> {
242-
mount::mount_share(server, share, username, password).await
283+
mount::mount_share(server, share, username, password, timeout_ms).await
243284
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! Settings-related commands.
2+
3+
use std::net::TcpListener;
4+
5+
use crate::file_system::update_debounce_ms;
6+
#[cfg(target_os = "macos")]
7+
use crate::network::bonjour::update_resolve_timeout;
8+
9+
/// Check if a port is available for binding.
10+
#[tauri::command]
11+
pub fn check_port_available(port: u16) -> bool {
12+
TcpListener::bind(("127.0.0.1", port)).is_ok()
13+
}
14+
15+
/// Find an available port starting from the given port.
16+
/// Scans up to 100 ports from the start port.
17+
#[tauri::command]
18+
pub fn find_available_port(start_port: u16) -> Option<u16> {
19+
for offset in 0..100 {
20+
let port = start_port.saturating_add(offset);
21+
if check_port_available(port) {
22+
return Some(port);
23+
}
24+
}
25+
None
26+
}
27+
28+
/// Updates the file watcher debounce duration in milliseconds.
29+
/// This affects newly created watchers; existing watchers keep their original duration.
30+
#[tauri::command]
31+
pub fn update_file_watcher_debounce(debounce_ms: u64) {
32+
update_debounce_ms(debounce_ms);
33+
}
34+
35+
/// Updates the Bonjour service resolve timeout in milliseconds.
36+
/// This affects future service resolutions; ongoing resolutions keep their original timeout.
37+
#[cfg(target_os = "macos")]
38+
#[tauri::command]
39+
pub fn update_service_resolve_timeout(timeout_ms: u64) {
40+
update_resolve_timeout(timeout_ms);
41+
}
42+
43+
/// Stub for non-macOS platforms - network discovery is not supported.
44+
#[cfg(not(target_os = "macos"))]
45+
#[tauri::command]
46+
pub fn update_service_resolve_timeout(_timeout_ms: u64) {
47+
// No-op on non-macOS platforms
48+
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use super::*;
53+
54+
#[test]
55+
fn test_check_port_available() {
56+
// Port 0 should let OS pick an available port, so this should succeed
57+
// But we test a high port that's likely free
58+
let result = check_port_available(49999);
59+
// The function should return a valid boolean (either true or false)
60+
// This test verifies the function executes without panic
61+
let _ = result;
62+
}
63+
64+
#[test]
65+
fn test_find_available_port() {
66+
// Should find some available port
67+
let result = find_available_port(49000);
68+
// On most systems, we should find an available port in the high range
69+
assert!(result.is_some());
70+
if let Some(port) = result {
71+
assert!(port >= 49000);
72+
assert!(port < 49100);
73+
}
74+
}
75+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub use volume::{InMemoryVolume, LocalPosixVolume, Volume, VolumeError};
4141
#[allow(unused_imports, reason = "Public API re-exports for future use")]
4242
pub use volume_manager::VolumeManager;
4343
// Watcher management - init_watcher_manager must be called from lib.rs
44-
pub use watcher::init_watcher_manager;
44+
pub use watcher::{init_watcher_manager, update_debounce_ms};
4545
// Re-export write operation types
4646
pub use write_operations::{
4747
OperationStatus, OperationSummary, WriteOperationConfig, WriteOperationError, WriteOperationStartResult,

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,23 @@ use tauri::{AppHandle, Emitter};
1616

1717
use super::operations::{FileEntry, get_listing_entries, list_directory_core, update_listing_entries};
1818

19-
/// Debounce duration in milliseconds
20-
const DEBOUNCE_MS: u64 = 200;
19+
/// Default debounce duration in milliseconds (used if not configured)
20+
const DEFAULT_DEBOUNCE_MS: u64 = 200;
21+
22+
/// Configured debounce duration in milliseconds (set by frontend via update_debounce_ms)
23+
static DEBOUNCE_MS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(DEFAULT_DEBOUNCE_MS);
24+
25+
/// Updates the file watcher debounce duration.
26+
/// This affects newly started watchers; existing watchers keep their original duration.
27+
pub fn update_debounce_ms(ms: u64) {
28+
DEBOUNCE_MS.store(ms, std::sync::atomic::Ordering::Relaxed);
29+
log::debug!("File watcher debounce updated to {} ms", ms);
30+
}
31+
32+
/// Gets the current debounce duration in milliseconds.
33+
fn get_debounce_ms() -> u64 {
34+
DEBOUNCE_MS.load(std::sync::atomic::Ordering::Relaxed)
35+
}
2136

2237
/// Global watcher manager
2338
static WATCHER_MANAGER: LazyLock<RwLock<WatcherManager>> = LazyLock::new(|| RwLock::new(WatcherManager::new()));
@@ -88,8 +103,9 @@ pub fn start_watching(listing_id: &str, path: &Path) -> Result<(), String> {
88103
let listing_for_closure = listing_id_owned.clone();
89104

90105
// Create the debouncer with a callback that handles changes
106+
let debounce_duration = Duration::from_millis(get_debounce_ms());
91107
let mut debouncer = new_debouncer(
92-
Duration::from_millis(DEBOUNCE_MS),
108+
debounce_duration,
93109
None, // No tick rate limit
94110
move |result: DebounceEventResult| {
95111
if let Ok(_events) = result {

apps/desktop/src-tauri/src/file_system/write_operations/copy.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub(super) fn copy_files_with_progress(
5252
operation_id,
5353
WriteOperationType::Copy,
5454
state.progress_interval,
55+
config.max_conflicts_to_show,
5556
)? {
5657
return Ok(());
5758
}

apps/desktop/src-tauri/src/file_system/write_operations/move_op.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub(super) fn move_files_with_progress(
3737
operation_id,
3838
WriteOperationType::Move,
3939
state.progress_interval,
40+
config.max_conflicts_to_show,
4041
)? {
4142
return Ok(());
4243
}

apps/desktop/src-tauri/src/file_system/write_operations/scan.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ use super::state::{
1616
WriteOperationState, update_operation_status,
1717
};
1818
use super::types::{
19-
ConflictInfo, MAX_CONFLICTS_IN_RESULT, ScanPreviewCancelledEvent, ScanPreviewCompleteEvent, ScanPreviewErrorEvent,
20-
ScanPreviewProgressEvent, ScanPreviewStartResult, ScanProgressEvent, SortColumn, SortOrder, WriteOperationError,
21-
WriteOperationPhase, WriteOperationType, WriteProgressEvent,
19+
ConflictInfo, ScanPreviewCancelledEvent, ScanPreviewCompleteEvent, ScanPreviewErrorEvent, ScanPreviewProgressEvent,
20+
ScanPreviewStartResult, ScanProgressEvent, SortColumn, SortOrder, WriteOperationError, WriteOperationPhase,
21+
WriteOperationType, WriteProgressEvent,
2222
};
2323

2424
// ============================================================================
@@ -32,13 +32,14 @@ pub fn start_scan_preview(
3232
sources: Vec<PathBuf>,
3333
sort_column: SortColumn,
3434
sort_order: SortOrder,
35+
progress_interval_ms: u64,
3536
) -> ScanPreviewStartResult {
3637
let preview_id = Uuid::new_v4().to_string();
3738
let preview_id_clone = preview_id.clone();
3839

3940
let state = Arc::new(ScanPreviewState {
4041
cancelled: AtomicBool::new(false),
41-
progress_interval: Duration::from_millis(100),
42+
progress_interval: Duration::from_millis(progress_interval_ms),
4243
});
4344

4445
// Register state
@@ -668,6 +669,7 @@ pub(super) fn handle_dry_run(
668669
operation_id: &str,
669670
operation_type: WriteOperationType,
670671
progress_interval: Duration,
672+
max_conflicts_to_show: usize,
671673
) -> Result<bool, WriteOperationError> {
672674
use super::types::DryRunResult;
673675
use tauri::Emitter;
@@ -687,7 +689,7 @@ pub(super) fn handle_dry_run(
687689
)?;
688690

689691
let conflicts_count = scan_result.conflicts.len();
690-
let (sampled_conflicts, conflicts_sampled) = sample_conflicts(scan_result.conflicts, MAX_CONFLICTS_IN_RESULT);
692+
let (sampled_conflicts, conflicts_sampled) = sample_conflicts(scan_result.conflicts, max_conflicts_to_show);
691693

692694
let result = DryRunResult {
693695
operation_id: operation_id.to_string(),

0 commit comments

Comments
 (0)