Skip to main content

max / audiofiles

Adopt lint block, pin stable toolchain, cargo fmt, fix clippy Wire the shared clippy::pedantic block across all members, pin channel=stable, normalize with cargo fmt, and reach green under -D warnings: write! over format!-push (no unwrap, per deny(unwrap_used)), clone_from, let-else, #[must_use], concrete Default types, borrow/by-value where appropriate, saturating_sub for a guarded time subtraction. Scoped #[allow]s (with reasons) for test float compares, closure-signature-bound params, and the worker_runtime step-fn contract. clippy.toml spawn seal and deny(unwrap_used) attributes untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 15:43 UTC
Commit: 4de4d6133deb94ce97d2ae869542db22f9b90a3c
Parent: 587ca59
154 files changed, +1820 insertions, -1635 deletions
M Cargo.toml +32
@@ -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(&notes);
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(&reg, &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(&reg, &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(&reg, &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) => {