Skip to main content

max / quasi

0.42.0: the webview emitter writes htmx 4 `2246072d`. Max ruled it on 2026-08-18: the webview emitter targets htmx 4, not 2.x. quasi is pre-release, so writing the transport against a model that is being replaced means writing it twice, and 4.0 having no announced stable date is a bet taken deliberately rather than a thing to wait out. Wiki `quasi-overview`, "htmx 4, ruled 2026-08-18". Every change was checked against 4.0.0-beta6's own source rather than against the migration guide, which is also what the template now vendors. The swap is `outerMorph`, and it is unconditional. Under 2.x morphing came from idiomorph, which is why the shell carried a `morph_src`, registered `hx-ext="morph"` on the body and the emitter asked for a morph only when the extension was loaded: htmx fell back to a destructive `innerHTML` in silence when it was missing, and silently destructive is the one outcome decision 7 exists to avoid. In 4 there is nothing that can be missing, so `morph_src`, `without_morph()`, `morphs()` and the `morphs: bool` threaded through sixty call sites are all gone. `outerMorph` and not `innerMorph` because the extension's bare `morph` meant outerHTML. The lock is `hx-disable`, 4's name for 2.x's `hx-disabled-elt`. Worth saying out loud because the rename is a swap of meaning: what 2.x called `hx-disable` is `hx-ignore` in 4, so a find-and-replace in either direction is wrong. Nothing here emits `hx-ignore`. Inheritance is explicit, so the headers a document sends go on the body as `hx-headers:inherited`. A bare `hx-headers` there would be a header the body sends on its own requests and no control sends on any, which is the whole reason they are on the body. An emitter knows every attribute it writes, so this costs one attribute name and no design. `quasi_http::htmx::CONFIG_META` and `RESPONSE_HANDLING` are deleted rather than updated. 4 swaps every status but 204 and 304, and `responseHandling` is not a config key any more. That constant existed because 2.x dropped a 4xx, which made decision 9's classified errors (403 `Denied`, 404 `NotFound`) render nothing at all; the emitter gets it for free now, and a screen wanting per-status behaviour says so on the element with `hx-status:XXX`. The two scripts this crate ships listen on `htmx:after:settle`, 4's spelling of the settle event. The scaffolder vendors htmx 4.0.0-beta6 and no longer vendors idiomorph. A 2.x bundle behind `Shell::htmx_src` is a page whose controls fetch nothing and whose swaps destroy state, so the bundle and this crate move together. One behaviour difference is documented rather than designed around: on a GET, htmx 4 deletes an address parameter whose name an `hx-vals` value repeats before appending it, where 2.x appended both. Nothing in the tree names one thing in both bags on a read. Consumers forward-fixed to 0.42: MNW server (2282 lib tests green), goingson under `--features quasi` (677 green), audiofiles. MNW is now in the state the ruling sequences for: its described screens emit 4's markup while `static/htmx.min.js` is still 2.0.4, and the site's own migration is mnw-server `6b87e5ff`. Exposure is bounded by `QUASI_SCREENS`, which is off unless a host sets it. Workspace: 23 test binaries green, clippy clean but for the pre-existing `large_enum_variant` on `Outcome`.
Author: Max Johnson <me@maxj.phd> · 2026-08-20 16:30 UTC
Signed with PGP, not checked
Commit: 26e87ce7185941da45477075893fd0ce371b31c4
Parent: 6442b03
28 files changed, +311 insertions, -383 deletions
M Cargo.lock +15 -15
@@ -3404,11 +3404,11 @@
3404 3404
3405 3405 [[package]]
3406 3406 name = "quasi"
3407 - version = "0.41.0"
3407 + version = "0.42.0"
3408 3408
3409 3409 [[package]]
3410 3410 name = "quasi-axum"
3411 - version = "0.41.0"
3411 + version = "0.42.0"
3412 3412 dependencies = [
3413 3413 "axum",
3414 3414 "http",
@@ -3422,7 +3422,7 @@
3422 3422
3423 3423 [[package]]
3424 3424 name = "quasi-basics"
3425 - version = "0.41.0"
3425 + version = "0.42.0"
3426 3426 dependencies = [
3427 3427 "makeover-layout",
3428 3428 "quasi-http",
@@ -3432,7 +3432,7 @@
3432 3432
3433 3433 [[package]]
3434 3434 name = "quasi-bench"
3435 - version = "0.41.0"
3435 + version = "0.42.0"
3436 3436 dependencies = [
3437 3437 "dhat",
3438 3438 "makeover",
@@ -3447,7 +3447,7 @@
3447 3447
3448 3448 [[package]]
3449 3449 name = "quasi-http"
3450 - version = "0.41.0"
3450 + version = "0.42.0"
3451 3451 dependencies = [
3452 3452 "form_urlencoded",
3453 3453 "http",
@@ -3456,7 +3456,7 @@
3456 3456
3457 3457 [[package]]
3458 3458 name = "quasi-immediate"
3459 - version = "0.41.0"
3459 + version = "0.42.0"
3460 3460 dependencies = [
3461 3461 "docengine",
3462 3462 "egui",
@@ -3466,7 +3466,7 @@
3466 3466
3467 3467 [[package]]
3468 3468 name = "quasi-notifs"
3469 - version = "0.41.0"
3469 + version = "0.42.0"
3470 3470 dependencies = [
3471 3471 "quasi-router",
3472 3472 "synckit-config",
@@ -3474,7 +3474,7 @@
3474 3474
3475 3475 [[package]]
3476 3476 name = "quasi-router"
3477 - version = "0.41.0"
3477 + version = "0.42.0"
3478 3478 dependencies = [
3479 3479 "makeover-layout",
3480 3480 ]
@@ -3491,7 +3491,7 @@
3491 3491
3492 3492 [[package]]
3493 3493 name = "quasi-tauri"
3494 - version = "0.41.0"
3494 + version = "0.42.0"
3495 3495 dependencies = [
3496 3496 "http",
3497 3497 "quasi-http",
@@ -3504,7 +3504,7 @@
3504 3504
3505 3505 [[package]]
3506 3506 name = "quasi-tui"
3507 - version = "0.41.0"
3507 + version = "0.42.0"
3508 3508 dependencies = [
3509 3509 "docengine",
3510 3510 "makeover",
@@ -3516,7 +3516,7 @@
3516 3516
3517 3517 [[package]]
3518 3518 name = "quasi-webview"
3519 - version = "0.41.0"
3519 + version = "0.42.0"
3520 3520 dependencies = [
3521 3521 "docengine",
3522 3522 "makeover-layout",
@@ -6207,10 +6207,10 @@
6207 6207 name = "quasi-type"
6208 6208 version = "0.1.0"
6209 6209
6210 - [[patch.unused]]
6211 - name = "synckit-client"
6212 - version = "0.8.0"
6213 -
6214 6210 [[patch.unused]]
6215 6211 name = "makeover-build"
6216 6212 version = "0.47.0"
6213 +
6214 + [[patch.unused]]
6215 + name = "synckit-client"
6216 + version = "0.8.0"
M README.md +4 -3
@@ -108,9 +108,10 @@
108 108
109 109 `quasi-http` is what the two hosts turned out to share once there were two of
110 110 them. Decoding a query string and a form body, mapping an error's class to a
111 - status, naming a fragment's region in an `HX-Retarget` header, and the htmx
112 - client configuration a classified error needs. Nothing in it knows which host it
113 - is in, and nothing in it is async.
111 + status, and naming a fragment's region in an `HX-Retarget` header. The transport
112 + is htmx 4, which is what an emitted document links and what the response headers
113 + here are checked against. Nothing in it knows which host it is in, and nothing
114 + in it is async.
114 115
115 116 `quasi-axum` is the hosted adapter. What is left in it is axum's own: it mounts
116 117 as a fallback, so ordinary axum routes merged in front keep serving the things a
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-axum"
3 - version = "0.41.0"
3 + version = "0.42.0"
4 4 description = "The axum host adapter for quasi-router: an HTTP request in, a rendered description out"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -13,14 +13,14 @@
13 13 workspace = true
14 14
15 15 [dependencies]
16 - quasi-router = { path = "../quasi-router", version = "0.41.0" }
17 - quasi-http = { path = "../quasi-http", version = "0.41.0" }
16 + quasi-router = { path = "../quasi-router", version = "0.42.0" }
17 + quasi-http = { path = "../quasi-http", version = "0.42.0" }
18 18 axum = "0.8.8"
19 19 http = "1.3.1"
20 20 tokio = { version = "1.50.0", features = ["rt"] }
21 21
22 22 [dev-dependencies]
23 - quasi-webview = { path = "../quasi-webview", version = "0.41.0" }
23 + quasi-webview = { path = "../quasi-webview", version = "0.42.0" }
24 24 tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] }
25 25 tower = { version = "0.5.3", features = ["util"] }
26 26 http-body-util = "0.1.3"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-basics"
3 - version = "0.41.0"
3 + version = "0.42.0"
4 4 description = "The first-party widget set: named assemblies of primitives, shared across our apps"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -13,12 +13,12 @@
13 13 workspace = true
14 14
15 15 [dependencies]
16 - quasi-router = { path = "../quasi-router", version = "0.41.0" }
16 + quasi-router = { path = "../quasi-router", version = "0.42.0" }
17 17 makeover-layout = "0.31.0"
18 18
19 19 [dev-dependencies]
20 20 # A widget's guarantees are claims about what a renderer draws, so they are
21 21 # tested against a real one rather than by walking the tree the assembly just
22 22 # built. The webview is the renderer that recognises names today.
23 - quasi-webview = { path = "../quasi-webview", version = "0.41.0" }
24 - quasi-http = { path = "../quasi-http", version = "0.41.0" }
23 + quasi-webview = { path = "../quasi-webview", version = "0.42.0" }
24 + quasi-http = { path = "../quasi-http", version = "0.42.0" }
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-bench"
3 - version = "0.41.0"
3 + version = "0.42.0"
4 4 description = "What a described screen costs to render, in time and in allocations"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -22,13 +22,13 @@
22 22 count = ["dep:dhat"]
23 23
24 24 [dependencies]
25 - quasi-router = { path = "../quasi-router", version = "0.41.0" }
26 - quasi-webview = { path = "../quasi-webview", version = "0.41.0" }
27 - quasi-tui = { path = "../quasi-tui", version = "0.41.0" }
25 + quasi-router = { path = "../quasi-router", version = "0.42.0" }
26 + quasi-webview = { path = "../quasi-webview", version = "0.42.0" }
27 + quasi-tui = { path = "../quasi-tui", version = "0.42.0" }
28 28 # `Serves` is the trait carrying `screen` and `fragment`, which is what the
29 29 # webview is measured through. Taken directly rather than through quasi-webview
30 30 # because a bench calling a trait method should name the trait it calls.
31 - quasi-http = { path = "../quasi-http", version = "0.41.0" }
31 + quasi-http = { path = "../quasi-http", version = "0.42.0" }
32 32 makeover-layout = "0.31.0"
33 33 makeover-tui = { version = "0.30.0", features = ["theme"] }
34 34 # Only to load a bundled theme file. `makeover_tui::Theme` is `#[non_exhaustive]`,
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-http"
3 - version = "0.41.0"
3 + version = "0.42.0"
4 4 description = "The http-shaped seam quasi's webview host adapters share: decoding in, a rendered description out"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -13,6 +13,6 @@
13 13 workspace = true
14 14
15 15 [dependencies]
16 - quasi-router = { path = "../quasi-router", version = "0.41.0" }
16 + quasi-router = { path = "../quasi-router", version = "0.42.0" }
17 17 http = "1.3.1"
18 18 form_urlencoded = "1.2.2"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-immediate"
3 - version = "0.41.0"
3 + version = "0.42.0"
4 4 edition = "2024"
5 5 description = "The immediate-mode renderer for quasi-router: a described screen in, an egui frame out. Immediate mode is the constraint that matters, not the library."
6 6 license = "MIT"
@@ -12,7 +12,7 @@
12 12 categories = ["gui"]
13 13
14 14 [dependencies]
15 - quasi-router = { path = "../quasi-router", version = "0.41.0" }
15 + quasi-router = { path = "../quasi-router", version = "0.42.0" }
16 16 # The node drawing, which is the makeover layer's and not this crate's. Every
17 17 # widget here that is not a container comes from it: a screen walk that painted
18 18 # its own meter would be the divergence the suite exists to end, one copy per