max / audiofiles
154 files changed,
+1820 insertions,
-1635 deletions
| @@ -58,3 +58,35 @@ libc = "0.2" | |||
| 58 | 58 | midir = "0.11" | |
| 59 | 59 | tagtree = { path = "../../MNW/shared/tagtree" } | |
| 60 | 60 | makeover = "2.0.0" | |
| 61 | + | ||
| 62 | + | [workspace.lints.rust] | |
| 63 | + | unused = "warn" | |
| 64 | + | unreachable_pub = "warn" | |
| 65 | + | ||
| 66 | + | [workspace.lints.clippy] | |
| 67 | + | pedantic = { level = "warn", priority = -1 } | |
| 68 | + | # Allow-list tuned from a measured breakdown across server/multithreaded/pter | |
| 69 | + | # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything | |
| 70 | + | # else in `pedantic` stays a warning. Keep this block identical across repos. | |
| 71 | + | module_name_repetitions = "allow" | |
| 72 | + | # Doc lints. No docs-completeness push is underway. | |
| 73 | + | missing_errors_doc = "allow" | |
| 74 | + | missing_panics_doc = "allow" | |
| 75 | + | doc_markdown = "allow" | |
| 76 | + | # Numeric casts. Endemic and mostly intentional in size and byte math. | |
| 77 | + | cast_possible_truncation = "allow" | |
| 78 | + | cast_sign_loss = "allow" | |
| 79 | + | cast_precision_loss = "allow" | |
| 80 | + | cast_possible_wrap = "allow" | |
| 81 | + | cast_lossless = "allow" | |
| 82 | + | # Subjective structure and style nags. High churn, low signal. | |
| 83 | + | must_use_candidate = "allow" | |
| 84 | + | too_many_lines = "allow" | |
| 85 | + | struct_excessive_bools = "allow" | |
| 86 | + | similar_names = "allow" | |
| 87 | + | items_after_statements = "allow" | |
| 88 | + | single_match_else = "allow" | |
| 89 | + | # Frequent false-positives in TUI and router-heavy code. | |
| 90 | + | match_same_arms = "allow" | |
| 91 | + | unnecessary_wraps = "allow" | |
| 92 | + | type_complexity = "allow" |
| @@ -28,7 +28,7 @@ midir = { workspace = true } | |||
| 28 | 28 | rfd = { workspace = true } | |
| 29 | 29 | toml = { workspace = true } | |
| 30 | 30 | # Pinned to the v1 store bundle explicitly: Apple Keychain, Windows credential | |
| 31 | - | # manager, and Secret Service on Linux. Do not enable `linux-keyutils-keyring-store` — | |
| 31 | + | # manager, and Secret Service on Linux. Do not enable `linux-keyutils-keyring-store`, | |
| 32 | 32 | # that backing store is in-memory and drops every entry on reboot, which silently | |
| 33 | 33 | # destroyed stored secrets before keyring 4 moved the default to Secret Service. | |
| 34 | 34 | keyring = { version = "4", default-features = false, features = ["v1"] } | |
| @@ -39,3 +39,6 @@ tempfile = "3" | |||
| 39 | 39 | [target.'cfg(target_os = "linux")'.dependencies] | |
| 40 | 40 | eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] } | |
| 41 | 41 | gtk = "0.18" | |
| 42 | + | ||
| 43 | + | [lints] | |
| 44 | + | workspace = true |
| @@ -187,7 +187,7 @@ impl AudioFilesApp { | |||
| 187 | 187 | let server_url = SYNC_SERVER_URL.to_string(); | |
| 188 | 188 | let key = self.license_key_input.trim().to_string(); | |
| 189 | 189 | let mid = self.machine_id.clone(); | |
| 190 | - | self._runtime.spawn(async move { | |
| 190 | + | self.runtime.spawn(async move { | |
| 191 | 191 | let result = super::license::activate_key(&server_url, &key, &mid).await; | |
| 192 | 192 | *slot.lock() = Some(result); | |
| 193 | 193 | }); | |
| @@ -219,7 +219,7 @@ impl AudioFilesApp { | |||
| 219 | 219 | let server_url = SYNC_SERVER_URL.to_string(); | |
| 220 | 220 | let key = cache.key_code.clone(); | |
| 221 | 221 | let mid = self.machine_id.clone(); | |
| 222 | - | self._runtime.spawn(async move { | |
| 222 | + | self.runtime.spawn(async move { | |
| 223 | 223 | if let Err(e) = super::license::deactivate_key(&server_url, &key, &mid).await { | |
| 224 | 224 | tracing::warn!("Deactivation request failed (best-effort): {e}"); | |
| 225 | 225 | } | |
| @@ -248,11 +248,11 @@ pub(crate) fn mask_key(key: &str) -> String { | |||
| 248 | 248 | /// The recovery action offered to the user for a given activation error class. | |
| 249 | 249 | #[derive(Debug, PartialEq, Eq)] | |
| 250 | 250 | pub(crate) enum RecoveryAffordance { | |
| 251 | - | /// Transient/server-side failure — offer a "Try again" button. | |
| 251 | + | /// Transient/server-side failure, offer a "Try again" button. | |
| 252 | 252 | Retry, | |
| 253 | - | /// The key itself is bad — link to buy a fresh one. | |
| 253 | + | /// The key itself is bad, link to buy a fresh one. | |
| 254 | 254 | GetNewKey, | |
| 255 | - | /// Key is bound elsewhere / hit its activation limit — link to support. | |
| 255 | + | /// Key is bound elsewhere / hit its activation limit, link to support. | |
| 256 | 256 | ContactSupport, | |
| 257 | 257 | } | |
| 258 | 258 |
| @@ -18,13 +18,13 @@ fn parse_synckit_toml_key() -> Option<String> { | |||
| 18 | 18 | /// OS keychain entry for the SyncKit API key. The key is a credential, not a | |
| 19 | 19 | /// regenerable cache, so it belongs in the keychain rather than a plaintext | |
| 20 | 20 | /// file. Returns None when no keychain provider is available (headless Linux | |
| 21 | - | /// without secret-service, CI) — callers fall back to file/env/bundled. | |
| 21 | + | /// without secret-service, CI), callers fall back to file/env/bundled. | |
| 22 | 22 | fn api_key_keychain_entry() -> Option<keyring::Entry> { | |
| 23 | 23 | keyring::Entry::new("audiofiles", "sync_api_key").ok() | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | 26 | /// Marker recording that we successfully stored a user key in the OS keychain. | |
| 27 | - | /// Holds no secret — its presence alone distinguishes "first run, no key yet" | |
| 27 | + | /// Holds no secret, its presence alone distinguishes "first run, no key yet" | |
| 28 | 28 | /// from "the keychain lost the key it was holding", which we must not treat | |
| 29 | 29 | /// alike: the latter would otherwise fall through to the bundled shared alpha | |
| 30 | 30 | /// key and sync under the wrong identity. | |
| @@ -69,7 +69,7 @@ pub(crate) fn load_api_key(data_dir: &Path) -> Option<String> { | |||
| 69 | 69 | if data_dir.join(KEY_SENTINEL).exists() { | |
| 70 | 70 | tracing::error!( | |
| 71 | 71 | "The saved SyncKit API key is missing from the OS keychain. \ | |
| 72 | - | Sync is disabled until the key is entered again — not falling \ | |
| 72 | + | Sync is disabled until the key is entered again, not falling \ | |
| 73 | 73 | back to the bundled key, which would sync under a different identity." | |
| 74 | 74 | ); | |
| 75 | 75 | return None; | |
| @@ -119,7 +119,7 @@ mod tests { | |||
| 119 | 119 | use super::*; | |
| 120 | 120 | ||
| 121 | 121 | // The plaintext-file path is tested via `read_key_file` so the tests stay | |
| 122 | - | // deterministic — `load_api_key` reads the real OS keychain first, whose | |
| 122 | + | // deterministic, `load_api_key` reads the real OS keychain first, whose | |
| 123 | 123 | // state is global and not test-controlled. | |
| 124 | 124 | ||
| 125 | 125 | #[test] |
| @@ -13,7 +13,7 @@ use tracing::instrument; | |||
| 13 | 13 | ||
| 14 | 14 | /// Errors from audio output stream setup. | |
| 15 | 15 | #[derive(Error, Debug)] | |
| 16 | - | pub enum AudioError { | |
| 16 | + | pub(crate) enum AudioError { | |
| 17 | 17 | #[error("no output audio device found")] | |
| 18 | 18 | NoDevice, | |
| 19 | 19 | // cpal 0.18 collapsed its per-operation error types into one `cpal::Error`, | |
| @@ -29,18 +29,19 @@ pub enum AudioError { | |||
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | 31 | /// Build and start a cpal output stream that reads from the shared preview state. | |
| 32 | - | /// Returns `(stream, device_sample_rate, device_name)` — the stream handle must | |
| 32 | + | /// Returns `(stream, device_sample_rate, device_name)`, the stream handle must | |
| 33 | 33 | /// be kept alive. `device_name` is whatever cpal reports for the default | |
| 34 | 34 | /// output device; it's surfaced in the footer for diagnostic visibility. | |
| 35 | 35 | #[instrument(skip_all)] | |
| 36 | - | pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, String), AudioError> { | |
| 36 | + | pub(crate) fn start_output_stream( | |
| 37 | + | shared: Arc<SharedState>, | |
| 38 | + | ) -> Result<(Stream, u32, String), AudioError> { | |
| 37 | 39 | let host = cpal::default_host(); | |
| 38 | 40 | let device = host.default_output_device().ok_or(AudioError::NoDevice)?; | |
| 39 | 41 | ||
| 40 | 42 | let device_name = device | |
| 41 | 43 | .description() | |
| 42 | - | .map(|d| d.name().to_string()) | |
| 43 | - | .unwrap_or_else(|_| "default".to_string()); | |
| 44 | + | .map_or_else(|_| "default".to_string(), |d| d.name().to_string()); | |
| 44 | 45 | let config = device | |
| 45 | 46 | .default_output_config() | |
| 46 | 47 | .map_err(AudioError::DefaultConfig)?; | |
| @@ -134,7 +135,7 @@ fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>( | |||
| 134 | 135 | move |err| { | |
| 135 | 136 | tracing::error!("audio stream error: {err}"); | |
| 136 | 137 | // Signal the app loop to rebuild the stream. Lock-free store only | |
| 137 | - | // — this runs in cpal's error context, never take a lock here. | |
| 138 | + | // this runs in cpal's error context, never take a lock here. | |
| 138 | 139 | err_shared | |
| 139 | 140 | .device_lost | |
| 140 | 141 | .store(true, std::sync::atomic::Ordering::Relaxed); | |
| @@ -156,7 +157,7 @@ pub(crate) fn fill_preview( | |||
| 156 | 157 | device_sample_rate: u32, | |
| 157 | 158 | ) { | |
| 158 | 159 | let Some(mut guard) = playback.try_lock() else { | |
| 159 | - | return; // GUI thread holds lock — leave buffer unchanged (already zeroed) | |
| 160 | + | return; // GUI thread holds lock, leave buffer unchanged (already zeroed) | |
| 160 | 161 | }; | |
| 161 | 162 | ||
| 162 | 163 | if !guard.playing || device_sample_rate == 0 { | |
| @@ -184,7 +185,7 @@ pub(crate) fn fill_preview( | |||
| 184 | 185 | ||
| 185 | 186 | if pos_int >= total_frames { | |
| 186 | 187 | if guard.streaming { | |
| 187 | - | // Still decoding — stop filling but keep playing | |
| 188 | + | // Still decoding, stop filling but keep playing | |
| 188 | 189 | guard.position_frac = pos_frac; | |
| 189 | 190 | return; | |
| 190 | 191 | } | |
| @@ -275,6 +276,10 @@ mod tests { | |||
| 275 | 276 | } | |
| 276 | 277 | ||
| 277 | 278 | #[test] | |
| 279 | + | #[allow( | |
| 280 | + | clippy::float_cmp, | |
| 281 | + | reason = "exact equality on deterministic test values" | |
| 282 | + | )] | |
| 278 | 283 | fn fill_cpal_mono_f32() { | |
| 279 | 284 | let playback = make_playback(vec![0.4, 0.6, 0.2, 0.8], 44100, true); | |
| 280 | 285 | let mut data = vec![0.0f32; 2]; | |
| @@ -286,6 +291,10 @@ mod tests { | |||
| 286 | 291 | } | |
| 287 | 292 | ||
| 288 | 293 | #[test] | |
| 294 | + | #[allow( | |
| 295 | + | clippy::float_cmp, | |
| 296 | + | reason = "exact equality on deterministic test values" | |
| 297 | + | )] | |
| 289 | 298 | fn fill_cpal_stops_at_end() { | |
| 290 | 299 | let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true); | |
| 291 | 300 | let mut data = vec![9.0f32; 8]; | |
| @@ -345,6 +354,10 @@ mod tests { | |||
| 345 | 354 | } | |
| 346 | 355 | ||
| 347 | 356 | #[test] | |
| 357 | + | #[allow( | |
| 358 | + | clippy::float_cmp, | |
| 359 | + | reason = "exact equality on deterministic test values" | |
| 360 | + | )] | |
| 348 | 361 | fn fill_cpal_loop_wraps() { | |
| 349 | 362 | let playback = Mutex::new(PreviewPlayback { | |
| 350 | 363 | buffer: Some(PreviewBuffer { | |
| @@ -377,6 +390,10 @@ mod tests { | |||
| 377 | 390 | } | |
| 378 | 391 | ||
| 379 | 392 | #[test] | |
| 393 | + | #[allow( | |
| 394 | + | clippy::float_cmp, | |
| 395 | + | reason = "exact equality on deterministic test values" | |
| 396 | + | )] | |
| 380 | 397 | fn fill_cpal_loop_disabled_stops() { | |
| 381 | 398 | let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true); | |
| 382 | 399 | let mut data = vec![9.0f32; 8]; | |
| @@ -414,6 +431,10 @@ mod tests { | |||
| 414 | 431 | } | |
| 415 | 432 | ||
| 416 | 433 | #[test] | |
| 434 | + | #[allow( | |
| 435 | + | clippy::float_cmp, | |
| 436 | + | reason = "exact equality on deterministic test values" | |
| 437 | + | )] | |
| 417 | 438 | fn clamp_prevents_overflow() { | |
| 418 | 439 | // Simulate very loud mixed audio | |
| 419 | 440 | let buf = [1.5f32, -1.5, 0.5, -0.5]; |
| @@ -14,20 +14,20 @@ use thiserror::Error; | |||
| 14 | 14 | ||
| 15 | 15 | /// Cached license data, persisted to `license.json`. | |
| 16 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 17 | - | pub struct LicenseCache { | |
| 17 | + | pub(crate) struct LicenseCache { | |
| 18 | 18 | pub key_code: String, | |
| 19 | 19 | pub machine_id: String, | |
| 20 | 20 | pub activated_at: String, | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | 23 | /// Whether the app has a valid cached license. | |
| 24 | - | pub enum LicenseStatus { | |
| 24 | + | pub(crate) enum LicenseStatus { | |
| 25 | 25 | Unlicensed, | |
| 26 | 26 | Licensed(LicenseCache), | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 29 | 29 | /// Shared slot for async activation results, polled each frame. | |
| 30 | - | pub type ActivationResult = Arc<Mutex<Option<Result<(), ActivationError>>>>; | |
| 30 | + | pub(crate) type ActivationResult = Arc<Mutex<Option<Result<(), ActivationError>>>>; | |
| 31 | 31 | ||
| 32 | 32 | // ── API request/response types ── | |
| 33 | 33 | ||
| @@ -60,7 +60,7 @@ struct DeactivateResponse { | |||
| 60 | 60 | // ── Machine identity ── | |
| 61 | 61 | ||
| 62 | 62 | /// Read or create a stable machine ID (UUIDv4) for this installation. | |
| 63 | - | pub fn get_or_create_machine_id(data_dir: &Path) -> String { | |
| 63 | + | pub(crate) fn get_or_create_machine_id(data_dir: &Path) -> String { | |
| 64 | 64 | let path = data_dir.join("machine_id"); | |
| 65 | 65 | if let Ok(id) = std::fs::read_to_string(&path) { | |
| 66 | 66 | let id = id.trim().to_string(); | |
| @@ -79,11 +79,10 @@ pub fn get_or_create_machine_id(data_dir: &Path) -> String { | |||
| 79 | 79 | // ── License file I/O ── | |
| 80 | 80 | ||
| 81 | 81 | /// Load a cached license from disk. Returns `Unlicensed` if missing or corrupt. | |
| 82 | - | pub fn load_license(data_dir: &Path) -> LicenseStatus { | |
| 82 | + | pub(crate) fn load_license(data_dir: &Path) -> LicenseStatus { | |
| 83 | 83 | let path = data_dir.join("license.json"); | |
| 84 | - | let bytes = match std::fs::read(&path) { | |
| 85 | - | Ok(b) => b, | |
| 86 | - | Err(_) => return LicenseStatus::Unlicensed, | |
| 84 | + | let Ok(bytes) = std::fs::read(&path) else { | |
| 85 | + | return LicenseStatus::Unlicensed; | |
| 87 | 86 | }; | |
| 88 | 87 | match serde_json::from_slice::<LicenseCache>(&bytes) { | |
| 89 | 88 | Ok(cache) => LicenseStatus::Licensed(cache), | |
| @@ -95,7 +94,7 @@ pub fn load_license(data_dir: &Path) -> LicenseStatus { | |||
| 95 | 94 | } | |
| 96 | 95 | ||
| 97 | 96 | /// Write a license cache to disk (atomic: write .tmp then rename). | |
| 98 | - | pub fn save_license(data_dir: &Path, cache: &LicenseCache) -> io::Result<()> { | |
| 97 | + | pub(crate) fn save_license(data_dir: &Path, cache: &LicenseCache) -> io::Result<()> { | |
| 99 | 98 | let path = data_dir.join("license.json"); | |
| 100 | 99 | let tmp = data_dir.join("license.json.tmp"); | |
| 101 | 100 | let json = serde_json::to_string_pretty(cache).map_err(io::Error::other)?; | |
| @@ -104,7 +103,7 @@ pub fn save_license(data_dir: &Path, cache: &LicenseCache) -> io::Result<()> { | |||
| 104 | 103 | } | |
| 105 | 104 | ||
| 106 | 105 | /// Remove the cached license file. | |
| 107 | - | pub fn remove_license(data_dir: &Path) -> io::Result<()> { | |
| 106 | + | pub(crate) fn remove_license(data_dir: &Path) -> io::Result<()> { | |
| 108 | 107 | let path = data_dir.join("license.json"); | |
| 109 | 108 | match std::fs::remove_file(&path) { | |
| 110 | 109 | Ok(()) => Ok(()), | |
| @@ -117,7 +116,7 @@ pub fn remove_license(data_dir: &Path) -> io::Result<()> { | |||
| 117 | 116 | ||
| 118 | 117 | /// Classified activation failure for per-class UI messaging. | |
| 119 | 118 | #[derive(Debug, Clone)] | |
| 120 | - | pub enum ActivationError { | |
| 119 | + | pub(crate) enum ActivationError { | |
| 121 | 120 | /// Couldn't reach the activation server (DNS, timeout, connection refused). | |
| 122 | 121 | Network, | |
| 123 | 122 | /// HTTP non-2xx response from the server. | |
| @@ -175,7 +174,7 @@ fn classify_server_error(msg: &str) -> ActivationError { | |||
| 175 | 174 | /// Sends the key and machine ID to the server for validation. On success the | |
| 176 | 175 | /// server records an activation slot; on failure the returned error is | |
| 177 | 176 | /// classified for per-class UI messaging. | |
| 178 | - | pub async fn activate_key( | |
| 177 | + | pub(crate) async fn activate_key( | |
| 179 | 178 | server_url: &str, | |
| 180 | 179 | key: &str, | |
| 181 | 180 | machine_id: &str, | |
| @@ -221,7 +220,7 @@ pub async fn activate_key( | |||
| 221 | 220 | ||
| 222 | 221 | /// Best-effort deactivation failure. The caller logs; no user-facing copy. | |
| 223 | 222 | #[derive(Error, Debug)] | |
| 224 | - | pub enum DeactivationError { | |
| 223 | + | pub(crate) enum DeactivationError { | |
| 225 | 224 | #[error("HTTP client error: {0}")] | |
| 226 | 225 | ClientBuild(reqwest::Error), | |
| 227 | 226 | #[error("Network error: {0}")] | |
| @@ -235,7 +234,7 @@ pub enum DeactivationError { | |||
| 235 | 234 | } | |
| 236 | 235 | ||
| 237 | 236 | /// Deactivate a license key (best-effort, fire-and-forget). | |
| 238 | - | pub async fn deactivate_key( | |
| 237 | + | pub(crate) async fn deactivate_key( | |
| 239 | 238 | server_url: &str, | |
| 240 | 239 | key: &str, | |
| 241 | 240 | machine_id: &str, | |
| @@ -275,15 +274,15 @@ pub async fn deactivate_key( | |||
| 275 | 274 | ||
| 276 | 275 | /// Persisted trial state: tracks when the user first launched the app. | |
| 277 | 276 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 278 | - | pub struct TrialState { | |
| 277 | + | pub(crate) struct TrialState { | |
| 279 | 278 | pub first_launch_date: String, | |
| 280 | - | /// Last time the app was launched — used to detect system clock rollback. | |
| 279 | + | /// Last time the app was launched, used to detect system clock rollback. | |
| 281 | 280 | #[serde(default)] | |
| 282 | 281 | pub last_seen_date: Option<String>, | |
| 283 | 282 | } | |
| 284 | 283 | ||
| 285 | 284 | /// Load the trial state from `trial.json` in the config directory. | |
| 286 | - | pub fn load_trial(config_dir: &Path) -> Option<TrialState> { | |
| 285 | + | pub(crate) fn load_trial(config_dir: &Path) -> Option<TrialState> { | |
| 287 | 286 | let path = config_dir.join("trial.json"); | |
| 288 | 287 | let bytes = std::fs::read(&path).ok()?; | |
| 289 | 288 | let state: TrialState = serde_json::from_slice(&bytes).ok()?; | |
| @@ -296,7 +295,7 @@ pub fn load_trial(config_dir: &Path) -> Option<TrialState> { | |||
| 296 | 295 | } | |
| 297 | 296 | ||
| 298 | 297 | /// Save the trial state to `trial.json` in the config directory. | |
| 299 | - | pub fn save_trial(config_dir: &Path, state: &TrialState) -> io::Result<()> { | |
| 298 | + | pub(crate) fn save_trial(config_dir: &Path, state: &TrialState) -> io::Result<()> { | |
| 300 | 299 | let path = config_dir.join("trial.json"); | |
| 301 | 300 | let tmp = config_dir.join("trial.json.tmp"); | |
| 302 | 301 | let json = serde_json::to_string_pretty(state).map_err(io::Error::other)?; | |
| @@ -309,11 +308,11 @@ pub fn save_trial(config_dir: &Path, state: &TrialState) -> io::Result<()> { | |||
| 309 | 308 | /// Uses the furthest-seen elapsed time (max of now-since-first and | |
| 310 | 309 | /// last_seen-since-first). A clock rolled backward therefore cannot *inflate* the | |
| 311 | 310 | /// remaining days (the cheat the monotonic floor blocks), but it also never | |
| 312 | - | /// permanently zeros a genuine trial on a legitimate NTP/DST correction — the old | |
| 311 | + | /// permanently zeros a genuine trial on a legitimate NTP/DST correction, the old | |
| 313 | 312 | /// behavior, which contradicted trial-mode.md ("not DRM… does not lock the user | |
| 314 | 313 | /// out"). A corrupt `first_launch_date` is treated as expired here, but | |
| 315 | 314 | /// [`load_trial`] regenerates such a trial before this is reached. | |
| 316 | - | pub fn trial_days_remaining(trial: &TrialState) -> i64 { | |
| 315 | + | pub(crate) fn trial_days_remaining(trial: &TrialState) -> i64 { | |
| 317 | 316 | let Ok(first) = chrono::DateTime::parse_from_rfc3339(&trial.first_launch_date) else { | |
| 318 | 317 | return 0; | |
| 319 | 318 | }; | |
| @@ -333,7 +332,7 @@ pub fn trial_days_remaining(trial: &TrialState) -> i64 { | |||
| 333 | 332 | } | |
| 334 | 333 | ||
| 335 | 334 | /// Update the last_seen_date to now. Call on each app launch. | |
| 336 | - | pub fn touch_trial(config_dir: &std::path::Path) { | |
| 335 | + | pub(crate) fn touch_trial(config_dir: &std::path::Path) { | |
| 337 | 336 | if let Some(mut trial) = load_trial(config_dir) { | |
| 338 | 337 | trial.last_seen_date = Some(chrono::Utc::now().to_rfc3339()); | |
| 339 | 338 | let _ = save_trial(config_dir, &trial); | |
| @@ -499,7 +498,7 @@ mod tests { | |||
| 499 | 498 | last_seen_date: Some(seen.to_rfc3339()), | |
| 500 | 499 | }; | |
| 501 | 500 | // Clock rolled back 10 days: the monotonic floor uses the furthest-seen | |
| 502 | - | // elapsed (10 days), so remaining is 20 — not inflated to a fresh 30 (the | |
| 501 | + | // elapsed (10 days), so remaining is 20, not inflated to a fresh 30 (the | |
| 503 | 502 | // cheat) and not permanently zeroed (the lock-out trial-mode.md forbids). | |
| 504 | 503 | assert_eq!(trial_days_remaining(&state), 20); | |
| 505 | 504 | } |
| @@ -2,14 +2,14 @@ | |||
| 2 | 2 | //! | |
| 3 | 3 | //! Launches an eframe window with the shared egui browser UI and a cpal audio | |
| 4 | 4 | //! output stream for sample preview playback. Requires a valid license key | |
| 5 | - | //! before the browser is accessible — the activation result is cached locally | |
| 5 | + | //! before the browser is accessible, the activation result is cached locally | |
| 6 | 6 | //! so the app works offline after the first activation. | |
| 7 | 7 | //! | |
| 8 | 8 | //! ## Why immediate-mode GUI (egui) instead of Tauri/webview | |
| 9 | 9 | //! | |
| 10 | 10 | //! - **Waveform rendering:** Scrolling and zooming a 10-minute waveform at 60fps needs | |
| 11 | 11 | //! GPU-backed drawing, not DOM layout. egui's painter gives direct control over vertex | |
| 12 | - | //! buffers — no JS/CSS performance cliff for large datasets. | |
| 12 | + | //! buffers, no JS/CSS performance cliff for large datasets. | |
| 13 | 13 | //! - **No JS dependency:** The entire app is a single Rust binary. No Node.js build step, | |
| 14 | 14 | //! no npm dependencies, no webview security surface. | |
| 15 | 15 | //! - **Drag-out FFI:** Native drag-and-drop into DAWs requires platform pasteboard APIs | |
| @@ -77,10 +77,10 @@ fn main() -> eframe::Result<()> { | |||
| 77 | 77 | // bare backtrace on stderr. The default hook still runs after, for the trace. | |
| 78 | 78 | let default_hook = std::panic::take_hook(); | |
| 79 | 79 | std::panic::set_hook(Box::new(move |info| { | |
| 80 | - | let location = info | |
| 81 | - | .location() | |
| 82 | - | .map(|l| format!("{}:{}", l.file(), l.line())) | |
| 83 | - | .unwrap_or_else(|| "unknown".to_string()); | |
| 80 | + | let location = info.location().map_or_else( | |
| 81 | + | || "unknown".to_string(), | |
| 82 | + | |l| format!("{}:{}", l.file(), l.line()), | |
| 83 | + | ); | |
| 84 | 84 | let msg = info | |
| 85 | 85 | .payload() | |
| 86 | 86 | .downcast_ref::<&str>() | |
| @@ -106,7 +106,7 @@ fn main() -> eframe::Result<()> { | |||
| 106 | 106 | let prefs = preferences::Preferences::load(&config_dir); | |
| 107 | 107 | ||
| 108 | 108 | // OTA update checker (runs in background on the tokio runtime). The task | |
| 109 | - | // is always spawned but gated on the pref — toggling the About-modal | |
| 109 | + | // is always spawned but gated on the pref, toggling the About-modal | |
| 110 | 110 | // checkbox flips the gate via `set_enabled` without restart. | |
| 111 | 111 | if !prefs.check_for_updates { | |
| 112 | 112 | tracing::info!("Update checks disabled by preferences (toggle in About to enable)"); | |
| @@ -201,7 +201,7 @@ fn create_sync_manager(data_dir: &Path, runtime: &tokio::runtime::Handle) -> Opt | |||
| 201 | 201 | /// Which screen the app is showing. | |
| 202 | 202 | #[derive(Debug, PartialEq)] | |
| 203 | 203 | enum AppScreen { | |
| 204 | - | /// License activation gate — no browser access until a valid key is entered. | |
| 204 | + | /// License activation gate, no browser access until a valid key is entered. | |
| 205 | 205 | Activation, | |
| 206 | 206 | /// First-open vault location picker (shown after activation if no registry exists). | |
| 207 | 207 | VaultSetup, | |
| @@ -214,7 +214,7 @@ enum AppScreen { | |||
| 214 | 214 | /// This is the pure decision logic extracted from `AudioFilesApp::new()` so it | |
| 215 | 215 | /// can be tested without constructing the full app. | |
| 216 | 216 | fn resolve_initial_screen( | |
| 217 | - | vault_registry: &Option<VaultRegistry>, | |
| 217 | + | vault_registry: Option<&VaultRegistry>, | |
| 218 | 218 | license_status: &license::LicenseStatus, | |
| 219 | 219 | has_trial: bool, | |
| 220 | 220 | ) -> AppScreen { | |
| @@ -248,7 +248,7 @@ struct AudioFilesApp { | |||
| 248 | 248 | midi_connection: Option<midi::MidiConnection>, | |
| 249 | 249 | #[cfg_attr(not(target_os = "linux"), allow(dead_code))] | |
| 250 | 250 | gtk_ok: bool, | |
| 251 | - | _runtime: tokio::runtime::Runtime, | |
| 251 | + | runtime: tokio::runtime::Runtime, | |
| 252 | 252 | /// cpal output stream. Kept alive here (dropping it stops audio) and rebuilt | |
| 253 | 253 | /// by `poll_audio_device` when `SharedState::device_lost` fires. `None` when | |
| 254 | 254 | /// no output device is available. | |
| @@ -307,7 +307,8 @@ impl AudioFilesApp { | |||
| 307 | 307 | let has_active_trial = trial_state | |
| 308 | 308 | .as_ref() | |
| 309 | 309 | .is_some_and(|t| license::trial_days_remaining(t) > 0); | |
| 310 | - | let screen = resolve_initial_screen(&vault_registry, &license_status, has_active_trial); | |
| 310 | + | let screen = | |
| 311 | + | resolve_initial_screen(vault_registry.as_ref(), &license_status, has_active_trial); | |
| 311 | 312 | ||
| 312 | 313 | let licensed_or_trial = | |
| 313 | 314 | matches!(&license_status, license::LicenseStatus::Licensed(_)) || has_active_trial; | |
| @@ -373,7 +374,7 @@ impl AudioFilesApp { | |||
| 373 | 374 | show_about: false, | |
| 374 | 375 | midi_connection: None, | |
| 375 | 376 | gtk_ok, | |
| 376 | - | _runtime: runtime, | |
| 377 | + | runtime, | |
| 377 | 378 | audio_stream, | |
| 378 | 379 | audio_retry_at: None, | |
| 379 | 380 | vault_registry, | |
| @@ -435,12 +436,11 @@ impl AudioFilesApp { | |||
| 435 | 436 | /// Initialise the browser after successful activation. | |
| 436 | 437 | fn activate_browser(&mut self) { | |
| 437 | 438 | let _ = std::fs::create_dir_all(&self.data_dir); | |
| 438 | - | self.sync_manager = create_sync_manager(&self.data_dir, self._runtime.handle()); | |
| 439 | - | let vault_name = self | |
| 440 | - | .vault_registry | |
| 441 | - | .as_ref() | |
| 442 | - | .map(|r| vault_setup::vault_name_for_path(r, &self.data_dir)) | |
| 443 | - | .unwrap_or_else(|| "Library".to_string()); | |
| 439 | + | self.sync_manager = create_sync_manager(&self.data_dir, self.runtime.handle()); | |
| 440 | + | let vault_name = self.vault_registry.as_ref().map_or_else( | |
| 441 | + | || "Library".to_string(), | |
| 442 | + | |r| vault_setup::vault_name_for_path(r, &self.data_dir), | |
| 443 | + | ); | |
| 444 | 444 | let (browser, error) = init_browser(&self.data_dir, self.shared.clone(), &vault_name); | |
| 445 | 445 | self.browser = browser; | |
| 446 | 446 | self.error = error; | |
| @@ -497,7 +497,7 @@ fn init_browser( | |||
| 497 | 497 | match BrowserState::new(data_dir, shared, sample_rate, vault_name) { | |
| 498 | 498 | Ok(mut browser) => { | |
| 499 | 499 | // CLI args / OS "Open with": route through the background import | |
| 500 | - | // worker, never the synchronous per-file path — the sync walk here | |
| 500 | + | // worker, never the synchronous per-file path, the sync walk here | |
| 501 | 501 | // ran before the eframe loop started, so the window didn't appear | |
| 502 | 502 | // until the whole tree was hashed (fuzz-2026-07-06 B2). | |
| 503 | 503 | if let Some(vfs_id) = browser.current_vfs_id() { | |
| @@ -523,7 +523,7 @@ fn init_browser( | |||
| 523 | 523 | vfs_id, | |
| 524 | 524 | parent_id, | |
| 525 | 525 | }; | |
| 526 | - | browser.start_files_import(files, strategy); | |
| 526 | + | browser.start_files_import(&files, strategy); | |
| 527 | 527 | } | |
| 528 | 528 | } | |
| 529 | 529 | (Some(browser), None) | |
| @@ -543,7 +543,7 @@ impl eframe::App for AudioFilesApp { | |||
| 543 | 543 | ||
| 544 | 544 | // Apply the audiofiles theme every frame, before any screen draws. The | |
| 545 | 545 | // browser draw path also applies it, but onboarding (Activation / Vault | |
| 546 | - | // setup) renders before a browser exists — without this, first-run paints | |
| 546 | + | // setup) renders before a browser exists, without this, first-run paints | |
| 547 | 547 | // on stock egui dark and then visibly snaps to the theme once the browser | |
| 548 | 548 | // loads. Applying here uses the global theme (the audiofiles default until | |
| 549 | 549 | // the browser loads a saved choice), so every screen is themed from frame 1. | |
| @@ -589,10 +589,10 @@ impl eframe::App for AudioFilesApp { | |||
| 589 | 589 | } | |
| 590 | 590 | } | |
| 591 | 591 | ||
| 592 | - | // About modal — drawn last so it overlays the screen content. | |
| 592 | + | // About modal, drawn last so it overlays the screen content. | |
| 593 | 593 | self.draw_about_modal(ctx); | |
| 594 | 594 | ||
| 595 | - | // Show update notification overlay (bottom-right) — user must consent | |
| 595 | + | // Show update notification overlay (bottom-right), user must consent | |
| 596 | 596 | if self.update_checker.should_show() { | |
| 597 | 597 | let (version, notes, download_url) = { | |
| 598 | 598 | let s = self.update_checker.status.lock(); | |
| @@ -606,7 +606,7 @@ impl eframe::App for AudioFilesApp { | |||
| 606 | 606 | .inner_margin(12.0) | |
| 607 | 607 | .show(ui, |ui| { | |
| 608 | 608 | ui.set_max_width(280.0); | |
| 609 | - | ui.strong(format!("Update Available: v{}", version)); | |
| 609 | + | ui.strong(format!("Update Available: v{version}")); | |
| 610 | 610 | if !notes.is_empty() { | |
| 611 | 611 | ui.label(¬es); | |
| 612 | 612 | } | |
| @@ -718,8 +718,7 @@ impl AudioFilesApp { | |||
| 718 | 718 | let was_active = self | |
| 719 | 719 | .vault_registry | |
| 720 | 720 | .as_ref() | |
| 721 | - | .map(|r| r.active == old_path) | |
| 722 | - | .unwrap_or(false); | |
| 721 | + | .is_some_and(|r| r.active == old_path); | |
| 723 | 722 | let ok = self.with_vault_registry(|reg| { | |
| 724 | 723 | vault::relocate_vault(reg, &old_path, &new_path) | |
| 725 | 724 | }); | |
| @@ -729,7 +728,7 @@ impl AudioFilesApp { | |||
| 729 | 728 | } | |
| 730 | 729 | } | |
| 731 | 730 | VaultAction::ScanStorage => { | |
| 732 | - | // storage_stats() is a single indexed COUNT/SUM — fast | |
| 731 | + | // storage_stats() is a single indexed COUNT/SUM, fast | |
| 733 | 732 | // enough to run inline; no in-progress flag needed (the | |
| 734 | 733 | // old one was set and cleared in the same frame, so its | |
| 735 | 734 | // spinner could never render). | |
| @@ -739,8 +738,7 @@ impl AudioFilesApp { | |||
| 739 | 738 | browser.settings.storage_cache_at = Some( | |
| 740 | 739 | std::time::SystemTime::now() | |
| 741 | 740 | .duration_since(std::time::UNIX_EPOCH) | |
| 742 | - | .map(|d| d.as_secs() as i64) | |
| 743 | - | .unwrap_or(0), | |
| 741 | + | .map_or(0, |d| d.as_secs() as i64), | |
| 744 | 742 | ); | |
| 745 | 743 | } | |
| 746 | 744 | Err(e) => browser.status = format!("Storage scan failed: {e}"), | |
| @@ -824,7 +822,7 @@ impl AudioFilesApp { | |||
| 824 | 822 | if let Some(ref mut browser) = self.browser { | |
| 825 | 823 | if let Some(vfs_id) = browser.current_vfs_id() { | |
| 826 | 824 | let parent_id = browser.nav.current_dir; | |
| 827 | - | // Loose files go to the worker as one batch — dropping hundreds | |
| 825 | + | // Loose files go to the worker as one batch, dropping hundreds | |
| 828 | 826 | // of files used to hash them synchronously on the frame thread | |
| 829 | 827 | // and freeze the window (fuzz-2026-07-06 B3). Directories keep | |
| 830 | 828 | // their structured (folder-preserving) import path. | |
| @@ -845,7 +843,7 @@ impl AudioFilesApp { | |||
| 845 | 843 | vfs_id, | |
| 846 | 844 | parent_id, | |
| 847 | 845 | }; | |
| 848 | - | browser.start_files_import(files, strategy); | |
| 846 | + | browser.start_files_import(&files, strategy); | |
| 849 | 847 | } | |
| 850 | 848 | } | |
| 851 | 849 | audiofiles_browser::editor::draw_browser(ui, browser, self.sync_manager.as_ref()); | |
| @@ -928,11 +926,10 @@ impl AudioFilesApp { | |||
| 928 | 926 | ui.horizontal(|ui| { | |
| 929 | 927 | ui.add_space(ui.available_width() / 2.0 - 200.0); | |
| 930 | 928 | if ui.button("Try again").clicked() { | |
| 931 | - | let vault_name = self | |
| 932 | - | .vault_registry | |
| 933 | - | .as_ref() | |
| 934 | - | .map(|r| vault_setup::vault_name_for_path(r, &self.data_dir)) | |
| 935 | - | .unwrap_or_else(|| "Library".to_string()); | |
| 929 | + | let vault_name = self.vault_registry.as_ref().map_or_else( | |
| 930 | + | || "Library".to_string(), | |
| 931 | + | |r| vault_setup::vault_name_for_path(r, &self.data_dir), | |
| 932 | + | ); | |
| 936 | 933 | let (browser, error) = | |
| 937 | 934 | init_browser(&self.data_dir, self.shared.clone(), &vault_name); | |
| 938 | 935 | self.browser = browser; | |
| @@ -1046,7 +1043,7 @@ fn reveal_folder_label() -> &'static str { | |||
| 1046 | 1043 | } | |
| 1047 | 1044 | } | |
| 1048 | 1045 | ||
| 1049 | - | /// Open `path` in the native file manager. Errors are silently dropped — the | |
| 1046 | + | /// Open `path` in the native file manager. Errors are silently dropped, the | |
| 1050 | 1047 | /// user can fall back to the displayed path string. | |
| 1051 | 1048 | fn reveal_in_file_manager(path: &Path) { | |
| 1052 | 1049 | #[cfg(target_os = "macos")] | |
| @@ -1087,7 +1084,7 @@ mod tests { | |||
| 1087 | 1084 | let reg = Some(make_registry(dir.path())); | |
| 1088 | 1085 | let status = license::LicenseStatus::Licensed(make_license_cache()); | |
| 1089 | 1086 | assert_eq!( | |
| 1090 | - | resolve_initial_screen(®, &status, false), | |
| 1087 | + | resolve_initial_screen(reg.as_ref(), &status, false), | |
| 1091 | 1088 | AppScreen::Browser | |
| 1092 | 1089 | ); | |
| 1093 | 1090 | } | |
| @@ -1096,7 +1093,7 @@ mod tests { | |||
| 1096 | 1093 | fn initial_screen_licensed_without_registry() { | |
| 1097 | 1094 | let status = license::LicenseStatus::Licensed(make_license_cache()); | |
| 1098 | 1095 | assert_eq!( | |
| 1099 | - | resolve_initial_screen(&None, &status, false), | |
| 1096 | + | resolve_initial_screen(None, &status, false), | |
| 1100 | 1097 | AppScreen::VaultSetup | |
| 1101 | 1098 | ); | |
| 1102 | 1099 | } | |
| @@ -1107,7 +1104,7 @@ mod tests { | |||
| 1107 | 1104 | let reg = Some(make_registry(dir.path())); | |
| 1108 | 1105 | let status = license::LicenseStatus::Unlicensed; | |
| 1109 | 1106 | assert_eq!( | |
| 1110 | - | resolve_initial_screen(®, &status, false), | |
| 1107 | + | resolve_initial_screen(reg.as_ref(), &status, false), | |
| 1111 | 1108 | AppScreen::Activation | |
| 1112 | 1109 | ); | |
| 1113 | 1110 | } | |
| @@ -1116,7 +1113,7 @@ mod tests { | |||
| 1116 | 1113 | fn initial_screen_unlicensed_without_registry() { | |
| 1117 | 1114 | let status = license::LicenseStatus::Unlicensed; | |
| 1118 | 1115 | assert_eq!( | |
| 1119 | - | resolve_initial_screen(&None, &status, false), | |
| 1116 | + | resolve_initial_screen(None, &status, false), | |
| 1120 | 1117 | AppScreen::Activation | |
| 1121 | 1118 | ); | |
| 1122 | 1119 | } | |
| @@ -1127,7 +1124,7 @@ mod tests { | |||
| 1127 | 1124 | let reg = Some(make_registry(dir.path())); | |
| 1128 | 1125 | let status = license::LicenseStatus::Unlicensed; | |
| 1129 | 1126 | assert_eq!( | |
| 1130 | - | resolve_initial_screen(®, &status, true), | |
| 1127 | + | resolve_initial_screen(reg.as_ref(), &status, true), | |
| 1131 | 1128 | AppScreen::Browser | |
| 1132 | 1129 | ); | |
| 1133 | 1130 | } | |
| @@ -1136,7 +1133,7 @@ mod tests { | |||
| 1136 | 1133 | fn initial_screen_trial_without_registry() { | |
| 1137 | 1134 | let status = license::LicenseStatus::Unlicensed; | |
| 1138 | 1135 | assert_eq!( | |
| 1139 | - | resolve_initial_screen(&None, &status, true), | |
| 1136 | + | resolve_initial_screen(None, &status, true), | |
| 1140 | 1137 | AppScreen::VaultSetup | |
| 1141 | 1138 | ); | |
| 1142 | 1139 | } | |
| @@ -1169,7 +1166,7 @@ mod tests { | |||
| 1169 | 1166 | #[test] | |
| 1170 | 1167 | fn migrate_license_skips_when_same_dir() { | |
| 1171 | 1168 | let dir = tempfile::tempdir().unwrap(); | |
| 1172 | - | // Should not panic or overwrite — same source and dest | |
| 1169 | + | // Should not panic or overwrite, same source and dest | |
| 1173 | 1170 | vault_setup::migrate_license_to_config(dir.path(), dir.path()); | |
| 1174 | 1171 | } | |
| 1175 | 1172 | ||
| @@ -1193,7 +1190,7 @@ mod tests { | |||
| 1193 | 1190 | fn migrate_license_handles_missing_source() { | |
| 1194 | 1191 | let src = tempfile::tempdir().unwrap(); | |
| 1195 | 1192 | let dst = tempfile::tempdir().unwrap(); | |
| 1196 | - | // No files in source — should not create anything in dest | |
| 1193 | + | // No files in source, should not create anything in dest | |
| 1197 | 1194 | vault_setup::migrate_license_to_config(dst.path(), src.path()); | |
| 1198 | 1195 | assert!(!dst.path().join("license.json").exists()); | |
| 1199 | 1196 | assert!(!dst.path().join("machine_id").exists()); |
| @@ -10,13 +10,13 @@ use thiserror::Error; | |||
| 10 | 10 | use tracing::instrument; | |
| 11 | 11 | ||
| 12 | 12 | /// An active MIDI input connection. Dropping this disconnects. | |
| 13 | - | pub struct MidiConnection { | |
| 13 | + | pub(crate) struct MidiConnection { | |
| 14 | 14 | _conn: MidiInputConnection<()>, | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | 17 | /// Errors from MIDI port enumeration and connection. | |
| 18 | 18 | #[derive(Error, Debug)] | |
| 19 | - | pub enum MidiError { | |
| 19 | + | pub(crate) enum MidiError { | |
| 20 | 20 | #[error("MIDI init: {0}")] | |
| 21 | 21 | Init(#[from] InitError), | |
| 22 | 22 | #[error("MIDI port index {idx} out of range ({count} ports available)")] | |
| @@ -30,7 +30,7 @@ pub enum MidiError { | |||
| 30 | 30 | enum NoteAction { | |
| 31 | 31 | /// Note On with a non-zero velocity. | |
| 32 | 32 | On { note: u8, velocity: u8 }, | |
| 33 | - | /// Note Off (status 0x80, or 0x90 with velocity 0 — running-status note-off). | |
| 33 | + | /// Note Off (status 0x80, or 0x90 with velocity 0, running-status note-off). | |
| 34 | 34 | Off { note: u8 }, | |
| 35 | 35 | /// Not a note message we act on (too short, or a non-note status byte). | |
| 36 | 36 | Ignore, | |
| @@ -71,7 +71,7 @@ fn push_note_event_capped(recent: &mut Vec<MidiNoteEvent>, event: MidiNoteEvent) | |||
| 71 | 71 | ||
| 72 | 72 | /// List available MIDI input port names. | |
| 73 | 73 | #[instrument(skip_all)] | |
| 74 | - | pub fn list_input_ports() -> Vec<String> { | |
| 74 | + | pub(crate) fn list_input_ports() -> Vec<String> { | |
| 75 | 75 | let Ok(midi_in) = MidiInput::new("audiofiles-enumerate") else { | |
| 76 | 76 | return Vec::new(); | |
| 77 | 77 | }; | |
| @@ -88,7 +88,10 @@ pub fn list_input_ports() -> Vec<String> { | |||
| 88 | 88 | /// on the instrument directly (low latency). Also pushes `MidiNoteEvent`s to | |
| 89 | 89 | /// `shared.midi_recent_notes` for the GUI activity display. | |
| 90 | 90 | #[instrument(skip_all)] | |
| 91 | - | pub fn connect(port_index: usize, shared: Arc<SharedState>) -> Result<MidiConnection, MidiError> { | |
| 91 | + | pub(crate) fn connect( | |
| 92 | + | port_index: usize, | |
| 93 | + | shared: Arc<SharedState>, | |
| 94 | + | ) -> Result<MidiConnection, MidiError> { | |
| 92 | 95 | let midi_in = MidiInput::new("audiofiles-input")?; | |
| 93 | 96 | let ports = midi_in.ports(); | |
| 94 | 97 | let port = ports.get(port_index).ok_or(MidiError::PortOutOfRange { | |
| @@ -96,23 +99,22 @@ pub fn connect(port_index: usize, shared: Arc<SharedState>) -> Result<MidiConnec | |||
| 96 | 99 | count: ports.len(), | |
| 97 | 100 | })?; | |
| 98 | 101 | ||
| 99 | - | let shared_cb = shared.clone(); | |
| 100 | 102 | let conn = midi_in.connect( | |
| 101 | 103 | port, | |
| 102 | 104 | "audiofiles-midi-in", | |
| 103 | - | move |_timestamp, message, _| match parse_note_message(message) { | |
| 105 | + | move |_timestamp, message, ()| match parse_note_message(message) { | |
| 104 | 106 | NoteAction::On { note, velocity } => { | |
| 105 | - | shared_cb.instrument.lock().note_on(note, velocity); | |
| 107 | + | shared.instrument.lock().note_on(note, velocity); | |
| 106 | 108 | let event = MidiNoteEvent { | |
| 107 | 109 | note, | |
| 108 | 110 | velocity, | |
| 109 | 111 | note_name: note_name(note), | |
| 110 | 112 | timestamp: Instant::now(), | |
| 111 | 113 | }; | |
| 112 | - | push_note_event_capped(&mut shared_cb.midi_recent_notes.lock(), event); | |
| 114 | + | push_note_event_capped(&mut shared.midi_recent_notes.lock(), event); | |
| 113 | 115 | } | |
| 114 | 116 | NoteAction::Off { note } => { | |
| 115 | - | shared_cb.instrument.lock().note_off(note); | |
| 117 | + | shared.instrument.lock().note_off(note); | |
| 116 | 118 | } | |
| 117 | 119 | NoteAction::Ignore => {} | |
| 118 | 120 | }, | |
| @@ -191,10 +193,10 @@ mod tests { | |||
| 191 | 193 | ||
| 192 | 194 | #[test] | |
| 193 | 195 | fn parse_ignores_non_note_status() { | |
| 194 | - | // 0xB0 = control change, 0xE0 = pitch bend — neither is a note message. | |
| 196 | + | // 0xB0 = control change, 0xE0 = pitch bend, neither is a note message. | |
| 195 | 197 | assert_eq!(parse_note_message(&[0xB0, 7, 127]), NoteAction::Ignore); | |
| 196 | 198 | assert_eq!(parse_note_message(&[0xE0, 0, 64]), NoteAction::Ignore); | |
| 197 | - | // 0xA0 = polyphonic aftertouch — shares note/velocity shape but ignored. | |
| 199 | + | // 0xA0 = polyphonic aftertouch, shares note/velocity shape but ignored. | |
| 198 | 200 | assert_eq!(parse_note_message(&[0xA0, 60, 40]), NoteAction::Ignore); | |
| 199 | 201 | } | |
| 200 | 202 |
| @@ -15,7 +15,7 @@ fn default_check_for_updates() -> bool { | |||
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 18 | - | pub struct Preferences { | |
| 18 | + | pub(crate) struct Preferences { | |
| 19 | 19 | /// Whether the OTA update checker is allowed to contact makenot.work. | |
| 20 | 20 | /// Defaults to `true`. Users on metered networks or in privacy-sensitive | |
| 21 | 21 | /// environments can disable from the About box. | |
| @@ -36,7 +36,7 @@ fn path_for(config_dir: &Path) -> PathBuf { | |||
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | 38 | impl Preferences { | |
| 39 | - | pub fn load(config_dir: &Path) -> Self { | |
| 39 | + | pub(crate) fn load(config_dir: &Path) -> Self { | |
| 40 | 40 | let path = path_for(config_dir); | |
| 41 | 41 | match std::fs::read_to_string(&path) { | |
| 42 | 42 | Ok(s) => serde_json::from_str(&s).unwrap_or_else(|e| { | |
| @@ -47,7 +47,7 @@ impl Preferences { | |||
| 47 | 47 | } | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | - | pub fn save(&self, config_dir: &Path) { | |
| 50 | + | pub(crate) fn save(&self, config_dir: &Path) { | |
| 51 | 51 | let path = path_for(config_dir); | |
| 52 | 52 | match serde_json::to_string_pretty(self) { | |
| 53 | 53 | Ok(s) => { | |
| @@ -103,7 +103,7 @@ mod tests { | |||
| 103 | 103 | #[test] | |
| 104 | 104 | fn missing_check_for_updates_field_defaults_to_true() { | |
| 105 | 105 | let dir = tempfile::tempdir().unwrap(); | |
| 106 | - | std::fs::write(dir.path().join(PREFERENCES_FILENAME), r#"{}"#).unwrap(); | |
| 106 | + | std::fs::write(dir.path().join(PREFERENCES_FILENAME), r"{}").unwrap(); | |
| 107 | 107 | let loaded = Preferences::load(dir.path()); | |
| 108 | 108 | assert!(loaded.check_for_updates); | |
| 109 | 109 | } |
| @@ -4,14 +4,14 @@ use tray_icon::menu::{Menu, MenuEvent, MenuItem, PredefinedMenuItem}; | |||
| 4 | 4 | use tray_icon::{Icon, TrayIcon, TrayIconBuilder}; | |
| 5 | 5 | ||
| 6 | 6 | /// Actions the tray context menu can trigger. | |
| 7 | - | pub enum TrayAction { | |
| 7 | + | pub(crate) enum TrayAction { | |
| 8 | 8 | ShowWindow, | |
| 9 | 9 | TogglePlayback, | |
| 10 | 10 | Quit, | |
| 11 | 11 | } | |
| 12 | 12 | ||
| 13 | 13 | /// Owns the tray icon and tracks menu item IDs for event matching. | |
| 14 | - | pub struct AppTray { | |
| 14 | + | pub(crate) struct AppTray { | |
| 15 | 15 | icon: TrayIcon, | |
| 16 | 16 | show_id: tray_icon::menu::MenuId, | |
| 17 | 17 | toggle_id: tray_icon::menu::MenuId, | |
| @@ -20,7 +20,7 @@ pub struct AppTray { | |||
| 20 | 20 | ||
| 21 | 21 | impl AppTray { | |
| 22 | 22 | /// Create the tray icon and context menu. Must be called on the main thread (macOS). | |
| 23 | - | pub fn new() -> Result<Self, tray_icon::Error> { | |
| 23 | + | pub(crate) fn new() -> Result<Self, tray_icon::Error> { | |
| 24 | 24 | let show = MenuItem::new("Show Window", true, None); | |
| 25 | 25 | let toggle = MenuItem::new("Play / Pause", true, None); | |
| 26 | 26 | let quit = MenuItem::new("Quit", true, None); | |
| @@ -53,7 +53,7 @@ impl AppTray { | |||
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | 55 | /// Poll for menu events. Non-blocking, returns `None` if no event. | |
| 56 | - | pub fn poll(&self) -> Option<TrayAction> { | |
| 56 | + | pub(crate) fn poll(&self) -> Option<TrayAction> { | |
| 57 | 57 | if let Ok(event) = MenuEvent::receiver().try_recv() { | |
| 58 | 58 | if event.id == self.show_id { | |
| 59 | 59 | Some(TrayAction::ShowWindow) | |
| @@ -70,7 +70,7 @@ impl AppTray { | |||
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | 72 | /// Update the tooltip to reflect playback state. | |
| 73 | - | pub fn set_tooltip(&self, text: &str) { | |
| 73 | + | pub(crate) fn set_tooltip(&self, text: &str) { | |
| 74 | 74 | let _ = self.icon.set_tooltip(Some(text)); | |
| 75 | 75 | } | |
| 76 | 76 | } | |
| @@ -136,7 +136,7 @@ mod tests { | |||
| 136 | 136 | } | |
| 137 | 137 | ||
| 138 | 138 | assert_eq!(rgba.len(), SIZE * SIZE * 4); | |
| 139 | - | // Middle bar (index 2) is full height — pixel at (9, 0) should be coloured | |
| 139 | + | // Middle bar (index 2) is full height, pixel at (9, 0) should be coloured | |
| 140 | 140 | // (row 0, col 9) -> offset (0 * SIZE + 9) * 4 | |
| 141 | 141 | let mid_offset = 9 * 4; | |
| 142 | 142 | assert_eq!(&rgba[mid_offset..mid_offset + 4], &bar_colour); |
| @@ -24,7 +24,7 @@ const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); | |||
| 24 | 24 | /// Hard cap on the OTA manifest body. The manifest is a tiny JSON object | |
| 25 | 25 | /// (version + url + notes); anything larger is a misbehaving or compromised | |
| 26 | 26 | /// endpoint. Without this, a 200 from an allowlisted host (or an auto-followed | |
| 27 | - | /// redirect) could stream unbounded bytes into memory — the 15 s request | |
| 27 | + | /// redirect) could stream unbounded bytes into memory, the 15 s request | |
| 28 | 28 | /// timeout bounds *time*, not *size*. | |
| 29 | 29 | const MAX_UPDATE_BODY: usize = 64 * 1024; | |
| 30 | 30 | ||
| @@ -42,7 +42,7 @@ struct UpdateResponse { | |||
| 42 | 42 | ||
| 43 | 43 | /// Read a response body into memory, refusing to buffer more than `cap` bytes. | |
| 44 | 44 | /// Rejects up front on an advertised `Content-Length` over the cap, then streams | |
| 45 | - | /// chunk-by-chunk and aborts the moment the accumulated size would exceed it — | |
| 45 | + | /// chunk-by-chunk and aborts the moment the accumulated size would exceed it, | |
| 46 | 46 | /// so a chunked/streamed response with no (or a lying) length header can't grow | |
| 47 | 47 | /// the buffer without bound. | |
| 48 | 48 | async fn read_capped_body(resp: reqwest::Response, cap: usize) -> Result<Vec<u8>, String> { | |
| @@ -79,14 +79,14 @@ pub struct UpdateStatus { | |||
| 79 | 79 | pub download_url: String, | |
| 80 | 80 | /// The version the user dismissed the banner for, if any. Stored as the | |
| 81 | 81 | /// version string rather than a bool so that dismissing v1.1.0 suppresses | |
| 82 | - | /// only that release — a later v1.2.0 re-surfaces the banner instead of | |
| 82 | + | /// only that release, a later v1.2.0 re-surfaces the banner instead of | |
| 83 | 83 | /// being silently swallowed until app restart. | |
| 84 | 84 | pub dismissed_version: Option<String>, | |
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | 87 | /// Handle to the update checker. Clone-cheap (Arc-wrapped). | |
| 88 | 88 | /// | |
| 89 | - | /// The background loop is always spawned but gated on a `watch` channel — the | |
| 89 | + | /// The background loop is always spawned but gated on a `watch` channel, the | |
| 90 | 90 | /// runtime toggle in the About modal flips the gate without restart. When | |
| 91 | 91 | /// disabled the loop stays parked on `enabled.changed()` (no wake-ups, no | |
| 92 | 92 | /// network), so the cost of "enabled" living in the type is zero at rest. | |
| @@ -94,7 +94,7 @@ pub struct UpdateStatus { | |||
| 94 | 94 | pub struct UpdateChecker { | |
| 95 | 95 | pub status: Arc<Mutex<UpdateStatus>>, | |
| 96 | 96 | /// `None` for the no-runtime / test-only inert checker built by | |
| 97 | - | /// `UpdateChecker::inert()` — calling `set_enabled` on it is a no-op. | |
| 97 | + | /// `UpdateChecker::inert()`: calling `set_enabled` on it is a no-op. | |
| 98 | 98 | enabled_tx: Option<Arc<watch::Sender<bool>>>, | |
| 99 | 99 | } | |
| 100 | 100 | ||
| @@ -107,7 +107,7 @@ impl UpdateChecker { | |||
| 107 | 107 | ||
| 108 | 108 | let status_for_task = status.clone(); | |
| 109 | 109 | runtime.spawn(async move { | |
| 110 | - | // Honor the pref before we even start the initial-delay timer — a | |
| 110 | + | // Honor the pref before we even start the initial-delay timer, a | |
| 111 | 111 | // user who set check_for_updates=false in their JSON shouldn't see | |
| 112 | 112 | // a 10-second network call on launch. | |
| 113 | 113 | if !*rx.borrow() { | |
| @@ -127,7 +127,7 @@ impl UpdateChecker { | |||
| 127 | 127 | } | |
| 128 | 128 | // Sleep until the next interval or until the pref flips, whichever first. | |
| 129 | 129 | tokio::select! { | |
| 130 | - | _ = tokio::time::sleep(std::time::Duration::from_secs(CHECK_INTERVAL_SECS)) => {} | |
| 130 | + | () = tokio::time::sleep(std::time::Duration::from_secs(CHECK_INTERVAL_SECS)) => {} | |
| 131 | 131 | res = rx.changed() => { | |
| 132 | 132 | if res.is_err() { | |
| 133 | 133 | return; | |
| @@ -372,7 +372,7 @@ mod tests { | |||
| 372 | 372 | #[test] | |
| 373 | 373 | fn set_enabled_on_inert_is_noop() { | |
| 374 | 374 | let checker = UpdateChecker::inert(); | |
| 375 | - | // No panic, no observable side-effect — there's no task to gate. | |
| 375 | + | // No panic, no observable side-effect, there's no task to gate. | |
| 376 | 376 | checker.set_enabled(true); | |
| 377 | 377 | checker.set_enabled(false); | |
| 378 | 378 | } | |
| @@ -386,7 +386,7 @@ mod tests { | |||
| 386 | 386 | let checker = UpdateChecker::new(rt.handle(), false); | |
| 387 | 387 | ||
| 388 | 388 | // Subscribe directly to the same channel by cloning the sender's | |
| 389 | - | // receiver — this is the contract the background task relies on. | |
| 389 | + | // receiver, this is the contract the background task relies on. | |
| 390 | 390 | let mut rx = checker | |
| 391 | 391 | .enabled_tx | |
| 392 | 392 | .as_ref() |
| @@ -54,7 +54,7 @@ impl AudioFilesApp { | |||
| 54 | 54 | ui.add_space(theme::space::MD); | |
| 55 | 55 | ||
| 56 | 56 | // Location: shown inline with the picker action. No | |
| 57 | - | // separate "Use default location" button — the default | |
| 57 | + | // separate "Use default location" button, the default | |
| 58 | 58 | // is already the value displayed, and "Continue" | |
| 59 | 59 | // commits whatever is shown here. | |
| 60 | 60 | let chosen_path = self | |
| @@ -86,7 +86,7 @@ impl AudioFilesApp { | |||
| 86 | 86 | ||
| 87 | 87 | ui.add_space(theme::space::XL); | |
| 88 | 88 | ||
| 89 | - | // Single primary action — the only path that advances. | |
| 89 | + | // Single primary action, the only path that advances. | |
| 90 | 90 | if widgets::primary_button(ui, "Continue").clicked() { | |
| 91 | 91 | self.finalize_vault_setup(chosen_path); | |
| 92 | 92 | } | |
| @@ -146,7 +146,7 @@ impl AudioFilesApp { | |||
| 146 | 146 | ||
| 147 | 147 | // Update registry | |
| 148 | 148 | if let Some(ref mut reg) = self.vault_registry { | |
| 149 | - | reg.active = path.clone(); | |
| 149 | + | reg.active.clone_from(&path); | |
| 150 | 150 | if let Err(e) = vault::save_registry(reg) { | |
| 151 | 151 | tracing::error!("Failed to save vault registry: {e}"); | |
| 152 | 152 | } | |
| @@ -233,6 +233,5 @@ pub(crate) fn vault_name_for_path(reg: &VaultRegistry, path: &Path) -> String { | |||
| 233 | 233 | reg.vaults | |
| 234 | 234 | .iter() | |
| 235 | 235 | .find(|v| v.path.canonicalize().unwrap_or_else(|_| v.path.clone()) == canonical) | |
| 236 | - | .map(|v| v.name.clone()) | |
| 237 | - | .unwrap_or_else(|| "Library".to_string()) | |
| 236 | + | .map_or_else(|| "Library".to_string(), |v| v.name.clone()) | |
| 238 | 237 | } |
| @@ -12,3 +12,6 @@ audiofiles-core = { workspace = true, features = ["analysis"] } | |||
| 12 | 12 | serde = { workspace = true } | |
| 13 | 13 | serde_json = { workspace = true } | |
| 14 | 14 | rayon = { workspace = true } | |
| 15 | + | ||
| 16 | + | [lints] | |
| 17 | + | workspace = true |
| @@ -951,7 +951,7 @@ fn bench_persistence_scaling() { | |||
| 951 | 951 | } | |
| 952 | 952 | ||
| 953 | 953 | // Per-row autocommit (the pre-CHRONIC-#2 pattern), for contrast. Capped at | |
| 954 | - | // 1k rows — at 10k the autocommit fsyncs dominate the whole bench's runtime. | |
| 954 | + | // 1k rows, at 10k the autocommit fsyncs dominate the whole bench's runtime. | |
| 955 | 955 | let per_row_n = size.min(1_000); | |
| 956 | 956 | let t = Instant::now(); | |
| 957 | 957 | for i in 0..per_row_n { | |
| @@ -960,7 +960,7 @@ fn bench_persistence_scaling() { | |||
| 960 | 960 | } | |
| 961 | 961 | let per_row_ms = t.elapsed().as_secs_f64() * 1000.0 * (size as f64 / per_row_n as f64); // extrapolate to `size` rows | |
| 962 | 962 | ||
| 963 | - | // Load from DB (I/O) then build the index (CPU) — the full cold-start cost. | |
| 963 | + | // Load from DB (I/O) then build the index (CPU), the full cold-start cost. | |
| 964 | 964 | let t = Instant::now(); | |
| 965 | 965 | let data = SimilarityIndex::load_data(&db).unwrap_or_default(); | |
| 966 | 966 | let _index = SimilarityIndex::build_from_data(data); |
| @@ -43,3 +43,6 @@ objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSWindow", "NSV | |||
| 43 | 43 | [target.'cfg(target_os = "windows")'.dependencies] | |
| 44 | 44 | windows = { version = "0.62", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Memory", "Win32_System_SystemServices", "Win32_Graphics_Gdi", "Win32_Storage_FileSystem"] } | |
| 45 | 45 | windows-core = "0.62" | |
| 46 | + | ||
| 47 | + | [lints] | |
| 48 | + | workspace = true |
| @@ -1,7 +1,10 @@ | |||
| 1 | 1 | //! `DirectBackend` analysis and rules: stored analysis results, batch saves, | |
| 2 | 2 | //! feature backfill queries, and rule application over one or many samples. | |
| 3 | 3 | ||
| 4 | - | use super::*; | |
| 4 | + | use super::{ | |
| 5 | + | AnalysisBackend, AnalysisResult, BackendResult, ConfigBackend, DirectBackend, RulesBackend, | |
| 6 | + | SearchCommand, VfsId, instrument, vfs, | |
| 7 | + | }; | |
| 5 | 8 | use crate::backend::ConfigKey; | |
| 6 | 9 | ||
| 7 | 10 | impl AnalysisBackend for DirectBackend { | |
| @@ -18,11 +21,11 @@ impl AnalysisBackend for DirectBackend { | |||
| 18 | 21 | } | |
| 19 | 22 | let db = self.db.lock(); | |
| 20 | 23 | audiofiles_core::analysis::save_analysis_batch(&db, results)?; | |
| 21 | - | // Invalidate the search worker's cached VP-trees once for the batch — new | |
| 24 | + | // Invalidate the search worker's cached VP-trees once for the batch, new | |
| 22 | 25 | // analysis data changes normalization ranges and may add fingerprints, so | |
| 23 | 26 | // both indexes must rebuild on the next query. | |
| 24 | 27 | if let Some(w) = self.search_worker.lock().as_ref() { | |
| 25 | - | // Fire-and-forget cache drop; if the worker is gone it simply rebuilds | |
| 28 | + | // Fire-and-forget cache drop; if the worker is gone it rebuilds | |
| 26 | 29 | // on next spawn, so a dropped Invalidate is harmless (no busy flag). | |
| 27 | 30 | let _ = w.send(SearchCommand::Invalidate); | |
| 28 | 31 | } |
| @@ -1,7 +1,11 @@ | |||
| 1 | 1 | //! `DirectBackend` search and classification: folder-scoped and global search, | |
| 2 | 2 | //! plus the k-NN classifier head (training, info, and auto-apply). | |
| 3 | 3 | ||
| 4 | - | use super::*; | |
| 4 | + | use super::{ | |
| 5 | + | BackendError, BackendResult, ClassifierBackend, DirectBackend, NodeId, Path, SearchBackend, | |
| 6 | + | SearchCommand, SearchFilter, SimilarityBackend, TrainedHeadInfo, VfsId, VfsNodeWithAnalysis, | |
| 7 | + | WaveformData, instrument, search, | |
| 8 | + | }; | |
| 5 | 9 | ||
| 6 | 10 | impl SearchBackend for DirectBackend { | |
| 7 | 11 | // --- Search --- | |
| @@ -82,7 +86,7 @@ impl ClassifierBackend for DirectBackend { | |||
| 82 | 86 | if let Some(old) = self.classifier_worker.lock().take() { | |
| 83 | 87 | let _ = | |
| 84 | 88 | audiofiles_core::worker_runtime::spawn_detached("classifier-reaper", move || { | |
| 85 | - | drop(old) | |
| 89 | + | drop(old); | |
| 86 | 90 | }); | |
| 87 | 91 | } | |
| 88 | 92 | let handle = crate::classifier_worker::spawn_classifier_job(db_path, job) |
| @@ -1,7 +1,10 @@ | |||
| 1 | 1 | //! `DirectBackend` Sample Forge: chop and conform, in both the synchronous form | |
| 2 | 2 | //! and the worker-backed form that streams progress back to the UI. | |
| 3 | 3 | ||
| 4 | - | use super::*; | |
| 4 | + | use super::{ | |
| 5 | + | BackendError, BackendResult, ChopMethod, ConformResult, ConformTarget, DirectBackend, | |
| 6 | + | ForgeBackend, ForgeCommand, ForgePending, NodeId, VfsId, instrument, | |
| 7 | + | }; | |
| 5 | 8 | ||
| 6 | 9 | impl ForgeBackend for DirectBackend { | |
| 7 | 10 | // --- Sample Forge --- | |
| @@ -118,7 +121,7 @@ impl ForgeBackend for DirectBackend { | |||
| 118 | 121 | ||
| 119 | 122 | // Reuse the persistent worker. Sending Cancel sets the cancel flag so any | |
| 120 | 123 | // in-flight job bails cooperatively; the queued Chop then runs. Dropping | |
| 121 | - | // the old handle here would join its thread ON the GUI thread — a visible | |
| 124 | + | // the old handle here would join its thread ON the GUI thread, a visible | |
| 122 | 125 | // frame stall on a long in-flight job. The worker resets the flag when it | |
| 123 | 126 | // dequeues the new command. | |
| 124 | 127 | { |
| @@ -1,7 +1,11 @@ | |||
| 1 | 1 | //! `DirectBackend` library structure: VFS create/rename/delete, directory | |
| 2 | 2 | //! listing (plain and analysis-enriched), tags, and collections. | |
| 3 | 3 | ||
| 4 | - | use super::*; | |
| 4 | + | use super::{ | |
| 5 | + | BackendResult, Collection, CollectionBackend, CollectionId, DirectBackend, NodeId, | |
| 6 | + | SearchFilter, TagBackend, Vfs, VfsBackend, VfsId, VfsNode, VfsNodeWithAnalysis, collections, | |
| 7 | + | instrument, tags, vfs, | |
| 8 | + | }; | |
| 5 | 9 | ||
| 6 | 10 | impl VfsBackend for DirectBackend { | |
| 7 | 11 | // --- VFS --- |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | //! Direct backend: wraps `Mutex<Database>` + `SampleStore`, calls core functions directly. | |
| 2 | 2 | //! | |
| 3 | - | //! This is the "same as before" implementation — every Backend method delegates | |
| 3 | + | //! This is the "same as before" implementation, every Backend method delegates | |
| 4 | 4 | //! to the corresponding audiofiles-core function. Used in standalone mode, tests, | |
| 5 | 5 | //! and as a reference implementation. | |
| 6 | 6 | ||
| @@ -89,7 +89,7 @@ impl DirectBackend { | |||
| 89 | 89 | /// Create a new DirectBackend from a database and sample store. | |
| 90 | 90 | pub fn new(db: Database, store: SampleStore, data_dir: PathBuf) -> Self { | |
| 91 | 91 | // One-time maintenance: hard-delete tombstones past their retention | |
| 92 | - | // window (docs/design-sample-deletion.md Phase 4). Best-effort — a | |
| 92 | + | // window (docs/design-sample-deletion.md Phase 4). Best-effort, a | |
| 93 | 93 | // failed sweep must never block app start, so errors are logged, not | |
| 94 | 94 | // propagated. Runs against the owned db before it moves into the Mutex. | |
| 95 | 95 | match store.sweep_expired_tombstones(&db) { | |
| @@ -115,7 +115,7 @@ impl DirectBackend { | |||
| 115 | 115 | #[cfg(feature = "device-profiles")] | |
| 116 | 116 | plugin_registry: audiofiles_rhai::create_registry().unwrap_or_else(|e| { | |
| 117 | 117 | // Embedded (include_str!) profiles are compile-checked, so this | |
| 118 | - | // should never trip — but if it does, the conform/M8/MPC wedge | |
| 118 | + | // should never trip, but if it does, the conform/M8/MPC wedge | |
| 119 | 119 | // would silently show "No device profiles". Log so the empty | |
| 120 | 120 | // registry is diagnosable instead of mysterious. | |
| 121 | 121 | tracing::error!( | |
| @@ -215,9 +215,8 @@ impl DirectBackend { | |||
| 215 | 215 | None => return, | |
| 216 | 216 | }; | |
| 217 | 217 | ||
| 218 | - | let plugin = match self.plugin_registry.get(&profile_name) { | |
| 219 | - | Some(p) => p, | |
| 220 | - | None => return, | |
| 218 | + | let Some(plugin) = self.plugin_registry.get(&profile_name) else { | |
| 219 | + | return; | |
| 221 | 220 | }; | |
| 222 | 221 | ||
| 223 | 222 | let profile = &plugin.profile; | |
| @@ -247,7 +246,7 @@ impl DirectBackend { | |||
| 247 | 246 | } | |
| 248 | 247 | ||
| 249 | 248 | // Naming rules | |
| 250 | - | config.naming_rules = profile.naming.clone(); | |
| 249 | + | config.naming_rules.clone_from(&profile.naming); | |
| 251 | 250 | ||
| 252 | 251 | // File size limit | |
| 253 | 252 | config.max_file_size_bytes = profile.limits.as_ref().and_then(|l| l.max_file_size_bytes); | |
| @@ -290,7 +289,7 @@ impl DirectBackend { | |||
| 290 | 289 | // A deliberate `false` from the script rejects the sample. | |
| 291 | 290 | Ok(decision) => decision, | |
| 292 | 291 | // A script *fault* (not a rejection) must not silently shrink | |
| 293 | - | // the export — previously `.unwrap_or(false)` dropped the file | |
| 292 | + | // the export, previously `.unwrap_or(false)` dropped the file | |
| 294 | 293 | // and reported success. Fail open (keep the sample) and surface | |
| 295 | 294 | // the error loudly instead. | |
| 296 | 295 | Err(e) => { |