Skip to main content

max / goingson

Take Field::writes quasi 0.76.0 renamed Field::changes. Mechanical across 16 call sites, no control's behaviour touched. `compose.rs` bound a closure called `writes` returning the route each field posts to, so the rename read as `.writes(writes(name))`. It is `field_post`. `emails.rs:531` and `:547` now read `.writes(Action::get(...))`, which says something false about them: they are folder and label filters, and a filter does not write. Left visible on purpose. quasicoherent `aeb44860` is where the vocabulary gains the member that says a reader-driven re-ask, and quietly making these look right would hide the site it needs.
Author: Max Johnson <me@maxj.phd> · 2026-08-28 22:10 UTC
Signed with PGP, not checked
Commit: 0a93dfce3c55f1800a80661224f5fdadb5172ea1
Parent: aab8670
12 files changed, +62 insertions, -62 deletions
M Cargo.lock +31 -31
@@ -1387,7 +1387,7 @@
1387 1387 "libc",
1388 1388 "option-ext",
1389 1389 "redox_users",
1390 - "windows-sys 0.61.2",
1390 + "windows-sys 0.60.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.61.2",
1631 + "windows-sys 0.60.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.61.2",
3653 + "windows-sys 0.60.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.61.2",
3786 + "windows-sys 0.60.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.75.0"
4720 + version = "0.76.0"
4721 4721 dependencies = [
4722 4722 "form_urlencoded",
4723 4723 "http",
@@ -4726,7 +4726,7 @@
4726 4726
4727 4727 [[package]]
4728 4728 name = "quasi-notifs"
4729 - version = "0.75.0"
4729 + version = "0.76.0"
4730 4730 dependencies = [
4731 4731 "quasi-router",
4732 4732 "synckit-config",
@@ -4734,14 +4734,14 @@
4734 4734
4735 4735 [[package]]
4736 4736 name = "quasi-router"
4737 - version = "0.75.0"
4737 + version = "0.76.0"
4738 4738 dependencies = [
4739 4739 "makeover-layout",
4740 4740 ]
4741 4741
4742 4742 [[package]]
4743 4743 name = "quasi-tauri"
4744 - version = "0.75.0"
4744 + version = "0.76.0"
4745 4745 dependencies = [
4746 4746 "http",
4747 4747 "quasi-http",
@@ -4767,7 +4767,7 @@
4767 4767
4768 4768 [[package]]
4769 4769 name = "quasi-webview"
4770 - version = "0.75.0"
4770 + version = "0.76.0"
4771 4771 dependencies = [
4772 4772 "docengine",
4773 4773 "makeover-layout",
@@ -5206,7 +5206,7 @@
5206 5206 "errno",
5207 5207 "libc",
5208 5208 "linux-raw-sys",
5209 - "windows-sys 0.61.2",
5209 + "windows-sys 0.60.2",
5210 5210 ]
5211 5211
5212 5212 [[package]]
@@ -5263,7 +5263,7 @@
5263 5263 "security-framework",
5264 5264 "security-framework-sys",
5265 5265 "webpki-root-certs",
5266 - "windows-sys 0.61.2",
5266 + "windows-sys 0.60.2",
5267 5267 ]
5268 5268
5269 5269 [[package]]
@@ -5803,7 +5803,7 @@
5803 5803 checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
5804 5804 dependencies = [
5805 5805 "libc",
5806 - "windows-sys 0.61.2",
5806 + "windows-sys 0.60.2",
5807 5807 ]
5808 5808
5809 5809 [[package]]
@@ -6563,7 +6563,7 @@
6563 6563 "getrandom 0.4.3",
6564 6564 "once_cell",
6565 6565 "rustix",
6566 - "windows-sys 0.61.2",
6566 + "windows-sys 0.60.2",
6567 6567 ]
6568 6568
6569 6569 [[package]]
@@ -6585,7 +6585,7 @@
6585 6585 "parking_lot",
6586 6586 "rustix",
6587 6587 "signal-hook",
6588 - "windows-sys 0.61.2",
6588 + "windows-sys 0.60.2",
6589 6589 ]
6590 6590
6591 6591 [[package]]
@@ -7067,7 +7067,7 @@
7067 7067 "png 0.18.1",
7068 7068 "serde",
7069 7069 "thiserror 2.0.20",
7070 - "windows-sys 0.61.2",
7070 + "windows-sys 0.60.2",
7071 7071 ]
7072 7072
7073 7073 [[package]]
@@ -7102,7 +7102,7 @@
7102 7102 dependencies = [
7103 7103 "memoffset",
7104 7104 "tempfile",
7105 - "windows-sys 0.61.2",
7105 + "windows-sys 0.60.2",
7106 7106 ]
7107 7107
7108 7108 [[package]]
@@ -7632,7 +7632,7 @@
7632 7632 source = "registry+https://github.com/rust-lang/crates.io-index"
7633 7633 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
7634 7634 dependencies = [
7635 - "windows-sys 0.61.2",
7635 + "windows-sys 0.60.2",
7636 7636 ]
7637 7637
7638 7638 [[package]]
@@ -8490,21 +8490,21 @@
8490 8490 ]
8491 8491
8492 8492 [[patch.unused]]
8493 - name = "quasi-axum"
8494 - version = "0.75.0"
8495 -
8496 - [[patch.unused]]
8497 - name = "quasi-basics"
8498 - version = "0.75.0"
8499 -
8500 - [[patch.unused]]
8501 - name = "quasi-immediate"
8502 - version = "0.75.0"
8503 -
8504 - [[patch.unused]]
8505 - name = "quasi-store"
8493 + name = "ops-status"
8506 8494 version = "0.1.0"
8507 8495
8508 8496 [[patch.unused]]
8509 - name = "ops-status"
8497 + name = "quasi-axum"
8498 + version = "0.76.0"
8499 +
8500 + [[patch.unused]]
8501 + name = "quasi-basics"
8502 + version = "0.76.0"
8503 +
8504 + [[patch.unused]]
8505 + name = "quasi-immediate"
8506 + version = "0.76.0"
8507 +
8508 + [[patch.unused]]
8509 + name = "quasi-store"
8510 8510 version = "0.1.0"
@@ -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.75" }
39 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.76" }
40 40
41 41 [dependencies]
42 42 goingson-core = { workspace = true }
@@ -54,10 +54,10 @@
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.75" }
58 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.75" }
59 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.75" }
60 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.75" }
57 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.76" }
58 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.76" }
59 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.76" }
60 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.76" }
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
@@ -74,7 +74,7 @@
74 74 # before it has ported its screens cannot take it. `notifications::Notifier` is
75 75 # the eight lines meanwhile, and it goes when the flip makes quasi-router
76 76 # unconditional here.
77 - quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.75", features = ["synckit", "describe"] }
77 + quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.76", features = ["synckit", "describe"] }
78 78
79 79 # Tauri
80 80 tauri = { workspace = true, features = ["image-png", "devtools"] }
@@ -34,14 +34,14 @@
34 34 //!
35 35 //! # The draft saves as you write it, which is why there is no Save button
36 36 //!
37 - //! Every field here carries [`Field::changes`], so leaving a field writes it to
37 + //! Every field here carries [`Field::writes`], so leaving a field writes it to
38 38 //! the draft. That is what a mail client does anyway, and it is also what gets
39 39 //! this screen past a known gap: quasicoherent `7da72faf` records that a
40 40 //! [`Node::Form`] has one submit, and compose wanted three verbs over one set of
41 41 //! values. It does not, once the values are already saved. Queue and Discard act
42 42 //! on a draft that is already on disk, and there is no form at all.
43 43 //!
44 - //! `Field::changes` is the DOM `change` event in a webview, which fires when a
44 + //! `Field::writes` is the DOM `change` event in a webview, which fires when a
45 45 //! field settles rather than per keystroke. On a terminal it is per keystroke,
46 46 //! which is quasicoherent `8032fe61` and is that renderer's to answer; a draft
47 47 //! written more often than necessary is the harmless end of that finding.
@@ -228,14 +228,14 @@
228 228 .list_by_user(DESKTOP_USER_ID)
229 229 .map_err(|error| RouteError::internal(error.to_string()))?;
230 230
231 - let writes = |name: &str| Action::post(format!("/compose/{id}/field")).with("field", name);
231 + let field_post = |name: &str| Action::post(format!("/compose/{id}/field")).with("field", name);
232 232 let text = |name: &'static str, label: &str, value: String| {
233 233 Node::field(
234 234 Field {
235 235 value: Some(value),
236 236 ..Field::new(makeover_layout::FieldKind::Text, name, label)
237 237 }
238 - .changes(writes(name)),
238 + .writes(field_post(name)),
239 239 )
240 240 };
241 241
@@ -258,7 +258,7 @@
258 258 value: draft.draft_account_id.map(|id| id.to_string()),
259 259 ..Field::new(makeover_layout::FieldKind::Select, FROM, "From")
260 260 }
261 - .changes(writes(FROM)),
261 + .writes(field_post(FROM)),
262 262 ));
263 263 header = header
264 264 .with(text(TO, "To", draft.to.clone()))
@@ -285,7 +285,7 @@
285 285 value: Some(draft.body.clone()),
286 286 ..Field::new(makeover_layout::FieldKind::Textarea, MESSAGE, "Message")
287 287 }
288 - .changes(writes(MESSAGE)),
288 + .writes(field_post(MESSAGE)),
289 289 ));
290 290 if queued {
291 291 header = header
@@ -492,7 +492,7 @@
492 492
493 493 /// The two filters, and the archive switch.
494 494 ///
495 - /// Fields with a [`Field::changes`] rather than a [`Node::Select`]: the folder
495 + /// Fields with a [`Field::writes`] rather than a [`Node::Select`]: the folder
496 496 /// set is whatever the server has and can be any length, and `Node::Select`'s
497 497 /// three kinds — segmented, toggle, tabs — are all shapes for a handful of
498 498 /// options. The JS reaches the same conclusion by using a `<select>`, and
@@ -528,7 +528,7 @@
528 528 if !folders.is_empty() {
529 529 let mut options = vec![Choice::new("", "All folders")];
530 530 options.extend(folders.iter().map(|folder| Choice::new(folder, folder)));
531 - let mut field = Field::select("folder", "Folder", options).changes(
531 + let mut field = Field::select("folder", "Folder", options).writes(
532 532 View {
533 533 folder: None,
534 534 ..base.clone()
@@ -544,7 +544,7 @@
544 544 if !labels.is_empty() {
545 545 let mut options = vec![Choice::new("", "All labels")];
546 546 options.extend(labels.iter().map(|label| Choice::new(label, label)));
547 - let mut field = Field::select("label", "Label", options).changes(
547 + let mut field = Field::select("label", "Label", options).writes(
548 548 View {
549 549 label: None,
550 550 ..base.clone()
@@ -274,14 +274,14 @@
274 274
275 275 /// A control that stands on its own and writes as soon as it changes.
276 276 ///
277 - /// [`Node::Field`] and [`Field::changes`] together, which is what finding
277 + /// [`Node::Field`] and [`Field::writes`] together, which is what finding
278 278 /// `14612ed8` was closed for and what this screen is the first consumer of. The
279 279 /// JS says the same thing with `data-change` on a bare `<select>` with no form
280 280 /// around it; wrapping these in a [`Node::Form`] would describe a submit button
281 281 /// that does not exist.
282 282 fn setting(field: Field) -> Node {
283 283 let key = field.name.clone();
284 - Node::field(field.changes(writes(&key)))
284 + Node::field(field.writes(writes(&key)))
285 285 }
286 286
287 287 /// A select over a fixed set of values, holding the one in force.
@@ -799,7 +799,7 @@
799 799 );
800 800 out.push(Node::field(
801 801 Field::select("priority", "Set priority", priorities)
802 - .changes(over(Action::post("/tasks/list/priority")))
802 + .writes(over(Action::post("/tasks/list/priority")))
803 803 .over(SELECTION),
804 804 ));
805 805
@@ -818,7 +818,7 @@
818 818 );
819 819 out.push(Node::field(
820 820 Field::select("project", "Set project", options)
821 - .changes(over(Action::post("/tasks/list/project")))
821 + .writes(over(Action::post("/tasks/list/project")))
822 822 .over(SELECTION),
823 823 ));
824 824
@@ -834,7 +834,7 @@
834 834 );
835 835 out.push(Node::field(
836 836 Field::select("until", "Snooze until", whens)
837 - .changes(over(Action::post("/tasks/list/snooze")))
837 + .writes(over(Action::post("/tasks/list/snooze")))
838 838 .over(SELECTION),
839 839 ));
840 840
@@ -947,7 +947,7 @@
947 947
948 948 /// The filter controls.
949 949 ///
950 - /// The two long lists are [`Field::select`] with a [`Field::changes`], for the
950 + /// The two long lists are [`Field::select`] with a [`Field::writes`], for the
951 951 /// reason the mail screen gives: the project set is whatever the user has and
952 952 /// can be any length, and `Node::Select`'s three kinds are all shapes for a
953 953 /// handful of options. The short ones are chips, latched, which is what the
@@ -991,7 +991,7 @@
991 991 // Clearing the project clears the milestone with it: a milestone id
992 992 // that outlived its project filters to a project the view no longer
993 993 // names, and the control that would clear it is not on screen.
994 - let mut field = Field::select("project", "Project", options).changes(
994 + let mut field = Field::select("project", "Project", options).writes(
995 995 View {
996 996 project: None,
997 997 milestone: None,
@@ -1017,7 +1017,7 @@
1017 1017 .iter()
1018 1018 .map(|milestone| Choice::new(milestone.id.to_string(), &milestone.name)),
1019 1019 );
1020 - let mut field = Field::select("milestone", "Milestone", options).changes(
1020 + let mut field = Field::select("milestone", "Milestone", options).writes(
1021 1021 View {
1022 1022 milestone: None,
1023 1023 ..base.clone()
@@ -333,7 +333,7 @@
333 333 /// makeover-layout. The vocabulary has no notion of an action at all, so "what
334 334 /// this calls" was never a thing it could say; `Row::activate` was already the
335 335 /// precedent sitting next to it. This is the small half of the finding — the
336 - /// other 13 sites are standalone controls, and `Field::changes` is theirs.
336 + /// other 13 sites are standalone controls, and `Field::writes` is theirs.
337 337 ///
338 338 /// A linked subtask keeps the button: its state follows the task it links to, so
339 339 /// it is not tickable at all, and `Act::disabled` is what says that. Describing
@@ -533,12 +533,12 @@
533 533 /// field sends its value under its own name and an address carrying it too
534 534 /// would be answering with the value the user just replaced.
535 535 fn modes(view: View) -> Slot {
536 - let work = bounded("work", "Minutes of work", view.work, 1, 240).changes(
536 + let work = bounded("work", "Minutes of work", view.work, 1, 240).writes(
537 537 Action::get("/timer")
538 538 .carrying("break", view.rest.to_string())
539 539 .carrying("days", view.days.to_string()),
540 540 );
541 - let rest = bounded("break", "Minutes of break", view.rest, 1, 60).changes(
541 + let rest = bounded("break", "Minutes of break", view.rest, 1, 60).writes(
542 542 Action::get("/timer")
543 543 .carrying("work", view.work.to_string())
544 544 .carrying("days", view.days.to_string()),
@@ -182,7 +182,7 @@
182 182 .body
183 183 .iter()
184 184 .filter_map(|ranked| match &ranked.node {
185 - Node::Field(field) => field.changes.clone(),
185 + Node::Field(field) => field.writes.clone(),
186 186 Node::Token(tag) => tag.action.clone(),
187 187 _ => None,
188 188 })
@@ -108,7 +108,7 @@
108 108 "When a new signed release is available, a banner appears in the app. \
109 109 Install is always user-initiated.",
110 110 )
111 - .changes(Action::post("/settings/about/update-check")),
111 + .writes(Action::post("/settings/about/update-check")),
112 112 ),
113 113 ]
114 114 }
@@ -173,7 +173,7 @@
173 173 )
174 174 }
175 175 .hint("When off, nothing is sent or fetched until a sync is started by hand.")
176 - .changes(Action::post("/settings/sync/auto")),
176 + .writes(Action::post("/settings/sync/auto")),
177 177 ));
178 178 nodes.push(Node::field(
179 179 Field {
@@ -186,7 +186,7 @@
186 186 "Minutes between syncs",
187 187 )
188 188 }
189 - .changes(Action::post("/settings/sync/interval")),
189 + .writes(Action::post("/settings/sync/interval")),
190 190 ));
191 191
192 192 if state.authenticated {
@@ -139,7 +139,7 @@
139 139 #[tokio::test]
140 140 async fn a_control_writes_as_soon_as_it_changes_with_no_form_around_it() {
141 141 // What finding 14612ed8 was closed for, and this screen is its first
142 - // consumer: Node::Field plus Field::changes, which is a bare control with a
142 + // consumer: Node::Field plus Field::writes, which is a bare control with a
143 143 // route on it. A Node::Form here would describe a submit that does not
144 144 // exist.
145 145 let state = state().await;