Skip to main content

max / goingson

Take quasi 0.89.0: delivery is the framework's adapter now quasicoherent c231ff13. notifications::Notifier was eight lines that existed only because the framework's copy lived in quasi-tauri, which takes quasi-router unconditionally. It is quasi-notifs' now, behind that crate's tauri feature, so taking it costs the notification plugin this app already links and nothing else. Two things the hand-written copy did that the framework's does not: a debug line per delivery, and a warning per refusal. The first goes; the second is kept where it means something, as a count read off the adapter once a tick. A watcher the platform refuses every time has a permission problem rather than a quiet user, and Notifier::refused exists to say so. send_notification keeps NotificationExt for itself. It is the one path with no declared kind -- new mail is a per-account switch, which quasi_notifs' header rules is the app's own -- so it has no Occurrence to hand a Notifier. The Cargo.toml note that filed this against quasi came out with it. It had also gone stale in a second way: it argued from the `quasi` feature keeping the router out of a default build, and that feature was removed on 2026-08-22.
Author: Max Johnson <me@maxj.phd> · 2026-08-30 21:28 UTC
Signed with PGP, not checked
Commit: a8c3ad8adaae190552d2631e4099083d54ca5db3
Parent: 4de9270
3 files changed, +71 insertions, -81 deletions
M Cargo.lock +22 -20
@@ -1387,7 +1387,7 @@
1387 1387 "libc",
1388 1388 "option-ext",
1389 1389 "redox_users",
1390 - "windows-sys 0.60.2",
1390 + "windows-sys 0.61.2",
1391 1391 ]
1392 1392
1393 1393 [[package]]
@@ -1628,7 +1628,7 @@
1628 1628 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1629 1629 dependencies = [
1630 1630 "libc",
1631 - "windows-sys 0.60.2",
1631 + "windows-sys 0.61.2",
1632 1632 ]
1633 1633
1634 1634 [[package]]
@@ -3650,7 +3650,7 @@
3650 3650 "png 0.18.1",
3651 3651 "serde",
3652 3652 "thiserror 2.0.20",
3653 - "windows-sys 0.60.2",
3653 + "windows-sys 0.61.2",
3654 3654 ]
3655 3655
3656 3656 [[package]]
@@ -3783,7 +3783,7 @@
3783 3783 source = "registry+https://github.com/rust-lang/crates.io-index"
3784 3784 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
3785 3785 dependencies = [
3786 - "windows-sys 0.60.2",
3786 + "windows-sys 0.61.2",
3787 3787 ]
3788 3788
3789 3789 [[package]]
@@ -4717,7 +4717,7 @@
4717 4717
4718 4718 [[package]]
4719 4719 name = "quasi-http"
4720 - version = "0.88.0"
4720 + version = "0.89.0"
4721 4721 dependencies = [
4722 4722 "form_urlencoded",
4723 4723 "http",
@@ -4726,22 +4726,24 @@
4726 4726
4727 4727 [[package]]
4728 4728 name = "quasi-notifs"
4729 - version = "0.88.0"
4729 + version = "0.89.0"
4730 4730 dependencies = [
4731 4731 "quasi-router",
4732 4732 "synckit-config",
4733 + "tauri",
4734 + "tauri-plugin-notification",
4733 4735 ]
4734 4736
4735 4737 [[package]]
4736 4738 name = "quasi-router"
4737 - version = "0.88.0"
4739 + version = "0.89.0"
4738 4740 dependencies = [
4739 4741 "makeover-layout",
4740 4742 ]
4741 4743
4742 4744 [[package]]
4743 4745 name = "quasi-tauri"
4744 - version = "0.88.0"
4746 + version = "0.89.0"
4745 4747 dependencies = [
4746 4748 "http",
4747 4749 "quasi-http",
@@ -4767,7 +4769,7 @@
4767 4769
4768 4770 [[package]]
4769 4771 name = "quasi-webview"
4770 - version = "0.88.0"
4772 + version = "0.89.0"
4771 4773 dependencies = [
4772 4774 "docengine",
4773 4775 "makeover-layout",
@@ -5206,7 +5208,7 @@
5206 5208 "errno",
5207 5209 "libc",
5208 5210 "linux-raw-sys",
5209 - "windows-sys 0.60.2",
5211 + "windows-sys 0.61.2",
5210 5212 ]
5211 5213
5212 5214 [[package]]
@@ -5263,7 +5265,7 @@
5263 5265 "security-framework",
5264 5266 "security-framework-sys",
5265 5267 "webpki-root-certs",
5266 - "windows-sys 0.60.2",
5268 + "windows-sys 0.61.2",
5267 5269 ]
5268 5270
5269 5271 [[package]]
@@ -5803,7 +5805,7 @@
5803 5805 checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
5804 5806 dependencies = [
5805 5807 "libc",
5806 - "windows-sys 0.60.2",
5808 + "windows-sys 0.61.2",
5807 5809 ]
5808 5810
5809 5811 [[package]]
@@ -6563,7 +6565,7 @@
6563 6565 "getrandom 0.4.3",
6564 6566 "once_cell",
6565 6567 "rustix",
6566 - "windows-sys 0.60.2",
6568 + "windows-sys 0.61.2",
6567 6569 ]
6568 6570
6569 6571 [[package]]
@@ -6585,7 +6587,7 @@
6585 6587 "parking_lot",
6586 6588 "rustix",
6587 6589 "signal-hook",
6588 - "windows-sys 0.60.2",
6590 + "windows-sys 0.61.2",
6589 6591 ]
6590 6592
6591 6593 [[package]]
@@ -7067,7 +7069,7 @@
7067 7069 "png 0.18.1",
7068 7070 "serde",
7069 7071 "thiserror 2.0.20",
7070 - "windows-sys 0.60.2",
7072 + "windows-sys 0.61.2",
7071 7073 ]
7072 7074
7073 7075 [[package]]
@@ -7102,7 +7104,7 @@
7102 7104 dependencies = [
7103 7105 "memoffset",
7104 7106 "tempfile",
7105 - "windows-sys 0.60.2",
7107 + "windows-sys 0.61.2",
7106 7108 ]
7107 7109
7108 7110 [[package]]
@@ -7632,7 +7634,7 @@
7632 7634 source = "registry+https://github.com/rust-lang/crates.io-index"
7633 7635 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
7634 7636 dependencies = [
7635 - "windows-sys 0.60.2",
7637 + "windows-sys 0.61.2",
7636 7638 ]
7637 7639
7638 7640 [[package]]
@@ -8491,15 +8493,15 @@
8491 8493
8492 8494 [[patch.unused]]
8493 8495 name = "quasi-axum"
8494 - version = "0.88.0"
8496 + version = "0.89.0"
8495 8497
8496 8498 [[patch.unused]]
8497 8499 name = "quasi-basics"
8498 - version = "0.88.0"
8500 + version = "0.89.0"
8499 8501
8500 8502 [[patch.unused]]
8501 8503 name = "quasi-immediate"
8502 - version = "0.88.0"
8504 + version = "0.89.0"
8503 8505
8504 8506 [[patch.unused]]
8505 8507 name = "quasi-store"
@@ -36,7 +36,7 @@
36 36 makeover-geometry = "0.7"
37 37 # Only to write its selection and clock scripts into the frontend. Those two are
38 38 # the whole of `frontend/js/` since the swap.
39 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.88" }
39 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
40 40
41 41 [dependencies]
42 42 goingson-core = { workspace = true }
@@ -54,27 +54,22 @@
54 54 # dependency's source even with the feature off -- so a public clone could not
55 55 # build at all against a URL it could not reach. quasi went public that day and
56 56 # the URL moved here with it.
57 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.88" }
58 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.88" }
59 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.88" }
60 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.88" }
57 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
58 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
59 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
60 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
61 61
62 62 # The notification declaration, and it is NOT behind the `quasi` feature: the
63 63 # watcher is desktop app behaviour rather than a described screen, and the
64 64 # generated config keys are part of `config_key::CONFIG` on every platform. The
65 65 # crate is dep-free at its core for exactly this case; `synckit` adds the spec
66 66 # it produces, and `describe` (below, under `quasi`) adds the settings pane.
67 - #
68 - # NOT `quasi-tauri`'s `notifs` feature, which is where the delivery adapter
69 - # lives. That crate depends on `quasi-router` unconditionally, so taking its
70 - # `Notifier` -- eight lines over `tauri-plugin-notification`, which this app
71 - # already links -- would pull the whole description stack into a default build
72 - # and undo what the `quasi` feature is for. Filed against quasi as a finding:
73 - # the adapter is welded to the router crate, so an app that wants delivery
74 - # before it has ported its screens cannot take it. `notifications::Notifier` is
75 - # the eight lines meanwhile, and it goes when the flip makes quasi-router
76 - # unconditional here.
77 - quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.88", features = ["synckit", "describe"] }
67 + # `tauri` is the delivery adapter, which used to live in `quasi-tauri` and could
68 + # not be taken from there: that crate depends on `quasi-router` unconditionally,
69 + # so an app wanting eight lines over `tauri-plugin-notification` had to link the
70 + # whole description stack for them. quasicoherent `c231ff13` moved it here,
71 + # where the `Deliver` trait it implements already lives.
72 + quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.89", features = ["synckit", "describe", "tauri"] }
78 73
79 74 # Tauri
80 75 tauri = { workspace = true, features = ["image-png", "devtools"] }
@@ -23,13 +23,13 @@
23 23 //! the data and the notification is a fact about the reader, so the unsnooze
24 24 //! happens either way.
25 25 //!
26 - //! # The one thing here that is only here because of a crate boundary
26 + //! # Delivery is the framework's, as of `c231ff13`
27 27 //!
28 - //! [`Notifier`], eight lines. `quasi-tauri` ships exactly this behind its
29 - //! `notifs` feature and taking it would pull `quasi-router` into every build,
30 - //! because that crate depends on the router unconditionally. See the note in
31 - //! `Cargo.toml`; this goes when the flip makes the description stack
32 - //! unconditional.
28 + //! The eight lines that put an occurrence in front of a person used to be
29 + //! here, because the only copy of them lived in `quasi-tauri` and that crate
30 + //! takes `quasi-router` unconditionally. They are [`quasi_notifs::Notifier`]
31 + //! now, behind that crate's `tauri` feature, so taking them costs a plugin
32 + //! this app already links and nothing else.
33 33
34 34 use crate::commands::all_config;
35 35 use crate::notifs::{
@@ -38,11 +38,13 @@
38 38 use crate::state::{AppState, DESKTOP_USER_ID};
39 39 use chrono::Utc;
40 40 use quasi_notifs::config::Settings;
41 - use quasi_notifs::{Deliver, Occurrence, Outbox};
41 + use quasi_notifs::{Notifier, Occurrence, Outbox};
42 42 use std::collections::HashMap;
43 43 use std::sync::Arc;
44 44 use std::time::Duration;
45 45 use tauri::Manager;
46 + // Still this file's own, for `send_notification` below: an undeclared kind
47 + // bypasses the outbox and so has no `Occurrence` to hand a `Notifier`.
46 48 use tauri_plugin_notification::NotificationExt;
47 49 use tokio_util::sync::CancellationToken;
48 50 use tracing::{debug, error, info, instrument, warn};
@@ -50,39 +52,14 @@
50 52 /// Check interval for snooze expiry (60 seconds)
51 53 const CHECK_INTERVAL_SECS: u64 = 60;
52 54
53 - /// Puts an occurrence in front of a person, through Tauri's notification plugin.
55 + /// The delivering adapter, over this app's runtime.
54 56 ///
55 - /// The host's entire share of delivery. It cannot fail in a way the caller
56 - /// could act on -- the occurrence is already noted as sent, and an outbox that
57 - /// retried would be a queue nobody asked for -- so a refusal is counted and
58 - /// logged rather than returned.
59 - struct Notifier {
60 - app: tauri::AppHandle,
61 - refused: usize,
62 - }
63 -
64 - impl Notifier {
65 - const fn new(app: tauri::AppHandle) -> Self {
66 - Self { app, refused: 0 }
67 - }
68 - }
69 -
70 - impl Deliver for Notifier {
71 - fn deliver(&mut self, note: &Occurrence) {
72 - debug!(title = %note.title, body = %note.body, kind = note.kind, "Sending notification");
73 - if let Err(e) = self
74 - .app
75 - .notification()
76 - .builder()
77 - .title(&note.title)
78 - .body(&note.body)
79 - .show()
80 - {
81 - self.refused += 1;
82 - warn!(error = %e, title = %note.title, refused = self.refused, "Failed to send notification");
83 - }
84 - }
85 - }
57 + /// `tauri::AppHandle` defaults to [`tauri::Wry`] and
58 + /// [`Notifier`](quasi_notifs::Notifier) does not: it is generic for the reason
59 + /// everything tauri hands out is, and quasi-notifs takes tauri with default
60 + /// features off so there is no default to inherit. Named once here rather than
61 + /// spelled at each `Outbox<..>` below.
62 + type Delivering = Notifier<tauri::Wry>;
86 63
87 64 /// What the reader has chosen, as the registry reads it.
88 65 ///
@@ -104,8 +81,14 @@
104 81 "Starting notification watcher (interval: {}s)",
105 82 CHECK_INTERVAL_SECS
106 83 );
107 - let mut outbox = Outbox::new(NOTIFS, Notifier::new(app.clone()));
84 + let mut outbox = Outbox::new(NOTIFS, Delivering::new(app.clone()));
108 85 let mut interval = tokio::time::interval(Duration::from_secs(CHECK_INTERVAL_SECS));
86 + // What the platform would not show. `Deliver` cannot fail in a way this
87 + // loop could act on -- the occurrence is already noted as sent, and
88 + // retrying would be a queue nobody asked for -- but a watcher refused
89 + // every time has a permission problem rather than a quiet user, and the
90 + // two look identical without saying so. Counted by the adapter, read here.
91 + let mut refused = 0;
109 92
110 93 loop {
111 94 tokio::select! {
@@ -148,12 +131,21 @@
148 131 if let Err(e) = check_tasks_due(&mut outbox, &state, &chosen) {
149 132 error!(error = %e, "Error checking tasks coming due");
150 133 }
134 +
135 + let so_far = outbox.adapter().refused();
136 + if so_far > refused {
137 + warn!(
138 + refused = so_far,
139 + "The platform refused a notification this tick"
140 + );
141 + refused = so_far;
142 + }
151 143 }
152 144 }
153 145
154 146 #[instrument(skip_all)]
155 147 async fn check_task_snoozes(
156 - outbox: &mut Outbox<Notifier>,
148 + outbox: &mut Outbox<Delivering>,
157 149 state: &Arc<AppState>,
158 150 chosen: &Chosen,
159 151 ) -> Result<(), String> {
@@ -206,7 +198,7 @@
206 198 }
207 199
208 200 fn check_email_snoozes(
209 - outbox: &mut Outbox<Notifier>,
201 + outbox: &mut Outbox<Delivering>,
210 202 state: &Arc<AppState>,
211 203 chosen: &Chosen,
212 204 ) -> Result<(), String> {
@@ -250,7 +242,7 @@
250 242 }
251 243
252 244 fn check_overdue_responses(
253 - outbox: &mut Outbox<Notifier>,
245 + outbox: &mut Outbox<Delivering>,
254 246 state: &Arc<AppState>,
255 247 chosen: &Chosen,
256 248 ) -> Result<(), String> {
@@ -315,7 +307,7 @@
315 307
316 308 #[instrument(skip_all)]
317 309 async fn check_event_reminders(
318 - outbox: &mut Outbox<Notifier>,
310 + outbox: &mut Outbox<Delivering>,
319 311 state: &Arc<AppState>,
320 312 chosen: &Chosen,
321 313 ) -> Result<(), String> {
@@ -398,7 +390,7 @@
398 390 /// of the bare task id would have lost.
399 391 #[instrument(skip_all)]
400 392 fn check_tasks_due(
401 - outbox: &mut Outbox<Notifier>,
393 + outbox: &mut Outbox<Delivering>,
402 394 state: &Arc<AppState>,
403 395 chosen: &Chosen,
404 396 ) -> Result<(), String> {
@@ -536,6 +528,7 @@
536 528 #[cfg(test)]
537 529 mod tests {
538 530 use super::*;
531 + use quasi_notifs::Deliver;
539 532 use quasi_notifs::config::Unset;
540 533 use std::cell::RefCell;
541 534 use std::rc::Rc;