Skip to main content

max / goingson

Mount quasi::protocol, so a described screen is reachable at all `src/quasi/mod.rs` defined `protocol()` and nothing called it. Grep for `quasi::protocol` outside the module returned one hit, in a test. So the thirteen described screens compiled under the feature and were reachable from tests only, and every task that said "turn the feature on" was describing a step that did not exist. Both entry points now register the scheme on the builder and fill its state in from `setup`, through quasi 0.47's `Late<AppState>`: the state needs an `AppHandle`, which does not exist until after every scheme is registered. Android is excluded, since its webview cannot read a request body. Nothing navigates there. The window still loads the JS frontend, and each screen moves over as its own flip; this is what those flips flip into. Three things the mount exposed, fixed here rather than left for the first flip to trip over: - The shell asked for one of the five stylesheets index.html links, so a described screen would have drawn without geometry, typography or tables. It now declares the same cascade in the same order, with the layer names. - Nothing served `/static` or `/fonts`. `quasi::assets` is the passthrough, embedding the sheets, the scripts and the three faces, because a bundled app has no directory to read them from. htmx is vendored under `frontend/vendor/` from the copy the MNW server serves; `build.rs` writes the clock script out beside the selection script it already wrote. Hyperscript is not vendored and is switched off rather than 404ing once per document. - The eleven `Outcome` matches in the screen tests predated quasi 0.45's `Suggestions`, so `--features quasi` had not compiled its tests since that bump. 712 pass now.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-21 23:34 UTC
Signed with PGP, not checked
Commit: 052179c01c44ed4c9e15fbdcb2ecff76d48f2515
Parent: 44d489d
22 files changed, +398 insertions, -53 deletions
M Cargo.lock +26 -39
@@ -2621,7 +2621,7 @@
2621 2621 "js-sys",
2622 2622 "log",
2623 2623 "wasm-bindgen",
2624 - "windows-core 0.62.2",
2624 + "windows-core",
2625 2625 ]
2626 2626
2627 2627 [[package]]
@@ -3459,11 +3459,11 @@
3459 3459
3460 3460 [[package]]
3461 3461 name = "makeover-layout"
3462 - version = "0.31.0"
3462 + version = "0.32.0"
3463 3463
3464 3464 [[package]]
3465 3465 name = "makeover-touch"
3466 - version = "0.22.0"
3466 + version = "0.22.1"
3467 3467 dependencies = [
3468 3468 "makeover-geometry",
3469 3469 "makeover-layout",
@@ -3471,7 +3471,7 @@
3471 3471
3472 3472 [[package]]
3473 3473 name = "makeover-tui"
3474 - version = "0.30.0"
3474 + version = "0.31.0"
3475 3475 dependencies = [
3476 3476 "makeover",
3477 3477 "makeover-layout",
@@ -3480,7 +3480,7 @@
3480 3480
3481 3481 [[package]]
3482 3482 name = "makeover-webview"
3483 - version = "0.53.0"
3483 + version = "0.54.0"
3484 3484 dependencies = [
3485 3485 "makeover-geometry",
3486 3486 "makeover-layout",
@@ -4696,7 +4696,7 @@
4696 4696
4697 4697 [[package]]
4698 4698 name = "quasi-http"
4699 - version = "0.45.0"
4699 + version = "0.47.0"
4700 4700 dependencies = [
4701 4701 "form_urlencoded",
4702 4702 "http",
@@ -4705,7 +4705,7 @@
4705 4705
4706 4706 [[package]]
4707 4707 name = "quasi-notifs"
4708 - version = "0.45.0"
4708 + version = "0.47.0"
4709 4709 dependencies = [
4710 4710 "quasi-router",
4711 4711 "synckit-config",
@@ -4713,14 +4713,14 @@
4713 4713
4714 4714 [[package]]
4715 4715 name = "quasi-router"
4716 - version = "0.45.0"
4716 + version = "0.47.0"
4717 4717 dependencies = [
4718 4718 "makeover-layout",
4719 4719 ]
4720 4720
4721 4721 [[package]]
4722 4722 name = "quasi-tauri"
4723 - version = "0.45.0"
4723 + version = "0.47.0"
4724 4724 dependencies = [
4725 4725 "http",
4726 4726 "quasi-http",
@@ -4746,7 +4746,7 @@
4746 4746
4747 4747 [[package]]
4748 4748 name = "quasi-webview"
4749 - version = "0.45.0"
4749 + version = "0.47.0"
4750 4750 dependencies = [
4751 4751 "docengine",
4752 4752 "makeover-layout",
@@ -5981,7 +5981,7 @@
5981 5981
5982 5982 [[package]]
5983 5983 name = "synckit-client"
5984 - version = "0.8.0"
5984 + version = "0.8.1"
5985 5985 dependencies = [
5986 5986 "apple-native-keyring-store",
5987 5987 "argon2",
@@ -6103,7 +6103,7 @@
6103 6103 "unicode-segmentation",
6104 6104 "url",
6105 6105 "windows",
6106 - "windows-core 0.61.2",
6106 + "windows-core",
6107 6107 "windows-version",
6108 6108 "x11-dl",
6109 6109 ]
@@ -7505,7 +7505,7 @@
7505 7505 "webview2-com-macros",
7506 7506 "webview2-com-sys",
7507 7507 "windows",
7508 - "windows-core 0.61.2",
7508 + "windows-core",
7509 7509 "windows-implement",
7510 7510 "windows-interface",
7511 7511 ]
@@ -7529,7 +7529,7 @@
7529 7529 dependencies = [
7530 7530 "thiserror 2.0.20",
7531 7531 "windows",
7532 - "windows-core 0.61.2",
7532 + "windows-core",
7533 7533 ]
7534 7534
7535 7535 [[package]]
@@ -7657,7 +7657,7 @@
7657 7657 checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
7658 7658 dependencies = [
7659 7659 "windows-collections",
7660 - "windows-core 0.61.2",
7660 + "windows-core",
7661 7661 "windows-future",
7662 7662 "windows-link 0.1.3",
7663 7663 "windows-numerics",
@@ -7669,7 +7669,7 @@
7669 7669 source = "registry+https://github.com/rust-lang/crates.io-index"
7670 7670 checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
7671 7671 dependencies = [
7672 - "windows-core 0.61.2",
7672 + "windows-core",
7673 7673 ]
7674 7674
7675 7675 [[package]]
@@ -7685,26 +7685,13 @@
7685 7685 "windows-strings 0.4.2",
7686 7686 ]
7687 7687
7688 - [[package]]
7689 - name = "windows-core"
7690 - version = "0.62.2"
7691 - source = "registry+https://github.com/rust-lang/crates.io-index"
7692 - checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
7693 - dependencies = [
7694 - "windows-implement",
7695 - "windows-interface",
7696 - "windows-link 0.2.1",
7697 - "windows-result 0.4.1",
7698 - "windows-strings 0.5.1",
7699 - ]
7700 -
7701 7688 [[package]]
7702 7689 name = "windows-future"
7703 7690 version = "0.2.1"
7704 7691 source = "registry+https://github.com/rust-lang/crates.io-index"
7705 7692 checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
7706 7693 dependencies = [
7707 - "windows-core 0.61.2",
7694 + "windows-core",
7708 7695 "windows-link 0.1.3",
7709 7696 "windows-threading",
7710 7697 ]
@@ -7762,7 +7749,7 @@
7762 7749 source = "registry+https://github.com/rust-lang/crates.io-index"
7763 7750 checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
7764 7751 dependencies = [
7765 - "windows-core 0.61.2",
7752 + "windows-core",
7766 7753 "windows-link 0.1.3",
7767 7754 ]
7768 7755
@@ -8160,7 +8147,7 @@
8160 8147 "webkit2gtk-sys",
8161 8148 "webview2-com",
8162 8149 "windows",
8163 - "windows-core 0.61.2",
8150 + "windows-core",
8164 8151 "windows-version",
8165 8152 "x11-dl",
8166 8153 ]
@@ -8496,26 +8483,26 @@
8496 8483 "winnow 1.0.4",
8497 8484 ]
8498 8485
8499 - [[patch.unused]]
8500 - name = "makeover-immediate"
8501 - version = "0.30.0"
8502 -
8503 8486 [[patch.unused]]
8504 8487 name = "ops-status"
8505 8488 version = "0.1.0"
8506 8489
8507 8490 [[patch.unused]]
8508 8491 name = "quasi-axum"
8509 - version = "0.45.0"
8492 + version = "0.47.0"
8510 8493
8511 8494 [[patch.unused]]
8512 8495 name = "quasi-basics"
8513 - version = "0.45.0"
8496 + version = "0.47.0"
8514 8497
8515 8498 [[patch.unused]]
8516 8499 name = "quasi-immediate"
8517 - version = "0.45.0"
8500 + version = "0.47.0"
8518 8501
8519 8502 [[patch.unused]]
8520 8503 name = "quasi-store"
8521 8504 version = "0.1.0"
8505 +
8506 + [[patch.unused]]
8507 + name = "makeover-immediate"
8508 + version = "0.31.0"
@@ -38,7 +38,7 @@
38 38 # `quasi` feature. Optional here as well as below so a default build compiles
39 39 # neither: one `dep:` name covers both tables, which is what lets the feature
40 40 # gate the build script and the crate together.
41 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.46", optional = true }
41 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.47", optional = true }
42 42
43 43 [dependencies]
44 44 goingson-core = { workspace = true }
@@ -56,10 +56,10 @@
56 56 # dependency's source even with the feature off -- so a public clone could not
57 57 # build at all against a URL it could not reach. quasi went public that day and
58 58 # the URL moved here with it.
59 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.46", optional = true }
60 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.46", optional = true }
61 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.46", optional = true }
62 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.46", optional = true }
59 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.47", optional = true }
60 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.47", optional = true }
61 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.47", optional = true }
62 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.47", optional = true }
63 63
64 64 # The notification declaration, and it is NOT behind the `quasi` feature: the
65 65 # watcher is desktop app behaviour rather than a described screen, and the
@@ -76,7 +76,7 @@
76 76 # before it has ported its screens cannot take it. `notifications::Notifier` is
77 77 # the eight lines meanwhile, and it goes when the flip makes quasi-router
78 78 # unconditional here.
79 - quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.46", features = ["synckit"] }
79 + quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.47", features = ["synckit"] }
80 80
81 81 # Tauri
82 82 tauri = { workspace = true, features = ["image-png", "devtools"] }
@@ -753,6 +753,13 @@
753 753 )
754 754 .expect("write js/quasi-selection.js");
755 755
756 + // The clock script, for the same reason: the running-timer band is drawn
757 + // from a description and ticks from this, and a stale copy is a band that
758 + // shows the time the screen was served.
759 + #[cfg(feature = "quasi")]
760 + fs::write(frontend.join("js/quasi-clock.js"), quasi_webview::CLOCK_JS)
761 + .expect("write js/quasi-clock.js");
762 +
756 763 // The generated files above cannot drift from SizeClass. The hand-written
757 764 // ones can, so they are checked rather than trusted.
758 765 //
@@ -364,6 +364,20 @@
364 364
365 365 let mut builder = tauri::Builder::default();
366 366
367 + // The described screens, on the same terms as the desktop entry point:
368 + // registered on the builder because a scheme can only be, filled in from
369 + // `setup` because `AppState` needs an `AppHandle` that does not exist yet.
370 + // Not on android, which is the one platform the adapter cannot serve --
371 + // its webview cannot read a request body, so a POST would arrive with its
372 + // form dropped.
373 + #[cfg(all(feature = "quasi", not(target_os = "android")))]
374 + let quasi_state = {
375 + let (protocol, late) = quasi::protocol();
376 + builder =
377 + builder.register_asynchronous_uri_scheme_protocol("quasi", protocol.into_handler());
378 + late
379 + };
380 +
367 381 // Desktop-only plugins
368 382 #[cfg(not(any(target_os = "ios", target_os = "android")))]
369 383 {
@@ -395,7 +409,12 @@
395 409 // Reclaim orphaned attachment blobs before the schedulers spawn
396 410 // and before the UI can create attachments; race-free here.
397 411 blob_gc::reconcile(&state.db, &state.data_dir).await;
398 - app_handle.manage(Arc::new(state));
412 + let state = Arc::new(state);
413 + // Closes the 503 window the deferred protocol opens, before
414 + // anything can put a window up.
415 + #[cfg(all(feature = "quasi", not(target_os = "android")))]
416 + quasi_state.set(Arc::clone(&state));
417 + app_handle.manage(state);
399 418 });
400 419
401 420 // Background services that work on all platforms
@@ -122,6 +122,19 @@
122 122
123 123 let mut builder = tauri::Builder::default();
124 124
125 + // The described screens, at `quasi://localhost/<screen>`. Registered here
126 + // because a scheme can only be registered on the builder, and filled in
127 + // from `setup` below because `AppState` needs an `AppHandle` that does not
128 + // exist yet. Nothing navigates to it: the window still loads the JS
129 + // frontend, and each screen moves over as its own flip.
130 + #[cfg(feature = "quasi")]
131 + let quasi_state = {
132 + let (protocol, late) = goingson_desktop::quasi::protocol();
133 + builder =
134 + builder.register_asynchronous_uri_scheme_protocol("quasi", protocol.into_handler());
135 + late
136 + };
137 +
125 138 // Desktop-only plugins
126 139 #[cfg(not(any(target_os = "ios", target_os = "android")))]
127 140 {
@@ -337,7 +350,12 @@
337 350 // Reclaim orphaned attachment blobs before the schedulers spawn
338 351 // and before the UI can create attachments; race-free here.
339 352 blob_gc::reconcile(&state.db, &state.data_dir).await;
340 - app_handle.manage(Arc::new(state));
353 + let state = Arc::new(state);
354 + // Before anything can serve a window, so the 503 window the
355 + // deferred protocol opens closes here rather than at first use.
356 + #[cfg(feature = "quasi")]
357 + quasi_state.set(Arc::clone(&state));
358 + app_handle.manage(state);
341 359 });
342 360
343 361 // Create shutdown coordination handles
@@ -184,6 +184,7 @@
184 184
185 185 use crate::state::{AppState, DESKTOP_USER_ID};
186 186
187 + pub mod assets;
187 188 pub mod board;
188 189 pub mod contacts;
189 190 pub mod data;
@@ -403,25 +404,50 @@
403 404 emails::routes(router)
404 405 }
405 406
406 - /// The custom protocol serving the screens inside the app.
407 + /// The custom protocol serving the screens inside the app, and the handle its
408 + /// state arrives through.
407 409 ///
408 410 /// `quasi://localhost/projects`. The assets come from the same scheme, which is
409 411 /// the one thing that differs from the same description served over HTTP.
412 + ///
413 + /// # Why the state comes later
414 + ///
415 + /// [`AppState::new`] takes an `AppHandle` to resolve the data directory, and
416 + /// the handle does not exist until tauri's builder runs — which is after every
417 + /// scheme is registered. So this hands back a `Late<AppState>` for `setup` to
418 + /// fill in, and a request in that gap is answered 503 rather than held. See
419 + /// [`quasi_tauri::Late`].
410 420 #[must_use]
411 - pub fn protocol(state: Arc<AppState>) -> quasi_tauri::Protocol<AppState, quasi_webview::Webview> {
412 - quasi_tauri::Protocol::new(
421 + pub fn protocol() -> (
422 + quasi_tauri::Protocol<AppState, quasi_webview::Webview>,
423 + quasi_tauri::Late<AppState>,
424 + ) {
425 + let (protocol, late) = quasi_tauri::Protocol::pending(
413 426 "quasi",
414 427 router(),
415 - state,
416 428 Arc::new(
417 429 quasi_webview::Webview::under("quasi://localhost/static").with_shell(
418 430 quasi_webview::Shell::under("quasi://localhost/static")
431 + // The same order and the same layers index.html declares,
432 + // because it is the same cascade: geometry and typography
433 + // before the sheet that reads their custom properties.
434 + .layered(["base", "components", "responsive"])
435 + .styled("/static/typography.css")
436 + .styled("/static/geometry.css")
437 + .styled("/static/layout.css")
438 + .styled("/static/tables.css")
419 439 .styled("/static/styles.css")
440 + // Not vendored, so asking for it would be one 404 per
441 + // document. Nothing described here uses it yet.
442 + .without_hyperscript()
420 443 // The running-timer band, which belongs to the app rather
421 444 // than to any screen it is drawn over. See
422 445 // [`time_tracking`].
423 446 .with_chrome(time_tracking::chrome()),
424 447 ),
425 448 ),
426 - )
449 + );
450 + // A stylesheet is not a description. This runs before the router and wins,
451 + // so the two address spaces are kept apart: see [`assets`].
452 + (protocol.passthrough(assets::get), late)
427 453 }