Skip to main content

max / audiofiles

Apply rustfmt across the crate Formatting only, no behavior change. cargo check --all-targets passes.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-21 23:46 UTC
Signed with PGP, not checked
Commit: 49d14a242d094dd8e30de2a4f809d5416d057906
Parent: 9f67ff4
142 files changed, +7929 insertions, -4103 deletions
@@ -3,7 +3,7 @@
3 3 use audiofiles_browser::ui::theme;
4 4 use eframe::egui;
5 5
6 - use super::{AudioFilesApp, AppScreen, SYNC_SERVER_URL};
6 + use super::{AppScreen, AudioFilesApp, SYNC_SERVER_URL};
7 7
8 8 impl AudioFilesApp {
9 9 /// Draw the license activation screen.
@@ -103,8 +103,15 @@
103 103
104 104 let can_activate = !self.activating && !self.license_key_input.trim().is_empty();
105 105 ui.horizontal(|ui| {
106 - let button_text = if self.activating { "Activating\u{2026}" } else { "Activate" };
107 - if ui.add_enabled(can_activate, egui::Button::new(button_text)).clicked() {
106 + let button_text = if self.activating {
107 + "Activating\u{2026}"
108 + } else {
109 + "Activate"
110 + };
111 + if ui
112 + .add_enabled(can_activate, egui::Button::new(button_text))
113 + .clicked()
114 + {
108 115 self.start_activation();
109 116 }
110 117 if self.activating {
@@ -139,10 +146,7 @@
139 146 }
140 147
141 148 ui.add_space(theme::space::MD);
142 - ui.hyperlink_to(
143 - "Get a license key",
144 - "https://makenot.work/store/audiofiles",
145 - );
149 + ui.hyperlink_to("Get a license key", "https://makenot.work/store/audiofiles");
146 150
147 151 ui.add_space(theme::space::XL);
148 152 ui.horizontal(|ui| {
@@ -196,16 +200,15 @@
196 200 browser.settings.license_key_masked = Some(mask_key(&cache.key_code));
197 201 browser.settings.trial_days_remaining = None;
198 202 } else if let Some(ref trial) = self.trial_state {
199 - browser.settings.trial_days_remaining = Some(super::license::trial_days_remaining(trial));
203 + browser.settings.trial_days_remaining =
204 + Some(super::license::trial_days_remaining(trial));
200 205 }
201 206 let mid = &self.machine_id;
202 - browser.settings.machine_id = Some(
203 - if mid.len() > 12 {
204 - format!("{}...{}", &mid[..8], &mid[mid.len()-4..])
205 - } else {
206 - mid.clone()
207 - }
208 - );
207 + browser.settings.machine_id = Some(if mid.len() > 12 {
208 + format!("{}...{}", &mid[..8], &mid[mid.len() - 4..])
209 + } else {
210 + mid.clone()
211 + });
209 212 }
210 213 }
211 214
@@ -289,8 +292,8 @@
289 292
290 293 #[cfg(test)]
291 294 mod tests {
292 - use super::*;
293 295 use super::super::license::{ActivationError, TrialState};
296 + use super::*;
294 297
295 298 fn trial_starting_days_ago(days: i64) -> TrialState {
296 299 let start = chrono::Utc::now() - chrono::Duration::days(days);
@@ -304,12 +307,18 @@
304 307
305 308 #[test]
306 309 fn network_error_offers_retry() {
307 - assert_eq!(recovery_affordance(&ActivationError::Network), RecoveryAffordance::Retry);
310 + assert_eq!(
311 + recovery_affordance(&ActivationError::Network),
312 + RecoveryAffordance::Retry
313 + );
308 314 }
309 315
310 316 #[test]
311 317 fn server_error_offers_retry() {
312 - assert_eq!(recovery_affordance(&ActivationError::Server(503)), RecoveryAffordance::Retry);
318 + assert_eq!(
319 + recovery_affordance(&ActivationError::Server(503)),
320 + RecoveryAffordance::Retry
321 + );
313 322 }
314 323
315 324 #[test]
@@ -133,7 +133,10 @@
133 133 fn read_key_file_trims_whitespace() {
134 134 let dir = tempfile::tempdir().unwrap();
135 135 std::fs::write(dir.path().join("sync_api_key"), " key-with-spaces \n").unwrap();
136 - assert_eq!(read_key_file(dir.path()), Some("key-with-spaces".to_string()));
136 + assert_eq!(
137 + read_key_file(dir.path()),
138 + Some("key-with-spaces".to_string())
139 + );
137 140 }
138 141
139 142 #[test]
@@ -5,11 +5,11 @@
5 5 use audiofiles_browser::instrument::render_voices;
6 6 use audiofiles_browser::preview::PreviewPlayback;
7 7 use audiofiles_browser::state::SharedState;
8 - use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
9 8 use cpal::Stream;
9 + use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
10 10 use parking_lot::Mutex;
11 - use tracing::instrument;
12 11 use thiserror::Error;
12 + use tracing::instrument;
13 13
14 14 /// Errors from audio output stream setup.
15 15 #[derive(Error, Debug)]
@@ -33,9 +33,7 @@
33 33 #[instrument(skip_all)]
34 34 pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, String), AudioError> {
35 35 let host = cpal::default_host();
36 - let device = host
37 - .default_output_device()
38 - .ok_or(AudioError::NoDevice)?;
36 + let device = host.default_output_device().ok_or(AudioError::NoDevice)?;
39 37
40 38 let device_name = device
41 39 .description()
@@ -98,47 +96,46 @@
98 96 // recovers instead of going permanently silent.
99 97 let err_shared = Arc::clone(&shared);
100 98
101 - let stream = device
102 - .build_output_stream(
103 - config,
104 - move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
105 - let num_samples = data.len();
99 + let stream = device.build_output_stream(
100 + config,
101 + move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
102 + let num_samples = data.len();
106 103
107 - // Backstop for an unexpectedly large block (pre-sized above to
108 - // avoid allocating here on the steady-state path).
109 - if mix_buf.len() < num_samples {
110 - mix_buf.resize(num_samples, 0.0);
111 - }
112 - let buf = &mut mix_buf[..num_samples];
104 + // Backstop for an unexpectedly large block (pre-sized above to
105 + // avoid allocating here on the steady-state path).
106 + if mix_buf.len() < num_samples {
107 + mix_buf.resize(num_samples, 0.0);
108 + }
109 + let buf = &mut mix_buf[..num_samples];
113 110
114 - // Zero the mix buffer
115 - for s in buf.iter_mut() {
116 - *s = 0.0;
117 - }
111 + // Zero the mix buffer
112 + for s in buf.iter_mut() {
113 + *s = 0.0;
114 + }
118 115
119 - // Fill preview audio
120 - fill_preview(&shared.preview, buf, channels, device_sample_rate);
116 + // Fill preview audio
117 + fill_preview(&shared.preview, buf, channels, device_sample_rate);
121 118
122 - // Fill instrument audio (additive)
123 - if let Some(mut inst) = shared.instrument.try_lock() {
124 - render_voices(&mut inst, buf, channels, device_sample_rate);
125 - }
119 + // Fill instrument audio (additive)
120 + if let Some(mut inst) = shared.instrument.try_lock() {
121 + render_voices(&mut inst, buf, channels, device_sample_rate);
122 + }
126 123
127 - // Convert f32 mix → output format with clamp
128 - for (out, &mix) in data.iter_mut().zip(buf.iter()) {
129 - *out = T::from_sample(mix.clamp(-1.0, 1.0));
130 - }
131 - },
132 - move |err| {
133 - tracing::error!("audio stream error: {err}");
134 - // Signal the app loop to rebuild the stream. Lock-free store only
135 - // — this runs in cpal's error context, never take a lock here.
136 - err_shared
137 - .device_lost
138 - .store(true, std::sync::atomic::Ordering::Relaxed);
139 - },
140 - None,
141 - )?;
124 + // Convert f32 mix → output format with clamp
125 + for (out, &mix) in data.iter_mut().zip(buf.iter()) {
126 + *out = T::from_sample(mix.clamp(-1.0, 1.0));
127 + }
128 + },
129 + move |err| {
130 + tracing::error!("audio stream error: {err}");
131 + // Signal the app loop to rebuild the stream. Lock-free store only
132 + // — this runs in cpal's error context, never take a lock here.
133 + err_shared
134 + .device_lost
135 + .store(true, std::sync::atomic::Ordering::Relaxed);
136 + },
137 + None,
138 + )?;
142 139 Ok(stream)
143 140 }
144 141
@@ -327,10 +324,7 @@
327 324
328 325 #[test]
329 326 fn fill_cpal_resamples_96k_to_48k() {
330 - let playback = make_playback(
331 - vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5],
332 - 96000, true,
333 - );
327 + let playback = make_playback(vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5], 96000, true);
334 328 let mut data = vec![0.0f32; 4];
335 329
336 330 fill_cpal_output(&playback, &mut data, 2, 48000);
@@ -98,8 +98,7 @@
98 98 pub fn save_license(data_dir: &Path, cache: &LicenseCache) -> io::Result<()> {
99 99 let path = data_dir.join("license.json");
100 100 let tmp = data_dir.join("license.json.tmp");
101 - let json = serde_json::to_string_pretty(cache)
102 - .map_err(io::Error::other)?;
101 + let json = serde_json::to_string_pretty(cache).map_err(io::Error::other)?;
103 102 std::fs::write(&tmp, &json)?;
104 103 std::fs::rename(&tmp, &path)
105 104 }
@@ -134,10 +133,22 @@
134 133 impl std::fmt::Display for ActivationError {
135 134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 135 match self {
137 - Self::Network => write!(f, "Couldn't reach the activation server. Check your connection and try again."),
138 - Self::Server(code) => write!(f, "The activation server returned an error ({code}). Try again in a few minutes."),
139 - Self::InvalidKey => write!(f, "We didn't recognise that key. Double-check spelling, or get a new one."),
140 - Self::MachineLimit => write!(f, "This key is already in use on another machine. Deactivate it there first."),
136 + Self::Network => write!(
137 + f,
138 + "Couldn't reach the activation server. Check your connection and try again."
139 + ),
140 + Self::Server(code) => write!(
141 + f,
142 + "The activation server returned an error ({code}). Try again in a few minutes."
143 + ),
144 + Self::InvalidKey => write!(
145 + f,
146 + "We didn't recognise that key. Double-check spelling, or get a new one."
147 + ),
148 + Self::MachineLimit => write!(
149 + f,
150 + "This key is already in use on another machine. Deactivate it there first."
151 + ),
141 152 Self::Other(msg) => write!(f, "{msg}"),
142 153 }
143 154 }
@@ -151,7 +162,8 @@
151 162 let lower = msg.to_lowercase();
152 163 if lower.contains("machine") || lower.contains("already activated") || lower.contains("limit") {
153 164 ActivationError::MachineLimit
154 - } else if lower.contains("invalid") || lower.contains("not found") || lower.contains("unknown") {
165 + } else if lower.contains("invalid") || lower.contains("not found") || lower.contains("unknown")
166 + {
155 167 ActivationError::InvalidKey
156 168 } else {
157 169 ActivationError::Other(msg.to_string())
@@ -163,7 +175,11 @@
163 175 /// Sends the key and machine ID to the server for validation. On success the
164 176 /// server records an activation slot; on failure the returned error is
165 177 /// classified for per-class UI messaging.
166 - pub async fn activate_key(server_url: &str, key: &str, machine_id: &str) -> Result<(), ActivationError> {
178 + pub async fn activate_key(
179 + server_url: &str,
180 + key: &str,
181 + machine_id: &str,
182 + ) -> Result<(), ActivationError> {
167 183 let client = reqwest::Client::builder()
168 184 .timeout(std::time::Duration::from_secs(15))
169 185 .build()
@@ -176,18 +192,13 @@
176 192 label: None,
177 193 };
178 194
179 - let resp = client
180 - .post(&url)
181 - .json(&body)
182 - .send()
183 - .await
184 - .map_err(|e| {
185 - if e.is_timeout() || e.is_connect() || e.is_request() {
186 - ActivationError::Network
187 - } else {
188 - ActivationError::Other(format!("Network error: {e}"))
189 - }
190 - })?;
195 + let resp = client.post(&url).json(&body).send().await.map_err(|e| {
196 + if e.is_timeout() || e.is_connect() || e.is_request() {
197 + ActivationError::Network
198 + } else {
199 + ActivationError::Other(format!("Network error: {e}"))
200 + }
201 + })?;
191 202
192 203 if !resp.status().is_success() {
193 204 return Err(ActivationError::Server(resp.status().as_u16()));
@@ -201,7 +212,9 @@
201 212 if parsed.valid {
202 213 Ok(())
203 214 } else {
204 - let msg = parsed.error.unwrap_or_else(|| "Invalid license key".to_string());
215 + let msg = parsed
216 + .error
217 + .unwrap_or_else(|| "Invalid license key".to_string());
205 218 Err(classify_server_error(&msg))
206 219 }
207 220 }
@@ -222,7 +235,11 @@
222 235 }
223 236
224 237 /// Deactivate a license key (best-effort, fire-and-forget).
225 - pub async fn deactivate_key(server_url: &str, key: &str, machine_id: &str) -> Result<(), DeactivationError> {
238 + pub async fn deactivate_key(
239 + server_url: &str,
240 + key: &str,
241 + machine_id: &str,
242 + ) -> Result<(), DeactivationError> {
226 243 let client = reqwest::Client::builder()
227 244 .timeout(std::time::Duration::from_secs(15))
228 245 .build()
@@ -369,14 +386,20 @@
369 386 #[test]
370 387 fn load_missing_returns_unlicensed() {
371 388 let dir = tempfile::tempdir().unwrap();
372 - assert!(matches!(load_license(dir.path()), LicenseStatus::Unlicensed));
389 + assert!(matches!(
390 + load_license(dir.path()),
391 + LicenseStatus::Unlicensed
392 + ));
373 393 }
374 394
375 395 #[test]
376 396 fn load_corrupt_returns_unlicensed() {
377 397 let dir = tempfile::tempdir().unwrap();
378 398 std::fs::write(dir.path().join("license.json"), "not json{{{").unwrap();
379 - assert!(matches!(load_license(dir.path()), LicenseStatus::Unlicensed));
399 + assert!(matches!(
400 + load_license(dir.path()),
401 + LicenseStatus::Unlicensed
402 + ));
380 403 }
381 404
382 405 #[test]
@@ -114,7 +114,9 @@
114 114 // rebuilt on device loss (see AudioFilesApp::poll_audio_device).
115 115 let initial_stream = match audio::start_output_stream(shared.clone()) {
116 116 Ok((stream, device_rate, device_name)) => {
117 - shared.device_sample_rate.store(device_rate, std::sync::atomic::Ordering::Relaxed);
117 + shared
118 + .device_sample_rate
119 + .store(device_rate, std::sync::atomic::Ordering::Relaxed);
118 120 *shared.preview_device_name.lock() = Some(device_name);
119 121 Some(stream)
120 122 }
@@ -155,7 +157,13 @@
155 157 Box::new(move |cc| {
156 158 audiofiles_browser::ui::theme::setup_fonts(&cc.egui_ctx);
157 159 Ok(Box::new(AudioFilesApp::new(
158 - config_dir, shared, app_tray, update_checker, prefs, runtime, gtk_ok,
160 + config_dir,
161 + shared,
162 + app_tray,
163 + update_checker,
164 + prefs,
165 + runtime,
166 + gtk_ok,
159 167 initial_stream,
160 168 )))
161 169 }),
@@ -165,13 +173,10 @@
165 173 // ── API key persistence ──
166 174
167 175 /// Create a SyncManager from a saved or env-provided API key.
168 - fn create_sync_manager(
169 - data_dir: &Path,
170 - runtime: &tokio::runtime::Handle,
171 - ) -> Option<SyncManager> {
176 + fn create_sync_manager(data_dir: &Path, runtime: &tokio::runtime::Handle) -> Option<SyncManager> {
172 177 let api_key = api_key::load_api_key(data_dir)?;
173 - let server_url = std::env::var("AF_SYNC_SERVER_URL")
174 - .unwrap_or_else(|_| SYNC_SERVER_URL.to_string());
178 + let server_url =
179 + std::env::var("AF_SYNC_SERVER_URL").unwrap_or_else(|_| SYNC_SERVER_URL.to_string());
175 180 let config = SyncKitConfig {
176 181 server_url,
177 182 api_key,
@@ -208,7 +213,8 @@
208 213 license_status: &license::LicenseStatus,
209 214 has_trial: bool,
210 215 ) -> AppScreen {
211 - let licensed_or_trial = matches!(license_status, license::LicenseStatus::Licensed(_)) || has_trial;
216 + let licensed_or_trial =
217 + matches!(license_status, license::LicenseStatus::Licensed(_)) || has_trial;
212 218 match (vault_registry, licensed_or_trial) {
213 219 (Some(_), true) => AppScreen::Browser,
214 220 (Some(_), false) => AppScreen::Activation,
@@ -293,10 +299,13 @@
293 299 }
294 300 };
295 301
296 - let has_active_trial = trial_state.as_ref().is_some_and(|t| license::trial_days_remaining(t) > 0);
302 + let has_active_trial = trial_state
303 + .as_ref()
304 + .is_some_and(|t| license::trial_days_remaining(t) > 0);
297 305 let screen = resolve_initial_screen(&vault_registry, &license_status, has_active_trial);
298 306
299 - let licensed_or_trial = matches!(&license_status, license::LicenseStatus::Licensed(_)) || has_active_trial;
307 + let licensed_or_trial =
308 + matches!(&license_status, license::LicenseStatus::Licensed(_)) || has_active_trial;
300 309
301 310 let (data_dir, browser, error, sync_manager, license_cache) =
302 311 match (&vault_registry, &license_status) {
@@ -305,7 +314,11 @@
305 314 let data_dir = reg.active.clone();
306 315 let _ = std::fs::create_dir_all(&data_dir);
307 316 let sync_manager = create_sync_manager(&data_dir, runtime.handle());
308 - let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir));
317 + let (browser, error) = init_browser(
318 + &data_dir,
319 + shared.clone(),
320 + &vault_setup::vault_name_for_path(reg, &data_dir),
321 + );
309 322 (data_dir, browser, error, sync_manager, Some(cache.clone()))
310 323 }
311 324 // Registry exists, unlicensed but in trial → open the active vault
@@ -313,7 +326,11 @@
313 326 let data_dir = reg.active.clone();
314 327 let _ = std::fs::create_dir_all(&data_dir);
315 328 let sync_manager = create_sync_manager(&data_dir, runtime.handle());
316 - let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir));
329 + let (browser, error) = init_browser(
330 + &data_dir,
331 + shared.clone(),
332 + &vault_setup::vault_name_for_path(reg, &data_dir),
333 + );
317 334 (data_dir, browser, error, sync_manager, None)
318 335 }
319 336 // Registry exists but unlicensed (deactivated and reactivated)
@@ -394,7 +411,9 @@
394 411 self.audio_stream = None;
395 412 match audio::start_output_stream(self.shared.clone()) {
396 413 Ok((stream, rate, name)) => {
397 - self.shared.device_sample_rate.store(rate, Ordering::Relaxed);
414 + self.shared
415 + .device_sample_rate
416 + .store(rate, Ordering::Relaxed);
398 417 *self.shared.preview_device_name.lock() = Some(name);
399 418 self.shared.device_lost.store(false, Ordering::Relaxed);
400 419 self.audio_stream = Some(stream);
@@ -412,7 +431,9 @@
412 431 fn activate_browser(&mut self) {
413 432 let _ = std::fs::create_dir_all(&self.data_dir);
414 433 self.sync_manager = create_sync_manager(&self.data_dir, self._runtime.handle());
415 - let vault_name = self.vault_registry.as_ref()
434 + let vault_name = self
435 + .vault_registry
436 + .as_ref()
416 437 .map(|r| vault_setup::vault_name_for_path(r, &self.data_dir))
417 438 .unwrap_or_else(|| "Library".to_string());
418 439 let (browser, error) = init_browser(&self.data_dir, self.shared.clone(), &vault_name);
@@ -450,8 +471,14 @@
450 471
451 472 /// Create a BrowserState, returning (Some(browser), None) on success or
452 473 /// (None, Some(error)) on failure.
453 - fn init_browser(data_dir: &Path, shared: Arc<SharedState>, vault_name: &str) -> (Option<BrowserState>, Option<String>) {
454 - let sample_rate = shared.device_sample_rate.load(std::sync::atomic::Ordering::Relaxed) as f32;
474 + fn init_browser(
475 + data_dir: &Path,
476 + shared: Arc<SharedState>,
477 + vault_name: &str,
478 + ) -> (Option<BrowserState>, Option<String>) {
479 + let sample_rate = shared
480 + .device_sample_rate
481 + .load(std::sync::atomic::Ordering::Relaxed) as f32;
455 482 match BrowserState::new(data_dir, shared, sample_rate, vault_name) {
456 483 Ok(mut browser) => {
457 484 // CLI args / OS "Open with": route through the background import
@@ -592,114 +619,121 @@
592 619 let ctx = &ctx;
593 620 // Poll tray menu events
594 621 if let Some(ref tray) = self.tray
595 - && let Some(action) = tray.poll() {
596 - match action {
597 - tray::TrayAction::ShowWindow => {
598 - ctx.send_viewport_cmd(ViewportCommand::Focus);
599 - }
600 - tray::TrayAction::TogglePlayback => {
601 - if let Some(ref mut browser) = self.browser {
602 - browser.toggle_preview();
603 - }
604 - }
605 - tray::TrayAction::Quit => {
606 - ctx.send_viewport_cmd(ViewportCommand::Close);
622 + && let Some(action) = tray.poll()
623 + {
624 + match action {
625 + tray::TrayAction::ShowWindow => {
626 + ctx.send_viewport_cmd(ViewportCommand::Focus);
627 + }
628 + tray::TrayAction::TogglePlayback => {
629 + if let Some(ref mut browser) = self.browser {
630 + browser.toggle_preview();
607 631 }
608 632 }
633 + tray::TrayAction::Quit => {
634 + ctx.send_viewport_cmd(ViewportCommand::Close);
635 + }
609 636 }
637 + }
610 638
611 639 // Update tray tooltip based on playback state
612 640 if let Some(ref tray) = self.tray
613 - && let Some(ref browser) = self.browser {
614 - let playing = browser.shared.preview.lock().playing;
615 - if playing {
616 - tray.set_tooltip(&browser.status);
617 - } else {
618 - tray.set_tooltip("audiofiles");
619 - }
641 + && let Some(ref browser) = self.browser
642 + {
643 + let playing = browser.shared.preview.lock().playing;
644 + if playing {
645 + tray.set_tooltip(&browser.status);
646 + } else {
647 + tray.set_tooltip("audiofiles");
620 648 }
649 + }
621 650
622 651 // Check if sync pulled remote changes → refresh browser contents
623 652 if let Some(ref sync) = self.sync_manager
624 - && sync.status().needs_refresh {
625 - if let Some(ref mut browser) = self.browser {
626 - browser.refresh_vfs_list();
627 - browser.refresh_contents();
628 - }
629 - sync.clear_needs_refresh();
653 + && sync.status().needs_refresh
654 + {
655 + if let Some(ref mut browser) = self.browser {
656 + browser.refresh_vfs_list();
657 + browser.refresh_contents();
630 658 }
659 + sync.clear_needs_refresh();
660 + }
631 661
632 662 // ── Sync setup actions (before draw, so UI sees results this frame) ──
633 663 // ── Vault actions ──
634 664 if let Some(ref mut browser) = self.browser
635 - && let Some(action) = browser.settings.pending_action.take() {
636 - use audiofiles_browser::state::VaultAction;
637 - match action {
638 - VaultAction::SwitchVault(path) => {
639 - self.switch_vault(path);
640 - return;
641 - }
642 - VaultAction::CreateVault { name, path, loose_files } => {
643 - let switch_path = path.clone();
644 - if self.with_vault_registry(|reg| vault::create_vault(reg, &name, &path)) {
645 - self.switch_vault(switch_path);
646 - if loose_files
647 - && let Some(ref mut browser) = self.browser {
648 - let _ = browser.backend.set_config("loose_files", "1");
649 - browser.settings.is_loose_files = true;
650 - }
651 - return;
665 + && let Some(action) = browser.settings.pending_action.take()
666 + {
667 + use audiofiles_browser::state::VaultAction;
668 + match action {
669 + VaultAction::SwitchVault(path) => {
670 + self.switch_vault(path);
671 + return;
672 + }
673 + VaultAction::CreateVault {
674 + name,
675 + path,
676 + loose_files,
677 + } => {
678 + let switch_path = path.clone();
679 + if self.with_vault_registry(|reg| vault::create_vault(reg, &name, &path)) {
680 + self.switch_vault(switch_path);
681 + if loose_files && let Some(ref mut browser) = self.browser {
682 + let _ = browser.backend.set_config("loose_files", "1");
683 + browser.settings.is_loose_files = true;
652 684 }
653 - }
654 - VaultAction::AddExistingVault { name, path } => {
655 - self.with_vault_registry(|reg| vault::add_existing_vault(reg, &name, &path));
656 - }
657 - VaultAction::RemoveVault(path) => {
658 - self.with_vault_registry(|reg| vault::remove_vault(reg, &path));
659 - }
660 - VaultAction::RenameVault { path, new_name } => {
661 - self.with_vault_registry(|reg| vault::rename_vault(reg, &path, &new_name));
662 - }
663 - VaultAction::RelocateVault { old_path, new_path } => {
664 - // If we're repointing the active vault, switch to the new
665 - // path so the open browser picks up the new location.
666 - let was_active = self
667 - .vault_registry
668 - .as_ref()
669 - .map(|r| r.active == old_path)
670 - .unwrap_or(false);
671 - let ok = self.with_vault_registry(|reg| {
672 - vault::relocate_vault(reg, &old_path, &new_path)
673 - });
674 - if ok && was_active {
675 - self.switch_vault(new_path);
676 - return;
677 - }
678 - }
679 - VaultAction::ScanStorage => {
680 - // storage_stats() is a single indexed COUNT/SUM — fast
681 - // enough to run inline; no in-progress flag needed (the
682 - // old one was set and cleared in the same frame, so its
683 - // spinner could never render).
684 - match browser.backend.storage_stats() {
685 - Ok(stats) => {
686 - browser.settings.storage_cache = Some(stats);
687 - browser.settings.storage_cache_at = Some(
688 - std::time::SystemTime::now()
689 - .duration_since(std::time::UNIX_EPOCH)
690 - .map(|d| d.as_secs() as i64)
691 - .unwrap_or(0),
692 - );
693 - }
694 - Err(e) => browser.status = format!("Storage scan failed: {e}"),
695 - }
696 - }
697 - VaultAction::DeactivateLicense => {
698 - self.deactivate();
699 685 return;
700 686 }
701 687 }
688 + VaultAction::AddExistingVault { name, path } => {
689 + self.with_vault_registry(|reg| vault::add_existing_vault(reg, &name, &path));
690 + }
691 + VaultAction::RemoveVault(path) => {
692 + self.with_vault_registry(|reg| vault::remove_vault(reg, &path));
693 + }
694 + VaultAction::RenameVault { path, new_name } => {
695 + self.with_vault_registry(|reg| vault::rename_vault(reg, &path, &new_name));
696 + }
697 + VaultAction::RelocateVault { old_path, new_path } => {
698 + // If we're repointing the active vault, switch to the new
699 + // path so the open browser picks up the new location.
700 + let was_active = self
701 + .vault_registry
702 + .as_ref()
703 + .map(|r| r.active == old_path)
704 + .unwrap_or(false);
705 + let ok = self.with_vault_registry(|reg| {
706 + vault::relocate_vault(reg, &old_path, &new_path)
707 + });
708 + if ok && was_active {
709 + self.switch_vault(new_path);
710 + return;
711 + }
712 + }
713 + VaultAction::ScanStorage => {
714 + // storage_stats() is a single indexed COUNT/SUM — fast
715 + // enough to run inline; no in-progress flag needed (the
716 + // old one was set and cleared in the same frame, so its
717 + // spinner could never render).
718 + match browser.backend.storage_stats() {
719 + Ok(stats) => {
720 + browser.settings.storage_cache = Some(stats);
721 + browser.settings.storage_cache_at = Some(
722 + std::time::SystemTime::now()
723 + .duration_since(std::time::UNIX_EPOCH)
724 + .map(|d| d.as_secs() as i64)
725 + .unwrap_or(0),
726 + );
727 + }
728 + Err(e) => browser.status = format!("Storage scan failed: {e}"),
729 + }
730 + }
731 + VaultAction::DeactivateLicense => {
732 + self.deactivate();
733 + return;
734 + }
702 735 }
736 + }
703 737
704 738 // ── VFS Mirror: sync if dirty ──
705 739 if let Some(ref mut browser) = self.browser {
@@ -715,24 +749,24 @@
715 749 MidiAction::RefreshPorts => {
716 750 browser.midi_state.available_ports = midi::list_input_ports();
717 751 }
718 - MidiAction::Connect(idx) => {
719 - match midi::connect(idx, self.shared.clone()) {
720 - Ok(conn) => {
721 - let name = browser.midi_state.available_ports
722 - .get(idx)
723 - .cloned()
724 - .unwrap_or_else(|| format!("Port {idx}"));
725 - browser.midi_state.connected_port = Some(idx);
726 - browser.midi_state.connected_port_name = Some(name);
727 - self.midi_connection = Some(conn);
728 - }
729 - Err(e) => {
730 - tracing::error!("MIDI connect failed: {e}");
731 - browser.midi_state.connected_port = None;
732 - browser.midi_state.connected_port_name = None;
733 - }
752 + MidiAction::Connect(idx) => match midi::connect(idx, self.shared.clone()) {
753 + Ok(conn) => {
754 + let name = browser
755 + .midi_state
756 + .available_ports
757 + .get(idx)
758 + .cloned()
759 + .unwrap_or_else(|| format!("Port {idx}"));
760 + browser.midi_state.connected_port = Some(idx);
761 + browser.midi_state.connected_port_name = Some(name);
762 + self.midi_connection = Some(conn);
734 763 }
735 - }
764 + Err(e) => {
765 + tracing::error!("MIDI connect failed: {e}");
766 + browser.midi_state.connected_port = None;
767 + browser.midi_state.connected_port_name = None;
768 + }
769 + },
736 770 MidiAction::Disconnect => {
737 771 self.midi_connection = None;
738 772 browser.midi_state.connected_port = None;
@@ -937,10 +971,7 @@
937 971 ui.separator();
938 972 ui.add_space(8.0);
939 973 ui.strong("Network");
940 - ui.checkbox(
941 - &mut updated_check_pref,
942 - "Check makenot.work for updates",
943 - );
974 + ui.checkbox(&mut updated_check_pref, "Check makenot.work for updates");
944 975 ui.label(
945 976 egui::RichText::new(
946 977 "When enabled, the app contacts makenot.work on launch and every 6 hours \
@@ -1037,13 +1068,19 @@
1037 1068 let dir = tempfile::tempdir().unwrap();
1038 1069 let reg = Some(make_registry(dir.path()));
1039 1070 let status = license::LicenseStatus::Licensed(make_license_cache());
1040 - assert_eq!(resolve_initial_screen(&reg, &status, false), AppScreen::Browser);
1071 + assert_eq!(
1072 + resolve_initial_screen(&reg, &status, false),
1073 + AppScreen::Browser
1074 + );
1041 1075 }
1042 1076
1043 1077 #[test]
1044 1078 fn initial_screen_licensed_without_registry() {
1045 1079 let status = license::LicenseStatus::Licensed(make_license_cache());
1046 - assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::VaultSetup);
1080 + assert_eq!(
1081 + resolve_initial_screen(&None, &status, false),
1082 + AppScreen::VaultSetup
1083 + );
1047 1084 }
1048 1085
1049 1086 #[test]
@@ -1051,13 +1088,19 @@
1051 1088 let dir = tempfile::tempdir().unwrap();
1052 1089 let reg = Some(make_registry(dir.path()));
1053 1090 let status = license::LicenseStatus::Unlicensed;
1054 - assert_eq!(resolve_initial_screen(&reg, &status, false), AppScreen::Activation);
1091 + assert_eq!(
1092 + resolve_initial_screen(&reg, &status, false),
1093 + AppScreen::Activation
1094 + );
1055 1095 }
1056 1096
1057 1097 #[test]
1058 1098 fn initial_screen_unlicensed_without_registry() {
1059 1099 let status = license::LicenseStatus::Unlicensed;
1060 - assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::Activation);
1100 + assert_eq!(
1101 + resolve_initial_screen(&None, &status, false),
1102 + AppScreen::Activation
1103 + );
1061 1104 }
1062 1105
1063 1106 #[test]
@@ -1065,13 +1108,19 @@
1065 1108 let dir = tempfile::tempdir().unwrap();
1066 1109 let reg = Some(make_registry(dir.path()));
1067 1110 let status = license::LicenseStatus::Unlicensed;
1068 - assert_eq!(resolve_initial_screen(&reg, &status, true), AppScreen::Browser);
1111 + assert_eq!(
1112 + resolve_initial_screen(&reg, &status, true),
1113 + AppScreen::Browser
1114 + );
1069 1115 }
1070 1116
1071 1117 #[test]
1072 1118 fn initial_screen_trial_without_registry() {
1073 1119 let status = license::LicenseStatus::Unlicensed;
1074 - assert_eq!(resolve_initial_screen(&None, &status, true), AppScreen::VaultSetup);
1120 + assert_eq!(
1121 + resolve_initial_screen(&None, &status, true),
1122 + AppScreen::VaultSetup
1123 + );
1075 1124 }
1076 1125
1077 1126 // ── License migration ──
@@ -1080,7 +1129,11 @@
1080 1129 fn migrate_license_copies_files() {
1081 1130 let src = tempfile::tempdir().unwrap();
1082 1131 let dst = tempfile::tempdir().unwrap();
1083 - std::fs::write(src.path().join("license.json"), r#"{"key_code":"k","machine_id":"m","activated_at":"t"}"#).unwrap();
1132 + std::fs::write(
1133 + src.path().join("license.json"),
1134 + r#"{"key_code":"k","machine_id":"m","activated_at":"t"}"#,
1135 + )
1136 + .unwrap();
1084 1137 std::fs::write(src.path().join("machine_id"), "mid-123").unwrap();
1085 1138
1086 1139 vault_setup::migrate_license_to_config(dst.path(), src.path());
@@ -1154,13 +1207,19 @@
1154 1207 fn vault_name_for_path_found() {
1155 1208 let dir = tempfile::tempdir().unwrap();
1156 1209 let reg = make_registry(dir.path());
1157 - assert_eq!(vault_setup::vault_name_for_path(&reg, dir.path()), "Library");
1210 + assert_eq!(
1211 + vault_setup::vault_name_for_path(&reg, dir.path()),
1212 + "Library"
1213 + );
1158 1214 }
1159 1215
1160 1216 #[test]
1161 1217 fn vault_name_for_path_not_found() {
1162 1218 let dir = tempfile::tempdir().unwrap();
1163 1219 let reg = make_registry(dir.path());
1164 - assert_eq!(vault_setup::vault_name_for_path(&reg, Path::new("/nonexistent")), "Library");
1220 + assert_eq!(
1221 + vault_setup::vault_name_for_path(&reg, Path::new("/nonexistent")),
1222 + "Library"
1223 + );
1165 1224 }
1166 1225 }
@@ -91,35 +91,33 @@
91 91 pub fn connect(port_index: usize, shared: Arc<SharedState>) -> Result<MidiConnection, MidiError> {
92 92 let midi_in = MidiInput::new("audiofiles-input")?;
93 93 let ports = midi_in.ports();
94 - let port = ports
95 - .get(port_index)
96 - .ok_or(MidiError::PortOutOfRange { idx: port_index, count: ports.len() })?;
94 + let port = ports.get(port_index).ok_or(MidiError::PortOutOfRange {
95 + idx: port_index,
96 + count: ports.len(),
97 + })?;
97 98
98 99 let shared_cb = shared.clone();
99 - let conn = midi_in
100 - .connect(
101 - port,
102 - "audiofiles-midi-in",
103 - move |_timestamp, message, _| {
104 - match parse_note_message(message) {
105 - NoteAction::On { note, velocity } => {
106 - shared_cb.instrument.lock().note_on(note, velocity);
107 - let event = MidiNoteEvent {
108 - note,
109 - velocity,
110 - note_name: note_name(note),
111 - timestamp: Instant::now(),
112 - };
113 - push_note_event_capped(&mut shared_cb.midi_recent_notes.lock(), event);
114 - }
115 - NoteAction::Off { note } => {
116 - shared_cb.instrument.lock().note_off(note);
117 - }
118 - NoteAction::Ignore => {}
119 - }
120 - },
121 - (),
122 - )?;
100 + let conn = midi_in.connect(
101 + port,
102 + "audiofiles-midi-in",
103 + move |_timestamp, message, _| match parse_note_message(message) {
104 + NoteAction::On { note, velocity } => {
105 + shared_cb.instrument.lock().note_on(note, velocity);
106 + let event = MidiNoteEvent {
107 + note,
108 + velocity,
109 + note_name: note_name(note),
110 + timestamp: Instant::now(),
111 + };
112 + push_note_event_capped(&mut shared_cb.midi_recent_notes.lock(), event);
113 + }
114 + NoteAction::Off { note } => {
115 + shared_cb.instrument.lock().note_off(note);
116 + }
117 + NoteAction::Ignore => {}
118 + },
119 + (),
120 + )?;
123 121
124 122 Ok(MidiConnection { _conn: conn })
125 123 }
@@ -143,7 +141,10 @@
143 141 fn parse_note_on() {
144 142 assert_eq!(
145 143 parse_note_message(&[0x90, 60, 100]),
146 - NoteAction::On { note: 60, velocity: 100 }
144 + NoteAction::On {
145 + note: 60,
146 + velocity: 100
147 + }
147 148 );
148 149 }
149 150
@@ -169,7 +170,10 @@
169 170 // Low nibble is the channel; note-on on channel 15 (0x9F) still parses.
170 171 assert_eq!(
171 172 parse_note_message(&[0x9F, 72, 1]),
172 - NoteAction::On { note: 72, velocity: 1 }
173 + NoteAction::On {
174 + note: 72,
175 + velocity: 1
176 + }
173 177 );
174 178 // Note-off on channel 7 (0x87).
175 179 assert_eq!(
@@ -199,7 +203,10 @@
199 203 // A message longer than 3 bytes still parses off the first three.
200 204 assert_eq!(
201 205 parse_note_message(&[0x90, 64, 80, 0xFF, 0xFF]),
202 - NoteAction::On { note: 64, velocity: 80 }
206 + NoteAction::On {
207 + note: 64,
208 + velocity: 80
209 + }
203 210 );
204 211 }
205 212
@@ -208,11 +215,17 @@
208 215 // Full note range 0..=127.
209 216 assert_eq!(
210 217 parse_note_message(&[0x90, 0, 1]),
211 - NoteAction::On { note: 0, velocity: 1 }
218 + NoteAction::On {
219 + note: 0,
220 + velocity: 1
221 + }
212 222 );
213 223 assert_eq!(
214 224 parse_note_message(&[0x90, 127, 127]),
215 - NoteAction::On { note: 127, velocity: 127 }
225 + NoteAction::On {
226 + note: 127,
227 + velocity: 127
228 + }
216 229 );
217 230 }
218 231