Skip to main content

max / quasi

Let a form's field be guarded, and let a theme picker accrete settings/email's advanced block is six server questions that are on the form only while the disclosure is open, and a form built its fields into a vec! literal, so a guarded field inside one was a parse the emitter refused. They accumulate now, exactly as every other container with a guarded member does, and R9 holds: the field is built whether or not it is asked. The grammar did not move; a field has taken a guard since wave 9. Two vocabulary additions, both the accreting-half gap again. Field::themes is the sixth constructor of that shape after Table::column, Cells::cell, Field::options, Field::option and Node::figure; goingson's Appearance section is the site, since its entries come from a supplier and the form has no expression to hold a list in. Settings for a HashMap covers a store that has already been read in one query, which is what the settings screen does, so the closure it was wrapping the map in said nothing the map does not. quasi-router 0.101.11, quasi-notifs 0.101.2. Both are additions, so consumers pinning 0.101 are owed no forward fix.
Author: Max Johnson <me@maxj.phd> · 2026-09-04 17:17 UTC
Signed with PGP, not checked
Commit: ca634583875d20451dbe58f0897b47fd2376f595
Parent: 94101e2
6 files changed, +87 insertions, -12 deletions
M Cargo.lock +15 -6
@@ -3522,6 +3522,15 @@
3522 3522 "ratatui",
3523 3523 ]
3524 3524
3525 + [[package]]
3526 + name = "quasi-declare"
3527 + version = "0.1.0"
3528 + dependencies = [
3529 + "proc-macro2",
3530 + "quote",
3531 + "syn 2.0.119",
3532 + ]
3533 +
3525 3534 [[package]]
3526 3535 name = "quasi-http"
3527 3536 version = "0.101.1"
@@ -3544,7 +3553,7 @@
3544 3553
3545 3554 [[package]]
3546 3555 name = "quasi-notifs"
3547 - version = "0.101.1"
3556 + version = "0.101.2"
3548 3557 dependencies = [
3549 3558 "quasi-router",
3550 3559 "synckit-config",
@@ -3554,7 +3563,7 @@
3554 3563
3555 3564 [[package]]
3556 3565 name = "quasi-router"
3557 - version = "0.101.1"
3566 + version = "0.101.11"
3558 3567 dependencies = [
3559 3568 "makeover-layout",
3560 3569 ]
@@ -6270,6 +6279,10 @@
6270 6279 "winnow 1.0.4",
6271 6280 ]
6272 6281
6282 + [[patch.unused]]
6283 + name = "synckit-client"
6284 + version = "0.10.0"
6285 +
6273 6286 [[patch.unused]]
6274 6287 name = "kberg"
6275 6288 version = "0.1.0"
@@ -6286,10 +6299,6 @@
6286 6299 name = "tagtree"
6287 6300 version = "0.4.1"
6288 6301
6289 - [[patch.unused]]
6290 - name = "synckit-client"
6291 - version = "0.10.0"
6292 -
6293 6302 [[patch.unused]]
6294 6303 name = "quasi-type"
6295 6304 version = "0.1.3"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-notifs"
3 - version = "0.101.1"
3 + version = "0.101.2"
4 4 description = "Declared notification kinds and the registry that holds them: what a notification says and what it is for, apart from how it reaches a person"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-router"
3 - version = "0.101.10"
3 + version = "0.101.11"
4 4 description = "Host-agnostic router: a request in, a renderer-agnostic description out"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -966,10 +966,18 @@
966 966 }
967 967
968 968 /// One form: where it writes, what its button says, and what it asks.
969 + ///
970 + /// The fields are pushed rather than written into a `vec![]` literal because a
971 + /// field may be guarded, which is what `settings::email`'s advanced block asked
972 + /// for: six server questions that are on the form only while the disclosure is
973 + /// open. R9 holds here as everywhere else -- the field is built whether or not
974 + /// it is asked -- so a supplier feeding a question nobody sees is still called
975 + /// and still answers.
969 976 fn form(action: &Action, body: &[Item]) -> Result<TokenStream> {
970 977 let mut submit = None;
971 - let mut fields = Vec::new();
972 - for item in body {
978 + let mut asked = Vec::new();
979 + let held = format_ident!("asked", span = Span::call_site());
980 + for (index, item) in body.iter().enumerate() {
973 981 match item {
974 982 Item::Attribute { name, args, guard } if name == "submit" => {
975 983 let [label] = args.as_slice() else {
@@ -988,7 +996,33 @@
988 996 name,
989 997 label,
990 998 body,
991 - }) => fields.push(field(kind, name, label, body)?),
999 + }) => {
1000 + let built = field(kind, name, label, body)?;
1001 + asked.push(quote!(#held.push(#built);));
1002 + }
1003 + Item::Emit(Emission::Guarded { guard, inner }) => {
1004 + let Emission::Field {
1005 + kind,
1006 + name,
1007 + label,
1008 + body,
1009 + } = &**inner
1010 + else {
1011 + return Err(syn::Error::new(
1012 + emission_span(inner),
1013 + "a form holds fields, and only a field may be guarded here",
1014 + ));
1015 + };
1016 + let built = field(kind, name, label, body)?;
1017 + let test = predicate(guard)?;
1018 + let question = format_ident!("question_{index}", span = Span::call_site());
1019 + asked.push(quote!({
1020 + let #question = #built;
1021 + if #test {
1022 + #held.push(#question);
1023 + }
1024 + }));
1025 + }
992 1026 Item::Attribute { name, .. } => {
993 1027 return Err(syn::Error::new(
994 1028 name.span(),
@@ -1020,7 +1054,11 @@
1020 1054 ::quasi_router::Node::Form {
1021 1055 action: #action,
1022 1056 submit: ::std::convert::Into::into(#submit),
1023 - fields: ::std::vec![#(#fields),*],
1057 + fields: {
1058 + let mut #held = ::std::vec::Vec::new();
1059 + #(#asked)*
1060 + #held
1061 + },
1024 1062 }
1025 1063 })
1026 1064 }
@@ -189,6 +189,18 @@
189 189 }
190 190 }
191 191
192 + /// A map an app has already read the whole of.
193 + ///
194 + /// The blanket closure impl covers a store that is asked one key at a time. An
195 + /// app that reads its config table in one query holds the answers before the
196 + /// pane is drawn, and a closure over the map is a wrapper that says nothing:
197 + /// GoingsOn's settings screen reads `all_config` once for the whole section.
198 + impl Settings for std::collections::HashMap<String, String> {
199 + fn get(&self, key: &str) -> Option<String> {
200 + self.get(key).cloned()
201 + }
202 + }
203 +
192 204 /// Nothing has been set. Every key reads as its default.
193 205 ///
194 206 /// What an app has on its very first run, and what a test wants when it is
@@ -3607,6 +3607,22 @@
3607 3607 }
3608 3608 }
3609 3609
3610 + /// Offer these themes, chaining.
3611 + ///
3612 + /// The accreting half of [`theme`](Self::theme), which takes the whole list
3613 + /// as an argument, and the same gap [`options`](Self::options) fills beside
3614 + /// [`select`](Self::select): a caller with no expression to hold a list in
3615 + /// still has to be able to offer themes. GoingsOn's Appearance section is
3616 + /// the site.
3617 + ///
3618 + /// Says nothing about the kind, exactly as `options` does not. A renderer
3619 + /// reads these only for [`layout::FieldKind::Theme`].
3620 + #[must_use]
3621 + pub fn themes(mut self, themes: impl IntoIterator<Item = ThemeChoice>) -> Self {
3622 + self.themes.extend(themes);
3623 + self
3624 + }
3625 +
3610 3626 /// The same picker, offering a row that tracks the ambient mode.
3611 3627 ///
3612 3628 /// The [`Choice`] carries the value the app's own store spells it with —