Skip to main content

max / audiofiles

Add sync status indicator, SAFETY comments, system tray docs - Toolbar Sync button now shows state: ✓ (synced), ↻ (syncing), (N) pending changes, – (disconnected). Tooltip shows detail. - Add SAFETY comments to statvfs and GetDiskFreeSpaceExW unsafe blocks - Document system tray behavior in help overlay Features tab Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-05 21:16 UTC
Commit: 653a0bcd04b33228585ab055943074fc53409c53
Parent: 469a78c
5 files changed, +97 insertions, -11 deletions
@@ -23,7 +23,7 @@ pub fn draw_browser(
23 23 ctx.request_repaint();
24 24 }
25 25 handle_keyboard(ctx, state);
26 - draw_normal_browser(ctx, state);
26 + draw_normal_browser(ctx, state, sync_manager);
27 27 }
28 28 ImportMode::ConfigureImport { .. } => {
29 29 import_screens::draw_configure_import(ctx, state);
@@ -134,12 +134,16 @@ pub fn draw_browser(
134 134 }
135 135
136 136 /// Draw the main browser layout: toolbar, footer, sidebar, detail panel, and file list.
137 - fn draw_normal_browser(ctx: &egui::Context, state: &mut BrowserState) {
137 + fn draw_normal_browser(
138 + ctx: &egui::Context,
139 + state: &mut BrowserState,
140 + sync_manager: Option<&audiofiles_sync::SyncManager>,
141 + ) {
138 142 // Top toolbar (breadcrumb + search)
139 143 egui::TopBottomPanel::top("toolbar")
140 144 .exact_height(56.0)
141 145 .show(ctx, |ui| {
142 - toolbar::draw_toolbar(ui, state);
146 + toolbar::draw_toolbar(ui, state, sync_manager);
143 147 });
144 148
145 149 // Bottom footer
@@ -16,6 +16,10 @@ fn available_disk_space(path: &Path) -> Option<u64> {
16 16 use std::os::unix::ffi::OsStrExt;
17 17
18 18 let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
19 + // SAFETY: `statvfs` is a POSIX FFI call. `c_path` is a valid NUL-terminated
20 + // C string (from CString::new). `stat` is zero-initialized, which is a valid
21 + // representation for libc::statvfs. The pointer to `stat` is valid for the
22 + // duration of the call.
19 23 unsafe {
20 24 let mut stat: libc::statvfs = std::mem::zeroed();
21 25 if libc::statvfs(c_path.as_ptr(), &mut stat) == 0 {
@@ -31,6 +35,10 @@ fn available_disk_space(path: &Path) -> Option<u64> {
31 35 use std::os::windows::ffi::OsStrExt;
32 36 let wide: Vec<u16> = path.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
33 37 let mut free_bytes: u64 = 0;
38 + // SAFETY: `GetDiskFreeSpaceExW` is a Win32 FFI call. `wide` is a valid
39 + // NUL-terminated UTF-16 string (from encode_wide + chain(once(0))).
40 + // `free_bytes` is a valid aligned u64 for the out-parameter. The pointer
41 + // to `wide` is valid for the duration of the call.
34 42 unsafe {
35 43 if windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
36 44 windows::core::PCWSTR(wide.as_ptr()),
@@ -138,6 +138,10 @@ fn draw_features_tab(ui: &mut egui::Ui) {
138 138
139 139 ui.heading("Cloud Sync");
140 140 ui.label("Sync metadata (tags, organization) across devices. Set up in the Sync panel (toolbar). Metadata sync is free. Blob sync (sample files) is tiered by storage.");
141 + ui.add_space(8.0);
142 +
143 + ui.heading("System Tray");
144 + ui.label("audiofiles runs in the system tray when you close the window. Right-click the tray icon for Show Window and Quit. Playback continues in the background while minimized.");
141 145 });
142 146 }
143 147
@@ -6,9 +6,13 @@ use crate::state::BrowserState;
6 6 use crate::ui::theme;
7 7
8 8 /// Draw the breadcrumb bar with VFS selector, path segments, search bar, and import button.
9 - pub fn draw_toolbar(ui: &mut egui::Ui, state: &mut BrowserState) {
9 + pub fn draw_toolbar(
10 + ui: &mut egui::Ui,
11 + state: &mut BrowserState,
12 + sync_manager: Option<&audiofiles_sync::SyncManager>,
13 + ) {
10 14 ui.horizontal(|ui| {
11 - draw_breadcrumb(ui, state);
15 + draw_breadcrumb(ui, state, sync_manager);
12 16 });
13 17
14 18 // Similarity mode banner
@@ -206,7 +210,11 @@ pub fn draw_toolbar(ui: &mut egui::Ui, state: &mut BrowserState) {
206 210 /// segments for each ancestor directory, and a right-aligned Import button.
207 211 ///
208 212 /// Clicking a non-terminal breadcrumb segment navigates to that directory.
209 - fn draw_breadcrumb(ui: &mut egui::Ui, state: &mut BrowserState) {
213 + fn draw_breadcrumb(
214 + ui: &mut egui::Ui,
215 + state: &mut BrowserState,
216 + sync_manager: Option<&audiofiles_sync::SyncManager>,
217 + ) {
210 218 // Logo
211 219 ui.label(
212 220 egui::RichText::new("af/")
@@ -346,8 +354,41 @@ fn draw_breadcrumb(ui: &mut egui::Ui, state: &mut BrowserState) {
346 354 state.start_export_flow(None);
347 355 }
348 356
349 - if ui.button("Sync")
350 - .on_hover_text("Cloud sync settings")
357 + // Sync button with status indicator
358 + let sync_label = if let Some(sync) = sync_manager {
359 + let status = sync.status();
360 + match status.state {
361 + audiofiles_sync::SyncState::Syncing => "Sync \u{21BB}".to_string(), // ↻
362 + audiofiles_sync::SyncState::Ready if status.pending_changes > 0 => {
363 + format!("Sync ({})", status.pending_changes)
364 + }
365 + audiofiles_sync::SyncState::Disconnected => "Sync \u{2013}".to_string(), // –
366 + _ => "Sync \u{2713}".to_string(), // ✓
367 + }
368 + } else {
369 + "Sync".to_string()
370 + };
371 + let sync_tooltip = if let Some(sync) = sync_manager {
372 + let status = sync.status();
373 + match status.state {
374 + audiofiles_sync::SyncState::Syncing => "Syncing...".to_string(),
375 + audiofiles_sync::SyncState::Ready if status.pending_changes > 0 => {
376 + format!("{} pending changes", status.pending_changes)
377 + }
378 + audiofiles_sync::SyncState::Ready => {
379 + match status.last_sync_at {
380 + Some(ref t) => format!("Synced: {t}"),
381 + None => "Connected, not yet synced".to_string(),
382 + }
383 + }
384 + audiofiles_sync::SyncState::Disconnected => "Not connected".to_string(),
385 + _ => "Cloud sync settings".to_string(),
386 + }
387 + } else {
388 + "Cloud sync settings".to_string()
389 + };
390 + if ui.button(&sync_label)
391 + .on_hover_text(&sync_tooltip)
351 392 .clicked()
352 393 {
353 394 state.sync.show_panel = !state.sync.show_panel;
M docs/todo.md +32 -3
@@ -3,7 +3,17 @@
3 3 ## Status
4 4 Done: All pre-beta phases + Phase 11. Active: None. Next: Vocal layer 2, sample forge (phases 10-16).
5 5
6 - v0.4.0. Audit grade A. 773 tests. Discoverability upgraded C+ → A-.
6 + v0.4.0. Audit grade A (Run 20, 2026-05-04). 780 tests. All remediations complete.
7 +
8 + ---
9 +
10 + ## Audit Run 20 (2026-05-04)
11 +
12 + All items resolved:
13 + - Split app/main.rs: activation.rs (198L), vault_setup.rs (218L), main.rs 1296→899L
14 + - Fixed Relaxed → Acquire/Release in analysis/worker.rs (6 atomic ops)
15 + - Added 7 sync tests (download query, upload query, resolve upsert/delete edge cases)
16 + - Aligned import_directory_recursive: added sorting, audio filtering, skipped-dir checks
7 17
8 18 ---
9 19
@@ -179,14 +189,14 @@ Overall grade: B+. Grades: Complexity B, Completeness B, Learnability B+, Discov
179 189 - [ ] Or: add "Recently Deleted" trash section in sidebar with recovery
180 190 - [ ] Bulk duplicate (create copies of selected samples)
181 191 - [x] Copy metadata: apply tags/BPM/key from one sample to selected others
182 - - [ ] Sync status indicator in toolbar: synced/syncing/pending count
192 + - [x] Sync status indicator in toolbar: synced/syncing/pending count — button label shows ✓/↻/count/–, tooltip shows detail
183 193 - [ ] Show sync conflict resolution when two devices edit same sample
184 194 - [ ] Smart folders should be dynamic (re-compute on visit, not static snapshots)
185 195
186 196 ### Documentation
187 197
188 198 - [x] Expand help overlay beyond shortcuts: add "Features" tab with search, filters, collections, tags, import, export
189 - - [ ] Document system tray integration in settings or help
199 + - [x] Document system tray integration in settings or help — added to Features tab in help overlay
190 200 - [x] Show device profile count in export dialog header
191 201
192 202 ## UX Audit Findings (2026-05-03)
@@ -207,6 +217,25 @@ Overall grade: B+. Grades: Complexity B, Completeness B+, Learnability C+, Disco
207 217
208 218 ---
209 219
220 + ## Rust-Fuzz Findings (2026-05-04)
221 +
222 + Rust quality audit: unsafe discipline, memory efficiency, error handling, smart pointers.
223 + Overall grade: A-. Unsafe: CLEAN. Memory: SOME WASTE. Errors: ELEGANT. Pointers: JUSTIFIED.
224 +
225 + ### Must Fix
226 + - [x] [rust-fuzz] `bulk_ops.rs:84-103` — `selected_nodes()` clones full structs; add field-specific accessors that extract ids/hashes without cloning
227 + - [x] [rust-fuzz] `import_workflow.rs:585,598` — full hash Vec cloned to escape `if let` borrow; use `std::mem::replace` to move data out
228 +
229 + ### Should Fix
230 + - [x] [rust-fuzz] `export_screens.rs:19,34` — add `// SAFETY:` comments to `statvfs` and `GetDiskFreeSpaceExW` unsafe blocks
231 + - [ ] [rust-fuzz] `file_list_menus.rs:255,327` — `selected_nodes()` in drag path; iterate indices by reference instead
232 + - [ ] [rust-fuzz] `bulk_ops.rs:248-382` — use `Option::take()` instead of cloning to escape `if let` borrows (4 sites)
233 + - [ ] [rust-fuzz] `sidebar.rs:361` — tag list cloned every frame when search empty; pass reference or cache tree
234 + - [ ] [rust-fuzz] `export/mod.rs:171` — silent row-drop via `.filter_map(|r| r.ok())`; add `tracing::warn!`
235 + - [ ] [rust-fuzz] `vault_setup.rs:195`, `license.rs:247` — silent mkdir/trial-save failures; log warnings
236 +
237 + ---
238 +
210 239 ## Shared Code Extraction (Cross-Project)
211 240 - [ ] Updater UI: extract updater.js from GO/BB into shared module
212 241 - [ ] Saved queries: unify GO saved views, BB query feeds, AF smart folders