Skip to main content

max / audiofiles

Locate missing sample files asks through Outcome::Locate The warning's Locate act says a folder is wanted and the answer lands on /library/loose-files/located, which does the search. The intent that opened the picker is gone, and the module header no longer calls this a consumer of a gap that closed in quasi 0.60.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-26 00:28 UTC
Signed with PGP, not checked
Commit: 1c76265ff2efb079e1e54d9b8d8f018c820758db
Parent: cb3e08d
5 files changed, +101 insertions, -58 deletions
M Cargo.lock +16 -16
@@ -7554,6 +7554,22 @@
7554 7554 "winnow 1.0.4",
7555 7555 ]
7556 7556
7557 + [[patch.unused]]
7558 + name = "quasi-type"
7559 + version = "0.1.0"
7560 +
7561 + [[patch.unused]]
7562 + name = "kberg"
7563 + version = "0.1.0"
7564 +
7565 + [[patch.unused]]
7566 + name = "ops-status"
7567 + version = "0.1.0"
7568 +
7569 + [[patch.unused]]
7570 + name = "painhours"
7571 + version = "0.1.0"
7572 +
7557 7573 [[patch.unused]]
7558 7574 name = "quasi-axum"
7559 7575 version = "0.62.0"
@@ -7581,19 +7597,3 @@
7581 7597 [[patch.unused]]
7582 7598 name = "quasi-webview"
7583 7599 version = "0.62.0"
7584 -
7585 - [[patch.unused]]
7586 - name = "kberg"
7587 - version = "0.1.0"
7588 -
7589 - [[patch.unused]]
7590 - name = "ops-status"
7591 - version = "0.1.0"
7592 -
7593 - [[patch.unused]]
7594 - name = "painhours"
7595 - version = "0.1.0"
7596 -
7597 - [[patch.unused]]
7598 - name = "quasi-type"
7599 - version = "0.1.0"
@@ -16,19 +16,21 @@
16 16 //! you. Which of the two is right is a question for the eyeball, and neither is
17 17 //! sayable today.
18 18 //!
19 - //! # THE FINDING, fourth consumer: Locate is a host act
19 + //! # Locate asks for a folder, and the gap it was filed under is closed
20 20 //!
21 - //! `Locate missing files...` opens a native folder picker
22 - //! (`state.dialogs.pick_folder`), and `quasi:vocabulary:host-save-location` was
23 - //! the gap that nothing describes one. Same shape as the export destination, the
24 - //! import source and the theme export before it. It is an ordinary act here and
25 - //! the host does what only a host can, which is the same workaround those three
26 - //! took.
21 + //! `Locate missing files...` opens a native folder picker, and this module's
22 + //! header used to call that the fourth consumer of
23 + //! `quasi:vocabulary:host-save-location` — the gap that nothing describes one.
24 + //! `ec92f9cb` closed it in quasi 0.60 with [`Outcome::Locate`], and the pair
25 + //! below is the conversion: [`locate`](fn@locate) says a folder is wanted and
26 + //! [`located`](fn@located) is called with what came back.
27 27 //!
28 - //! The gap is closed: `ec92f9cb` shipped `Outcome::Locate` in quasi 0.60, and
29 - //! the export flip took it at the destination picker. This site has not been
30 - //! converted, so the paragraph above describes what the code does rather than
31 - //! what it should do.
28 + //! The act shape rather than the form shape, which is [`advanced`]'s Import
29 + //! Theme: picking the folder *is* the search, so there is nothing to stash and
30 + //! nothing to confirm afterwards.
31 + //!
32 + //! [`Outcome::Locate`]: quasi_router::Outcome::Locate
33 + //! [`advanced`]: super::advanced
32 34 //!
33 35 //! # Purge says what it takes, on the control
34 36 //!
@@ -40,7 +42,8 @@
40 42
41 43 use quasi_router::layout::{Notice, Tone};
42 44 use quasi_router::{
43 - Act, Action, Node, Outcome, RegionKind, Request, Response, RouteError, Router, Screen, Slot,
45 + Act, Action, Locating, Node, Outcome, RegionKind, Request, Response, RouteError, Router,
46 + Screen, Slot,
44 47 };
45 48
46 49 use super::Panels;
@@ -51,6 +54,9 @@
51 54 /// Where an answered warning goes.
52 55 const BACK: &str = "/";
53 56
57 + /// The name a picked folder comes back under.
58 + const FOLDER: &str = "folder";
59 +
54 60 /// What Purge takes with it, said on the control that does it.
55 61 const BLAST: &str =
56 62 "Tags, analysis results, and history for these samples will be permanently deleted. Purge?";
@@ -61,6 +67,7 @@
61 67 .get("/library/loose-files", screen)
62 68 .post("/library/loose-files/dismiss", dismiss)
63 69 .post("/library/loose-files/locate", locate)
70 + .post("/library/loose-files/located", located)
64 71 .post("/library/loose-files/purge", purge)
65 72 }
66 73
@@ -111,12 +118,27 @@
111 118
112 119 /// `POST /library/loose-files/locate`
113 120 ///
114 - /// The answer leaves before the picker opens, and that is honest rather than
115 - /// hurried: the host's dialog runs on its own and lands in the app whenever the
116 - /// user is done with it, so there is nothing for this screen to wait for. See
117 - /// the module header on why a picker is not described at all.
118 - fn locate(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
119 - state.integrity.locate();
121 + /// Says a folder is wanted and leaves the picker to the host, which is the only
122 + /// half of this a description can hold. The search happens in
123 + /// [`located`](fn@located), on the answer.
124 + fn locate(_state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
125 + Ok(Response::locate(Locating::folder(
126 + "Locate missing sample files",
127 + Action::post("/library/loose-files/located"),
128 + FOLDER,
129 + )))
130 + }
131 +
132 + /// `POST /library/loose-files/located`
133 + ///
134 + /// A reader who backed out of the picker has answered nothing: `ui::dialog`
135 + /// skips its handler on an empty result, so this is the second guard rather than
136 + /// the only one, and it is here because the address is reachable by typing.
137 + fn located(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
138 + let folder = request.payload.get(FOLDER).unwrap_or_default();
139 + if !folder.is_empty() {
140 + state.integrity.locate(folder);
141 + }
120 142 Ok(Response::from(Outcome::Goto(Action::get(BACK))))
121 143 }
122 144
@@ -941,8 +941,8 @@
941 941 DismissLooseFiles,
942 942 /// Delete the registry entries whose files are gone.
943 943 PurgeLooseFiles,
944 - /// Ask the host for a folder to look for the missing files in.
945 - LocateLooseFiles,
944 + /// Look for the missing files in this folder, which the reader picked.
945 + LocateLooseFilesIn(std::path::PathBuf),
946 946 /// Bring a tombstoned sample back.
947 947 RestoreSample(String),
948 948 /// Put this machine's id on the clipboard.
@@ -4304,10 +4304,10 @@
4304 4304
4305 4305 /// The vault's own health, as much as the warning needs.
4306 4306 ///
4307 - /// The eleventh narrow trait. Every write is an intent, and the third of them is
4308 - /// the interesting one: locating the missing files opens a native folder picker,
4309 - /// which is a host act with no described step. See [`integrity`]'s header — it is
4310 - /// the **fourth consumer** of `quasi:vocabulary:host-save-location`.
4307 + /// The eleventh narrow trait. Every write is an intent, and the third of them
4308 + /// carries a folder: the description asks for one with
4309 + /// [`Outcome::Locate`](quasi_router::Outcome::Locate) and this is called with
4310 + /// what came back. See [`integrity`]'s header.
4311 4311 pub trait Integrity {
4312 4312 /// How many samples have lost the file they point at.
4313 4313 fn missing(&self) -> usize;
@@ -4315,8 +4315,8 @@
4315 4315 /// Put the warning away without acting.
4316 4316 fn dismiss(&self);
4317 4317
4318 - /// Go looking for the files.
4319 - fn locate(&self);
4318 + /// Go looking for the files in this folder.
4319 + fn locate(&self, folder: &str);
4320 4320
4321 4321 /// Delete the entries whose files are gone.
4322 4322 fn purge(&self);
@@ -4339,8 +4339,8 @@
4339 4339 self.push(Intent::DismissLooseFiles);
4340 4340 }
4341 4341
4342 - fn locate(&self) {
4343 - self.push(Intent::LocateLooseFiles);
4342 + fn locate(&self, folder: &str) {
4343 + self.push(Intent::LocateLooseFilesIn(std::path::PathBuf::from(folder)));
4344 4344 }
4345 4345
4346 4346 fn purge(&self) {
@@ -1392,15 +1392,10 @@
1392 1392 Intent::Rescan => state.classifier_review_library(),
1393 1393 Intent::CloseReview => state.close_review_screen(),
1394 1394 Intent::BeginExport => state.start_export_flow(None),
1395 - // The host act with no described step. See `integrity`'s header:
1396 - // fourth consumer of `quasi:vocabulary:host-save-location`.
1397 - Intent::LocateLooseFiles => {
1398 - state
1399 - .dialogs
1400 - .pick_folder("Locate missing sample files", |state, folder| {
1401 - state.locate_missing_loose_files(&folder);
1402 - });
1403 - }
1395 + // The picker is `locate` above rather than anything here: the
1396 + // warning asks with `Outcome::Locate` and this lands with the
1397 + // folder that came back.
1398 + Intent::LocateLooseFilesIn(folder) => state.locate_missing_loose_files(&folder),
1404 1399 // The Storage section. Every one of these is the shipped section's
1405 1400 // own write: `VaultAction` is the queue the app layer already drains
1406 1401 // each frame, so the described side asks for exactly what the egui
@@ -5794,7 +5794,7 @@
5794 5794 }
5795 5795
5796 5796 fn dismiss(&self) {}
5797 - fn locate(&self) {}
5797 + fn locate(&self, _folder: &str) {}
5798 5798 fn purge(&self) {}
5799 5799 }
5800 5800
@@ -6597,8 +6597,8 @@
6597 6597 self.asked.borrow_mut().push("dismiss".to_owned());
6598 6598 }
6599 6599
6600 - fn locate(&self) {
6601 - self.asked.borrow_mut().push("locate".to_owned());
6600 + fn locate(&self, folder: &str) {
6601 + self.asked.borrow_mut().push(format!("locate {folder}"));
6602 6602 }
6603 6603
6604 6604 fn purge(&self) {
@@ -6668,10 +6668,9 @@
6668 6668 }
6669 6669
6670 6670 #[test]
6671 - fn each_of_the_three_answers_does_one_thing_and_leaves() {
6671 + fn each_of_the_two_answers_does_one_thing_and_leaves() {
6672 6672 for (address, expected) in [
6673 6673 ("/library/loose-files/dismiss", "dismiss"),
6674 - ("/library/loose-files/locate", "locate"),
6675 6674 ("/library/loose-files/purge", "purge"),
6676 6675 ] {
6677 6676 let vault = FakeIntegrity::missing(3);
@@ -6684,6 +6683,33 @@
6684 6683 }
6685 6684 }
6686 6685
6686 + #[test]
6687 + fn locating_asks_the_host_for_a_folder_and_searches_it_on_the_answer() {
6688 + // The act shape of `Outcome::Locate`: picking the folder *is* the search,
6689 + // so the call that lands does the searching rather than stashing a path.
6690 + let vault = FakeIntegrity::missing(3);
6691 + let response = checking(&vault, Request::post("/library/loose-files/locate")).unwrap();
6692 + let Outcome::Locate(asking) = &response.outcome else {
6693 + panic!("expected an ask, got {:?}", response.outcome);
6694 + };
6695 + assert_eq!(asking.answers.route(), Some("/library/loose-files/located"));
6696 + assert_eq!(asking.sought, quasi_router::Sought::Folder);
6697 + assert!(vault.asked().is_empty(), "the ask went looking");
6698 +
6699 + let answered = Request::post("/library/loose-files/located")
6700 + .sending(Params::new().with("folder".to_owned(), "/mnt/samples".to_owned()));
6701 + let response = checking(&vault, answered).unwrap();
6702 + assert_eq!(vault.asked(), ["locate /mnt/samples"]);
6703 + assert!(matches!(response.outcome, Outcome::Goto(_)), "{response:?}");
6704 + }
6705 +
6706 + #[test]
6707 + fn backing_out_of_the_locate_picker_searches_nothing() {
6708 + let vault = FakeIntegrity::missing(3);
6709 + checking(&vault, Request::post("/library/loose-files/located")).unwrap();
6710 + assert!(vault.asked().is_empty());
6711 + }
6712 +
6687 6713 #[test]
6688 6714 fn a_healthy_vault_has_no_warning_to_open() {
6689 6715 assert!(checking(&Sound, Request::get("/library/loose-files")).is_err());