aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--_components/opengraph-embed.webc1439
-rw-r--r--_includes/default.html1
-rw-r--r--_posts/2020-07-18-monads-without-the-bullshit.md3
-rw-r--r--_posts/2020-08-21-survey-of-rust-gui-libraries.md2
-rw-r--r--_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md4
-rw-r--r--_posts/2022-09-21-ogham-balanced-elevenary.md22
-rw-r--r--_posts/2022-10-30-taxonomy-of-open-source.md22
-rw-r--r--_posts/2023-02-12-uuid-versions.md169
-rw-r--r--_posts/2023-05-23-two-heresies-about-link-rot.md42
-rw-r--r--_posts/2023-06-15-the-derivative-gardens-license.md18
-rw-r--r--_posts/2023-08-17-25-hour-time.md54
-rw-r--r--_posts/2023-11-26-tiny-cactus-cloudtest02.md29
-rw-r--r--_posts/2023-12-27-no-mans-sky-unless.md20
-rw-r--r--_posts/2023-12-31-the-browser-is-a-terrible-place-for-art.md24
-rw-r--r--_posts/2024-09-08-abdication-is-not-simplicity.md25
-rw-r--r--_posts/2024-09-20-eggbug-forever-ffxiv.md28
-rw-r--r--_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md1395
-rw-r--r--_posts/_posts.11tydata.js1
-rw-r--r--assets/2023-05-23-two-heresies-about-link-rot-1.pngbin0 -> 16032 bytes
-rw-r--r--assets/2023-11-26-tiny-cactus-cloudtest02-1.pngbin0 -> 1398353 bytes
-rw-r--r--assets/2023-11-26-tiny-cactus-cloudtest02-2.pngbin0 -> 347625 bytes
-rw-r--r--assets/2023-11-26-tiny-cactus-cloudtest02-3.pngbin0 -> 2036516 bytes
-rw-r--r--assets/2023-12-27-no-mans-sky-unless-1.pngbin0 -> 556646 bytes
-rw-r--r--assets/2024-09-20-eggbug-forever-ffxiv-1.pngbin0 -> 4578247 bytes
-rw-r--r--assets/2024-09-20-eggbug-forever-ffxiv-2.gifbin0 -> 10337916 bytes
-rw-r--r--assets/2024-09-20-eggbug-forever-ffxiv-3.gifbin0 -> 10265147 bytes
-rw-r--r--assets/2025-04-13-cushy.pngbin0 -> 10171 bytes
-rw-r--r--assets/2025-04-13-dioxus.pngbin0 -> 9844 bytes
-rw-r--r--assets/2025-04-13-egui.pngbin0 -> 5495 bytes
-rw-r--r--assets/2025-04-13-floem.pngbin0 -> 8674 bytes
-rw-r--r--assets/2025-04-13-fltk.pngbin0 -> 4938 bytes
-rw-r--r--assets/2025-04-13-flutter-rust-bridge.pngbin0 -> 10684 bytes
-rw-r--r--assets/2025-04-13-freya.pngbin0 -> 7365 bytes
-rw-r--r--assets/2025-04-13-gpui.pngbin0 -> 6793 bytes
-rw-r--r--assets/2025-04-13-gtk4-adwaita.pngbin0 -> 19558 bytes
-rw-r--r--assets/2025-04-13-gtk4.pngbin0 -> 9334 bytes
-rw-r--r--assets/2025-04-13-iced.pngbin0 -> 12794 bytes
-rw-r--r--assets/2025-04-13-imgui.pngbin0 -> 13030 bytes
-rw-r--r--assets/2025-04-13-kas.pngbin0 -> 5684 bytes
-rw-r--r--assets/2025-04-13-makepad.pngbin0 -> 9753 bytes
-rw-r--r--assets/2025-04-13-masonry.pngbin0 -> 9241 bytes
-rw-r--r--assets/2025-04-13-properties.pngbin0 -> 18886 bytes
-rw-r--r--assets/2025-04-13-relm4.pngbin0 -> 10460 bytes
-rw-r--r--assets/2025-04-13-ribir.pngbin0 -> 9607 bytes
-rw-r--r--assets/2025-04-13-rui.pngbin0 -> 8452 bytes
-rw-r--r--assets/2025-04-13-slint.pngbin0 -> 2836 bytes
-rw-r--r--assets/2025-04-13-tauri.pngbin0 -> 8058 bytes
-rw-r--r--assets/2025-04-13-tk.pngbin0 -> 2244 bytes
-rw-r--r--assets/2025-04-13-vizia-focused.pngbin0 -> 3598 bytes
-rw-r--r--assets/2025-04-13-vizia.pngbin0 -> 3580 bytes
-rw-r--r--assets/2025-04-13-winsafe.pngbin0 -> 4415 bytes
-rw-r--r--assets/2025-04-13-xilem.pngbin0 -> 10011 bytes
-rw-r--r--assets/site.css29
-rw-r--r--eleventy.config.js15
-rw-r--r--package-lock.json211
-rw-r--r--package.json3
-rw-r--r--projects.md2
57 files changed, 3533 insertions, 25 deletions
diff --git a/_components/opengraph-embed.webc b/_components/opengraph-embed.webc
new file mode 100644
index 0000000..f3159e0
--- /dev/null
+++ b/_components/opengraph-embed.webc
@@ -0,0 +1,1439 @@
+<!-- lifted from the iframe service cohost was using, not sorry -->
+<style webc:scoped>
+ :root {
+ --bc: #fff9f2;
+ --dc: #191919;
+ --hc: #191919;
+ --mc: #827f7c;
+ --wc: #827f7c;
+ --fbr: 3px;
+ }
+
+ ._thd {
+ --bc: #000;
+ --dc: #fff9f2;
+ --hc: #fff9f2;
+ --mc: #a09c98;
+ --wc: #a09c98;
+ }
+
+ .e {
+ padding-bottom: 62.5%;
+ }
+
+ .t {
+ font-weight: normal;
+ font-style: normal;
+ line-height: 1.4;
+ }
+
+ .th {
+ font-weight: 700;
+ font-style: normal;
+ }
+
+ a {
+ border-bottom: none;
+ }
+
+ :root {
+ box-sizing: border-box;
+ font-family: sans-serif;
+ line-height: 1.4;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ -ms-overflow-style: scrollbar;
+ -webkit-tap-highlight-color: transparent
+ }
+
+ *, ::after, ::before {
+ box-sizing: border-box
+ }
+
+ @-ms-viewport {
+ width: device-width
+ }
+
+ article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
+ display: block
+ }
+
+ [tabindex="-1"]:focus {
+ outline: 0 !important
+ }
+
+ h1, h2, h3, h4, h5, h6, li, ol, p, ul {
+ margin: 0;
+ padding: 0
+ }
+
+ b, strong {
+ font-weight: bolder
+ }
+
+ sub, sup {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline
+ }
+
+ sub {
+ bottom: -.25em
+ }
+
+ sup {
+ top: -.5em
+ }
+
+ a {
+ background-color: transparent;
+ -webkit-text-decoration-skip: objects
+ }
+
+ a:not([href]):not([tabindex]) {
+ color: inherit;
+ text-decoration: none
+ }
+
+ a:not([href]):not([tabindex]):focus {
+ outline: 0
+ }
+
+ img {
+ vertical-align: middle;
+ border-style: none
+ }
+
+ svg:not(:root) {
+ overflow: hidden
+ }
+
+ .w, body {
+ overflow: hidden;
+ margin: 0;
+ padding: 0;
+ background: none transparent;
+ text-align: left
+ }
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ touch-action: manipulation
+ }
+
+ .w {
+ line-height: 1.4;
+ font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ font-weight: 400;
+ font-size: 15px;
+ color: var(--dc);
+ -webkit-hyphens: auto;
+ hyphens: auto;
+ word-wrap: break-word;
+ overflow-wrap: break-word
+ }
+
+ ._rtl {
+ direction: rtl;
+ text-align: right
+ }
+
+ .t, .w, .wf {
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ width: 100%
+ }
+
+ @supports (-webkit-overflow-scrolling:touch) {
+ .w {
+ max-width: 100vw
+ }
+ }
+
+ .wc, .wt {
+ overflow: hidden
+ }
+
+ ._sc, ._sm {
+ background: var(--bc);
+ border: var(--fbw, 0) solid var(--fbc, transparent);
+ border-radius: var(--fbr, 0)
+ }
+
+ ._or .tf {
+ order: 0
+ }
+
+ ._or .th {
+ order: 1
+ }
+
+ ._or .td {
+ order: 2
+ }
+
+ ._alsr._ls .wf {
+ flex-direction: column-reverse
+ }
+
+ ._alcr._lc .wf {
+ flex-direction: row-reverse
+ }
+
+ ._sc._ls .wt, ._ss._ls .wt {
+ padding-left: 0;
+ padding-right: 0
+ }
+
+ ._sc._ls._alsd .wt, ._ss._ls._alsd .wt {
+ padding-bottom: 0
+ }
+
+ ._sc._ls._alsr .wt, ._ss._ls._alsr .wt {
+ padding-top: 0
+ }
+
+ ._sc._lc .wt, ._ss._lc .wt {
+ padding-top: 0;
+ padding-bottom: 0
+ }
+
+ ._ss._lc._alcd:not(._rtl) .wt {
+ padding-right: 0
+ }
+
+ ._ss._lc._alcd._rtl .wt {
+ padding-left: 0
+ }
+
+ ._ss._lc._alcr .wt {
+ padding-left: 0
+ }
+
+ ._lc .wf {
+ flex-direction: row
+ }
+
+ ._lc .wt {
+ display: flex;
+ flex: 1;
+ align-items: center
+ }
+
+ ._sc._lc._alcd:not(._rtl) .wf {
+ padding-right: 0 !important
+ }
+
+ ._sc._lc._alcd._rtl .wf {
+ padding-left: 0 !important
+ }
+
+ ._sc._lc._alcr:not(._rtl) .wf {
+ padding-left: 0 !important
+ }
+
+ ._sc._lc._alcr._rtl .wf {
+ padding-right: 0 !important
+ }
+
+ ._xc._sc._ls .wt, ._xc._ss._ls .wt {
+ padding-top: 0
+ }
+
+ ._xc._sc._lc:not(._rtl) .wt, ._xc._ss._lc:not(._rtl) .wt {
+ padding-left: 0
+ }
+
+ .wt {
+ padding: 8px 10px
+ }
+
+ @media (min-width: 360px) {
+ .wt {
+ padding: 12px 15px
+ }
+ }
+
+ @media (min-width: 600px) {
+ .wt {
+ padding: 16px 20px
+ }
+ }
+
+ ._lc._sm:not(.xd) .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+
+ @media (min-width: 360px) {
+ ._lc._sm:not(.xd) .wc {
+ min-width: 110px;
+ width: 110px;
+ min-height: 110px
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc._sm:not(.xd) .wc {
+ min-width: 140px;
+ width: 140px;
+ min-height: 140px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._lc._sm:not(.xd) .wc {
+ min-width: 160px;
+ width: 160px;
+ min-height: 160px
+ }
+ }
+
+ ._lc._sm._xd:not(._xf) .wc, ._lc._sm._xf:not(._xd) .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+
+ @media (min-width: 360px) {
+ ._lc._sm._xd:not(._xf) .wc, ._lc._sm._xf:not(._xd) .wc {
+ min-width: 110px;
+ width: 110px;
+ min-height: 110px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._lc._sm._xd:not(._xf) .wc, ._lc._sm._xf:not(._xd) .wc {
+ min-width: 120px;
+ width: 120px;
+ min-height: 120px
+ }
+ }
+
+ ._lc._sm._xd._xf .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+
+ ._lc._sc:not(.xd) .wc {
+ min-width: 92px;
+ width: 92px;
+ min-height: 92px
+ }
+
+ @media (min-width: 360px) {
+ ._lc._sc:not(.xd) .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc._sc:not(.xd) .wc {
+ min-width: 130px;
+ width: 130px;
+ min-height: 130px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._lc._sc:not(.xd) .wc {
+ min-width: 145px;
+ width: 145px;
+ min-height: 145px
+ }
+ }
+
+ ._lc._sc._xd:not(._xf) .wc, ._lc._sc._xf:not(._xd) .wc {
+ min-width: 92px;
+ width: 92px;
+ min-height: 92px
+ }
+
+ @media (min-width: 360px) {
+ ._lc._sc._xd:not(._xf) .wc, ._lc._sc._xf:not(._xd) .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._lc._sc._xd:not(._xf) .wc, ._lc._sc._xf:not(._xd) .wc {
+ min-width: 110px;
+ width: 110px;
+ min-height: 110px
+ }
+ }
+
+ ._lc._sc._xd._xf .wc {
+ min-width: 92px;
+ width: 92px;
+ min-height: 92px
+ }
+
+ @supports (-moz-appearance:meterbar) and (all:initial) {
+ ._lc .wc {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ align-content: stretch
+ }
+ }
+
+ ._lc._ss:not(.xd) .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+
+ @media (min-width: 360px) {
+ ._lc._ss:not(.xd) .wc {
+ min-width: 110px;
+ width: 110px;
+ min-height: 110px
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc._ss:not(.xd) .wc {
+ min-width: 140px;
+ width: 140px;
+ min-height: 140px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._lc._ss:not(.xd) .wc {
+ min-width: 160px;
+ width: 160px;
+ min-height: 160px
+ }
+ }
+
+ ._lc._ss._xd:not(._xf) .wc, ._lc._ss._xf:not(._xd) .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+
+ @media (min-width: 360px) {
+ ._lc._ss._xd:not(._xf) .wc, ._lc._ss._xf:not(._xd) .wc {
+ min-width: 110px;
+ width: 110px;
+ min-height: 110px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._lc._ss._xd:not(._xf) .wc, ._lc._ss._xf:not(._xd) .wc {
+ min-width: 120px;
+ width: 120px;
+ min-height: 120px
+ }
+ }
+
+ ._lc._ss._xd._xf .wc {
+ min-width: 100px;
+ width: 100px;
+ min-height: 100px
+ }
+
+ ._sc .wf {
+ padding: 8px
+ }
+
+ @media (min-width: 360px) {
+ ._sc:not(._xd):not(._xf) .wf {
+ padding: 10px
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._sc:not(._xd):not(._xf) .wf {
+ padding: 12px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._sc:not(._xd):not(._xf) .wf {
+ padding: 16px
+ }
+ }
+
+ ._ls .th {
+ -webkit-line-clamp: 2
+ }
+
+ ._ls._lh10 .th {
+ max-height: 2em
+ }
+
+ ._ls._lh11 .th {
+ max-height: 2.2em
+ }
+
+ ._ls._lh12 .th {
+ max-height: 2.4em
+ }
+
+ ._ls._lh13 .th {
+ max-height: 2.6em
+ }
+
+ ._ls._lh14 .th {
+ max-height: 2.8em
+ }
+
+ ._ls._lh15 .th {
+ max-height: 3em
+ }
+
+ ._ls .td {
+ -webkit-line-clamp: 3
+ }
+
+ ._ls._lh10 .td {
+ max-height: 3em
+ }
+
+ ._ls._lh11 .td {
+ max-height: 3.3em
+ }
+
+ ._ls._lh12 .td {
+ max-height: 3.6em
+ }
+
+ ._ls._lh13 .td {
+ max-height: 3.9em
+ }
+
+ ._ls._lh14 .td {
+ max-height: 4.2em
+ }
+
+ ._ls._lh15 .td {
+ max-height: 4.5em
+ }
+
+ ._ls .twd {
+ display: none
+ }
+
+ @media (max-width: 459px) {
+ ._lc .ti, ._lc .tm, ._lc .tw + .tx, ._lc .twt {
+ display: none
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc .twd {
+ display: none
+ }
+ }
+
+ ._lc:not(._ap):not(._ts) .th {
+ -webkit-line-clamp: 3
+ }
+
+ ._lc:not(._ap):not(._ts)._lh10 .th {
+ max-height: 3em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh11 .th {
+ max-height: 3.3em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh12 .th {
+ max-height: 3.6em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh13 .th {
+ max-height: 3.9em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh14 .th {
+ max-height: 4.2em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh15 .th {
+ max-height: 4.5em
+ }
+
+ @media (max-width: 359px) {
+ ._lc:not(._ap):not(._ts) .td {
+ display: none
+ }
+ }
+
+ @media (min-width: 360px) {
+ ._lc:not(._ap):not(._ts) .th {
+ -webkit-line-clamp: 2
+ }
+
+ ._lc:not(._ap):not(._ts)._lh10 .th {
+ max-height: 2em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh11 .th {
+ max-height: 2.2em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh12 .th {
+ max-height: 2.4em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh13 .th {
+ max-height: 2.6em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh14 .th {
+ max-height: 2.8em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh15 .th {
+ max-height: 3em
+ }
+
+ ._lc:not(._ap):not(._ts) .td {
+ -webkit-line-clamp: 1
+ }
+
+ ._lc:not(._ap):not(._ts)._lh10 .td {
+ max-height: 1em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh11 .td {
+ max-height: 1.1em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh12 .td {
+ max-height: 1.2em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh13 .td {
+ max-height: 1.3em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh14 .td {
+ max-height: 1.4em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh15 .td {
+ max-height: 1.5em
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc:not(._ap):not(._ts) .td {
+ -webkit-line-clamp: 2
+ }
+
+ ._lc:not(._ap):not(._ts)._lh10 .td {
+ max-height: 2em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh11 .td {
+ max-height: 2.2em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh12 .td {
+ max-height: 2.4em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh13 .td {
+ max-height: 2.6em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh14 .td {
+ max-height: 2.8em
+ }
+
+ ._lc:not(._ap):not(._ts)._lh15 .td {
+ max-height: 3em
+ }
+ }
+
+ ._lc._ap:not(._ts) .th {
+ -webkit-line-clamp: 3
+ }
+
+ ._lc._ap:not(._ts)._lh10 .th {
+ max-height: 3em
+ }
+
+ ._lc._ap:not(._ts)._lh11 .th {
+ max-height: 3.3em
+ }
+
+ ._lc._ap:not(._ts)._lh12 .th {
+ max-height: 3.6em
+ }
+
+ ._lc._ap:not(._ts)._lh13 .th {
+ max-height: 3.9em
+ }
+
+ ._lc._ap:not(._ts)._lh14 .th {
+ max-height: 4.2em
+ }
+
+ ._lc._ap:not(._ts)._lh15 .th {
+ max-height: 4.5em
+ }
+
+ ._lc._ap:not(._ts) .td {
+ -webkit-line-clamp: 2
+ }
+
+ ._lc._ap:not(._ts)._lh10 .td {
+ max-height: 2em
+ }
+
+ ._lc._ap:not(._ts)._lh11 .td {
+ max-height: 2.2em
+ }
+
+ ._lc._ap:not(._ts)._lh12 .td {
+ max-height: 2.4em
+ }
+
+ ._lc._ap:not(._ts)._lh13 .td {
+ max-height: 2.6em
+ }
+
+ ._lc._ap:not(._ts)._lh14 .td {
+ max-height: 2.8em
+ }
+
+ ._lc._ap:not(._ts)._lh15 .td {
+ max-height: 3em
+ }
+
+ @media (min-width: 360px) {
+ ._lc._ap:not(._ts) .th {
+ -webkit-line-clamp: 2
+ }
+
+ ._lc._ap:not(._ts)._lh10 .th {
+ max-height: 2em
+ }
+
+ ._lc._ap:not(._ts)._lh11 .th {
+ max-height: 2.2em
+ }
+
+ ._lc._ap:not(._ts)._lh12 .th {
+ max-height: 2.4em
+ }
+
+ ._lc._ap:not(._ts)._lh13 .th {
+ max-height: 2.6em
+ }
+
+ ._lc._ap:not(._ts)._lh14 .th {
+ max-height: 2.8em
+ }
+
+ ._lc._ap:not(._ts)._lh15 .th {
+ max-height: 3em
+ }
+
+ ._lc._ap:not(._ts) .td {
+ -webkit-line-clamp: 3
+ }
+
+ ._lc._ap:not(._ts)._lh10 .td {
+ max-height: 3em
+ }
+
+ ._lc._ap:not(._ts)._lh11 .td {
+ max-height: 3.3em
+ }
+
+ ._lc._ap:not(._ts)._lh12 .td {
+ max-height: 3.6em
+ }
+
+ ._lc._ap:not(._ts)._lh13 .td {
+ max-height: 3.9em
+ }
+
+ ._lc._ap:not(._ts)._lh14 .td {
+ max-height: 4.2em
+ }
+
+ ._lc._ap:not(._ts)._lh15 .td {
+ max-height: 4.5em
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc._ap:not(._ts) .td {
+ -webkit-line-clamp: 4
+ }
+
+ ._lc._ap:not(._ts)._lh10 .td {
+ max-height: 4em
+ }
+
+ ._lc._ap:not(._ts)._lh11 .td {
+ max-height: 4.4em
+ }
+
+ ._lc._ap:not(._ts)._lh12 .td {
+ max-height: 4.8em
+ }
+
+ ._lc._ap:not(._ts)._lh13 .td {
+ max-height: 5.2em
+ }
+
+ ._lc._ap:not(._ts)._lh14 .td {
+ max-height: 5.6em
+ }
+
+ ._lc._ap:not(._ts)._lh15 .td {
+ max-height: 6em
+ }
+ }
+
+ ._lc._ts .th {
+ -webkit-line-clamp: 1
+ }
+
+ ._lc._ts._lh10 .th {
+ max-height: 1em
+ }
+
+ ._lc._ts._lh11 .th {
+ max-height: 1.1em
+ }
+
+ ._lc._ts._lh12 .th {
+ max-height: 1.2em
+ }
+
+ ._lc._ts._lh13 .th {
+ max-height: 1.3em
+ }
+
+ ._lc._ts._lh14 .th {
+ max-height: 1.4em
+ }
+
+ ._lc._ts._lh15 .th {
+ max-height: 1.5em
+ }
+
+ ._lc._ts .td {
+ -webkit-line-clamp: 2
+ }
+
+ ._lc._ts._lh10 .td {
+ max-height: 2em
+ }
+
+ ._lc._ts._lh11 .td {
+ max-height: 2.2em
+ }
+
+ ._lc._ts._lh12 .td {
+ max-height: 2.4em
+ }
+
+ ._lc._ts._lh13 .td {
+ max-height: 2.6em
+ }
+
+ ._lc._ts._lh14 .td {
+ max-height: 2.8em
+ }
+
+ ._lc._ts._lh15 .td {
+ max-height: 3em
+ }
+
+ @media (min-width: 460px) {
+ ._lc._ts .th {
+ -webkit-line-clamp: 1
+ }
+
+ ._lc._ts._lh10 .th {
+ max-height: 1em
+ }
+
+ ._lc._ts._lh11 .th {
+ max-height: 1.1em
+ }
+
+ ._lc._ts._lh12 .th {
+ max-height: 1.2em
+ }
+
+ ._lc._ts._lh13 .th {
+ max-height: 1.3em
+ }
+
+ ._lc._ts._lh14 .th {
+ max-height: 1.4em
+ }
+
+ ._lc._ts._lh15 .th {
+ max-height: 1.5em
+ }
+
+ ._lc._ts .td {
+ -webkit-line-clamp: 3
+ }
+
+ ._lc._ts._lh10 .td {
+ max-height: 3em
+ }
+
+ ._lc._ts._lh11 .td {
+ max-height: 3.3em
+ }
+
+ ._lc._ts._lh12 .td {
+ max-height: 3.6em
+ }
+
+ ._lc._ts._lh13 .td {
+ max-height: 3.9em
+ }
+
+ ._lc._ts._lh14 .td {
+ max-height: 4.2em
+ }
+
+ ._lc._ts._lh15 .td {
+ max-height: 4.5em
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc._xf:not(._xd)._ts .td {
+ -webkit-line-clamp: 2
+ }
+
+ ._lc._xf:not(._xd)._ts._lh10 .td {
+ max-height: 2em
+ }
+
+ ._lc._xf:not(._xd)._ts._lh11 .td {
+ max-height: 2.2em
+ }
+
+ ._lc._xf:not(._xd)._ts._lh12 .td {
+ max-height: 2.4em
+ }
+
+ ._lc._xf:not(._xd)._ts._lh13 .td {
+ max-height: 2.6em
+ }
+
+ ._lc._xf:not(._xd)._ts._lh14 .td {
+ max-height: 2.8em
+ }
+
+ ._lc._xf:not(._xd)._ts._lh15 .td {
+ max-height: 3em
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._lc._xf:not(._xd)._tl .td, ._lc._xf:not(._xd)._tm .td {
+ -webkit-line-clamp: 1
+ }
+
+ ._lc._xf:not(._xd)._tl._lh10 .td, ._lc._xf:not(._xd)._tm .td {
+ max-height: 1em
+ }
+
+ ._lc._xf:not(._xd)._tl._lh11 .td, ._lc._xf:not(._xd)._tm .td {
+ max-height: 1.1em
+ }
+
+ ._lc._xf:not(._xd)._tl._lh12 .td, ._lc._xf:not(._xd)._tm .td {
+ max-height: 1.2em
+ }
+
+ ._lc._xf:not(._xd)._tl._lh13 .td, ._lc._xf:not(._xd)._tm .td {
+ max-height: 1.3em
+ }
+
+ ._lc._xf:not(._xd)._tl._lh14 .td, ._lc._xf:not(._xd)._tm .td {
+ max-height: 1.4em
+ }
+
+ ._lc._xf:not(._xd)._tl._lh15 .td, ._lc._xf:not(._xd)._tm .td {
+ max-height: 1.5em
+ }
+ }
+
+ .t {
+ -webkit-hyphens: auto;
+ hyphens: auto;
+ color: var(--dc)
+ }
+
+ .th {
+ color: var(--hc)
+ }
+
+ .td, .th {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: block
+ }
+
+ @supports (display:-webkit-box) {
+ .td, .th {
+ display: -webkit-box;
+ -webkit-box-orient: vertical
+ }
+ }
+
+ .td {
+ vertical-align: inherit
+ }
+
+ .tf, .th {
+ margin-bottom: .5em
+ }
+
+ .td {
+ margin-bottom: .6em
+ }
+
+ ._od .td:last-child, ._od .tf:last-child, ._od .th:last-child {
+ margin-bottom: 0 !important
+ }
+
+ ._or .td {
+ margin-bottom: 0 !important
+ }
+
+ .tf {
+ display: flex;
+ align-items: center;
+ color: var(--mc)
+ }
+
+ .tw {
+ color: var(--wc)
+ }
+
+ .tc {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis
+ }
+
+ .tim {
+ display: block;
+ min-width: 16px;
+ min-height: 16px;
+ width: 1em;
+ height: 1em;
+ margin-right: 6px
+ }
+
+ ._rtl .tim {
+ margin-left: 6px;
+ margin-right: 0
+ }
+
+ .tx {
+ opacity: .4;
+ margin: 0 .25em
+ }
+
+ .tx:last-child {
+ display: none !important
+ }
+
+ ._hd .td, ._hf .tf {
+ display: none
+ }
+
+ ._hw .ti, ._hw .tw, ._hw .tw + .tx {
+ display: none
+ }
+
+ ._hm .tm, ._hm .tw + .tx {
+ display: none
+ }
+
+ ._hwi .ti {
+ display: none
+ }
+
+ ._hwt .tw, ._hwt .tw + .tx {
+ display: none
+ }
+
+ ._hmt .tmt, ._hmt .tmt + .tx {
+ display: none
+ }
+
+ ._hmd .tm .tx, ._hmd .tmd {
+ display: none
+ }
+
+ ._od._hf .td {
+ margin-bottom: 0 !important
+ }
+
+ ._od._hd._hf .th, ._or._hd .th {
+ margin-bottom: 0 !important
+ }
+
+ @media (min-width: 460px) {
+ .td {
+ margin-bottom: .7em
+ }
+ }
+
+ ._ffsa {
+ font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif
+ }
+
+ ._ffse {
+ font-family: Georgia, "Times New Roman", Times, serif
+ }
+
+ ._ffmo {
+ font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace
+ }
+
+ ._ffco {
+ font-family: "Comic Sans MS", "Comic Sans", cursive
+ }
+
+ ._fwn {
+ font-weight: 400
+ }
+
+ ._fwb {
+ font-weight: 700
+ }
+
+ ._fsi {
+ font-style: italic
+ }
+
+ ._fsn {
+ font-style: normal
+ }
+
+ ._ttn {
+ text-transform: none
+ }
+
+ ._ttu {
+ text-transform: uppercase;
+ letter-spacing: .025em
+ }
+
+ ._lh10 {
+ line-height: 1
+ }
+
+ ._lh11 {
+ line-height: 1.1
+ }
+
+ ._lh12 {
+ line-height: 1.2
+ }
+
+ ._lh13 {
+ line-height: 1.3
+ }
+
+ ._lh14 {
+ line-height: 1.4
+ }
+
+ ._lh15 {
+ line-height: 1.5
+ }
+
+ ._f3m {
+ font-size: 11px
+ }
+
+ ._f0, ._f1m, ._f2m, ._f3m {
+ font-size: 12px
+ }
+
+ ._f1p, ._f2p {
+ font-size: 13px
+ }
+
+ ._f3p {
+ font-size: 14px
+ }
+
+ ._f4p {
+ font-size: 16px
+ }
+
+ @media (min-width: 360px) {
+ ._f0 {
+ font-size: 13px
+ }
+
+ ._f1p {
+ font-size: 14px
+ }
+
+ ._f2p {
+ font-size: 15px
+ }
+
+ ._f3p {
+ font-size: 16px
+ }
+
+ ._f4p {
+ font-size: 18px
+ }
+ }
+
+ @media (min-width: 460px) {
+ ._f1m {
+ font-size: 13px
+ }
+
+ ._f0 {
+ font-size: 14px
+ }
+
+ ._f1p {
+ font-size: 15px
+ }
+
+ ._f2p {
+ font-size: 16px
+ }
+
+ ._f3p {
+ font-size: 18px
+ }
+
+ ._f4p {
+ font-size: 21px
+ }
+ }
+
+ @media (min-width: 600px) {
+ ._f3m {
+ font-size: 12px
+ }
+
+ ._f2m {
+ font-size: 13px
+ }
+
+ ._f1m {
+ font-size: 14px
+ }
+
+ ._f0 {
+ font-size: 15px
+ }
+
+ ._f1p {
+ font-size: 17px
+ }
+
+ ._f2p {
+ font-size: 18px
+ }
+
+ ._f3p {
+ font-size: 21px
+ }
+
+ ._f4p {
+ font-size: 24px
+ }
+ }
+
+ .e {
+ overflow: hidden;
+ position: relative;
+ width: 100%
+ }
+
+ .e ._ls {
+ height: 0;
+ padding-bottom: 56.25%
+ }
+
+ @supports (-moz-appearance:meterbar) and (all:initial) {
+ ._lc .e {
+ flex: 1
+ }
+ }
+
+ ._lc:not(._ap) .e {
+ height: 100%;
+ padding-bottom: 0
+ }
+
+ .em {
+ position: absolute;
+ width: 100%;
+ height: 100%
+ }
+
+ .c, .co {
+ position: absolute;
+ width: 100%;
+ height: 100%
+ }
+
+ .c {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: no-repeat center;
+ background-size: cover
+ }
+
+ .c {
+ z-index: 20
+ }
+
+ .co {
+ z-index: 30
+ }
+
+ :root {
+ --pos: 14px;
+ --pop: 8px
+ }
+
+ @media (min-width: 460px) {
+ :root {
+ --pos: 18px;
+ --pop: 10px
+ }
+ }
+
+ ._p .wt {
+ position: relative;
+ overflow: initial
+ }
+
+ .po {
+ position: absolute;
+ z-index: 30;
+ bottom: 0;
+ right: 0;
+ display: block;
+ padding: var(--pop);
+ font-size: var(--pos)
+ }
+
+ ._rtl .po {
+ right: auto;
+ left: 0
+ }
+
+ .poi {
+ display: block;
+ width: 1em;
+ height: 1em;
+ font-size: 1em;
+ fill: var(--pc)
+ }
+
+ ._p:not(._rtl) .t {
+ padding-right: var(--pos)
+ }
+
+ ._p:not(._rtl):not(._sm)._alsr .po {
+ padding-right: 0
+ }
+
+ ._p:not(._rtl)._ss._alcd .po, ._p:not(._rtl):not(._sm):not(._lc)._alcd .po {
+ padding-right: 0
+ }
+
+ ._p:not(._rtl)._ss._alcd .t, ._p:not(._rtl):not(._sm):not(._lc)._alcd .t {
+ padding-right: calc(var(--pos) + var(--pop))
+ }
+
+ ._p._rtl .t {
+ padding-left: var(--pos)
+ }
+
+ ._p._rtl._ss._alcd .po, ._p._rtl:not(._sm):not(._lc)._alcd .po {
+ padding-left: 0
+ }
+
+ ._p._rtl._ss._alcd .t, ._p._rtl:not(._sm):not(._lc)._alcd .t {
+ padding-left: calc(var(--pos) + var(--pop))
+ }
+
+ ._p._rtl._sc._lc._alsr .po {
+ padding-bottom: 0
+ }
+
+ ._p:not(._sm):not(._alsr) .po {
+ padding-bottom: 0
+ }
+
+ ._p .wc .po {
+ padding: var(--pop) !important;
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .25))
+ }
+</style>
+<div class="w __if _tha _ls _sm _or _alsd _alcd _lh14 _xi _hmt _tm _dm" style="display: block;">
+ <div class="wf">
+ <div class="wc">
+ <div class="e" style="padding-bottom: 62.5000%;">
+ <div class="em"><a
+ :href="href"
+ target="_blank" rel="noopener" class="c ">
+ <img :src="imgSrc" class="c">
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="wt">
+ <div class="t _f0">
+ <div class="th _f1p"><a
+ :href="href"
+ target="_blank" rel="noopener" class="thl">
+ <slot name="title"></slot>
+ </a></div>
+ <div class="td">
+ <slot></slot>
+ </div>
+ <div class="tf _f1m"><a :href="site-href" target="_blank" rel="noopener" class="ti _f1m"><img
+ :src="siteFavicon" class="tim"></a>
+ <div class="tc"><a :href="site-href" target="_blank" rel="noopener" class="tw _f1m"><span
+ class="twt"><slot name="site-name"></slot></span><span class="twd"><slot
+ name="site-domain"></slot></span></a><span class="tx">/</span><span
+ class="tm"><span :datetime="datetime"
+ class="tmd"><slot name="date"></slot></span></span></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/_includes/default.html b/_includes/default.html
index 61f43bf..5b81206 100644
--- a/_includes/default.html
+++ b/_includes/default.html
@@ -21,6 +21,7 @@
<title>{{ title }} | boringcactus</title>
{% endif %}
<link rel="stylesheet" href="/assets/site.css">
+ <style>{% getBundle "css" %}</style>
<link type="application/atom+xml" rel="alternate" href="/feed.xml" title="boringcactus" />
<link rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/" />
<link rel="icon" href="/assets/icon.svg">
diff --git a/_posts/2020-07-18-monads-without-the-bullshit.md b/_posts/2020-07-18-monads-without-the-bullshit.md
index 59d39c3..0cef6cf 100644
--- a/_posts/2020-07-18-monads-without-the-bullshit.md
+++ b/_posts/2020-07-18-monads-without-the-bullshit.md
@@ -3,6 +3,9 @@ layout: default
title: "Monads, Explained Without Bullshit"
---
+*edit 2025-03-04*: this explanation is bad in the same way that most monad explanations are bad: it doesn’t address why the abstraction of “monad” is valuable.
+the first explainer i’ve seen that actually explains why it’s worth understanding is [Demystifying monads in Rust through property-based testing](https://sunshowers.io/posts/monads-through-pbt/); go read that instead.
+
there's a CS theory term, "monad," that has a reputation among extremely online programmers as being a really difficult to understand concept that nobody knows how to actually explain.
for a while, i thought that reputation was accurate; i tried like six times to understand what a monad is, and couldn't get anywhere.
but then a friend sent me [Philip Wadler's 1992 paper "Monads for functional programming"](https://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf) which turns out to be a really good explanation of what a monad is (in addition to a bunch of other stuff i don't care about).
diff --git a/_posts/2020-08-21-survey-of-rust-gui-libraries.md b/_posts/2020-08-21-survey-of-rust-gui-libraries.md
index 5f650fa..5ff053c 100644
--- a/_posts/2020-08-21-survey-of-rust-gui-libraries.md
+++ b/_posts/2020-08-21-survey-of-rust-gui-libraries.md
@@ -3,6 +3,8 @@ layout: default
title: A Survey of Rust GUI Libraries
---
+(Note from the future: I did this again [in 2021](/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md) and [in 2025](/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md).)
+
a popular trend in the Rust community is to ask "Are We X Yet" for various things that it would be nice to be able to develop easily in Rust - [game](https://arewegameyet.rs/) and [web](https://www.arewewebyet.org/) are the most prominent ones as far as i can tell - and one such question is [Are We GUI Yet](https://areweguiyet.com/).
that's a good question; *are* we GUI yet?
Are We GUI Yet has a list of libraries for building GUIs: let's go through them in alphabetical order and see if we can build a simple to-do list with them without too much struggle.
diff --git a/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md b/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md
index 3375637..e5cb21a 100644
--- a/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md
+++ b/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md
@@ -2,7 +2,9 @@
title: A 2021 Survey of Rust GUI Libraries
---
-A year and a half ago I [looked at a bunch of different Rust GUI libraries](_posts/2020-08-21-survey-of-rust-gui-libraries.md); that's a long time, so let's see if things have changed.
+(Note from the future: I did this again [in 2025](/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md).)
+
+A year and a half ago I [looked at a bunch of different Rust GUI libraries](/_posts/2020-08-21-survey-of-rust-gui-libraries.md); that's a long time, so let's see if things have changed.
As before, some context:
diff --git a/_posts/2022-09-21-ogham-balanced-elevenary.md b/_posts/2022-09-21-ogham-balanced-elevenary.md
new file mode 100644
index 0000000..f6c4367
--- /dev/null
+++ b/_posts/2022-09-21-ogham-balanced-elevenary.md
@@ -0,0 +1,22 @@
+---
+title: writing balanced elevenary in the ogham script
+---
+
+**for no particular reason**
+
+digits, from negative five to positive five:
+
+<span style="font-size:200%">ᚅ ᚄ ᚃ ᚂ ᚁ ᚋ ᚆ ᚇ ᚈ ᚉ ᚊ</span>
+
+(romanized as N S F L B M H D T C Q)
+
+radix point <span style="font-size:200%">ᚖ</span> (left as . when romanizing)
+
+a few nice values: <span style="font-size:200%">ᚆᚅᚈᚖᚆᚊᚇᚋᚅ</span>, <span style="font-size:200%">ᚈᚊᚇ</span>, <span style="font-size:200%">ᚈᚖᚇᚅᚆᚊᚆ</span>
+
+fuck around with it y'self:
+
+<div class="cohost-style-embed">
+<div style="left: 0; width: 100%; height: 300px; position: relative;"><iframe src="https://codepen.io/boringcactus/embed/preview/abGwgoG?default-tabs=js%2Cresult&height=300&host=https%3A%2F%2Fcodepen.io&slug-hash=abGwgoG" style="top: 0; left: 0; width: 100%; height: 100%; position: absolute; border: 0;" allowfullscreen></iframe></div>
+<div class="cohost-style-embed-link"><a href="https://codepen.io/boringcactus/pen/abGwgoG">https://codepen.io/boringcactus/pen/abGwgoG</a></div>
+</div>
diff --git a/_posts/2022-10-30-taxonomy-of-open-source.md b/_posts/2022-10-30-taxonomy-of-open-source.md
new file mode 100644
index 0000000..895a22d
--- /dev/null
+++ b/_posts/2022-10-30-taxonomy-of-open-source.md
@@ -0,0 +1,22 @@
+---
+title: some axes for a taxonomy of open source
+description: a list
+---
+
+- maintenance effort and expectations: professionally maintained / casually maintained / not maintained
+- source of priorities: owner-driven / community-driven
+- desire for input: PRs welcome / issues welcome / view-only
+- license standards: bare minimum / give code back / don't be evil / be specifically good
+- how bills get paid: not users' problem / crowdfunding / selling support / we productized the hosting so if you also productize the hosting you're being a dick
+
+for example:
+
+- React is professionally maintained, owner-driven, PRs welcome, bare minimum, not users' problem
+- MongoDB is the same but give code back and we productized the hosting
+- SQLite is professionally maintained, owner-driven, issues welcome, bare minimum, selling support
+- anything that runs on github sponsors or patreon is crowdfunding
+- the Hippocratic License or some of the others are don't be evil
+- ACSL counts as be specifically good
+- anti-licenses are the secret fifth tier of license standards, Good Fucking Luck
+- some projects are actually community-driven but I can't think of one off the top of my head
+- most of my side projects are not maintained, owner-driven, view-only, Good Fucking Luck, and crowdfunding at the time they were written but currently not your problem.
diff --git a/_posts/2023-02-12-uuid-versions.md b/_posts/2023-02-12-uuid-versions.md
new file mode 100644
index 0000000..52d5fde
--- /dev/null
+++ b/_posts/2023-02-12-uuid-versions.md
@@ -0,0 +1,169 @@
+---
+title: UUID versions through the ages
+---
+
+UUIDs are neat. y'know, `cfbff0d1-9375-5685-968c-48ce8b15ae17` type of shit. if you're like me until a few days ago, all you know about the types of UUID is that v4 is the good one. but why are there other ones? is there a secret better one? why are the dashes asymmetrical? let's take a (roughly paraphrased from [wikipedia](https://en.wikipedia.org/w/index.php?title=Universally_unique_identifier&oldid=1136241716) and probably not quite accurate) look.
+
+## wait why even
+
+sometimes you need an ID for something you are putting in the computer, so that you have a stable way to refer to it even if all the editable fields on it change. the simplest possible approach is to give the first thing ID 1, the second thing ID 2, and so on. cohost works this way right now - as i'm editing it, this draft post has ID 1009270, meaning this is the just-over-a-millionth thing in the posts table.
+
+your database sits there going "the next post has ID 8. oh, new post? it has ID 8, okay the next post has ID 9." and all is well. except a year later you have a million posts and a bunch of people posting all at once, and every new post needs a new ID but they have to get created one at a time in the database so that they all get the right ID. If You're In Line (To Get The Next Post ID), Stay In Line. and the only way to know what the next post ID is is to check with the database, so you can't do things like save drafts offline with proper IDs. (staff probably doesn't want that anyway, but we need something vaguely similar at work, which is how i got here.) if you need to work at, say, Twitter's scale, or you need to be able to generate IDs without checking with the database, you need something more involved than just sequential IDs.
+
+## version 1
+
+in the early 90s, some UNIX people ran into this problem when drawing up their Distributed Computing Environment. they called [their solution](https://pubs.opengroup.org/onlinepubs/9629399/apdxa.htm) "Universal Unique Identifiers", which they call "an identifier that is unique across both space and time". it's written as a hexadecimal string, but it can be stored as just the 16 bytes that are represented by that hexadecimal string. the way they make sure it's unique across both space and time is actually pretty straightforward: part of the UUID encodes the space where it was generated, and part of the UUID encodes the time where it was generated.
+
+the UUID format has two control fields and three data fields. the version field is pretty straightforward - it's 1 for UUIDv1, 2 for UUIDv2, etc. at this point, they only had 1 and 2, but they left room in the spec for up to 15 just in case. there's also a variant field, which says whether it's a normal UUID (`10`, hex value `8` through `b`) or some other bullshit that may or may not adhere to any of the rest of this spec.
+
+<details>
+<summary>other bullshit</summary>
+<aside>
+
+if the variant field is `0` then it's a UUID from Apollo Computer's Network Computing System, which had UUIDs before DCE but defined them in a slightly different way. if it's `110` then it's a UUID but the wrong endian, which Microsoft does sometimes when it makes UUIDs (it calls them GUIDs, because they're thinking too small, merely Global rather than Universal). if it's `111` then you're living in the future where they assigned a meaning to variant `111`. what's it like? how's the whole climate change thing going?
+
+</aside>
+</details>
+
+the data fields are
+
+- timestamp, which is measured since the start of the Gregorian calendar with 100ns resolution (which, with 60 bits available, repeats every 3653 years). this is how they make sure the UUID is unique across time.
+
+- clock sequence, which starts at a random number and goes up by one if time goes backwards, to ensure that things like clock drift or leap seconds don't lead to collisions. this is how they make sure the UUID is *actually* unique across time.
+
+- node, which is just the MAC address on your network card (or one of them, if you've got more than one), since different network cards already have to have different MAC addresses in order for networking to happen. this is how they make sure the UUID is unique across space.
+
+this is how we get that weird hyphen asymmetry, the groups come directly from the UUID data fields:
+
+<div style="display: grid; grid-template-rows: 1fr 1fr;"><div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 5; border-bottom: 1px solid;">version</span><span style="grid-column: 8; border-bottom: 1px solid;">variant</span></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><kbd style="font-weight: 600;">1a8188ce</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">aa78</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">1</kbd><kbd style="font-weight: 600;">1ed</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">a</kbd><kbd style="font-weight: 600;">fa1</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">0242ac120002</kbd></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 1; border-top: 1px solid;">time_low</span><span style="grid-column: 3; border-top: 1px solid;">time_mid</span><span style="grid-column: 6; border-top: 1px solid;">time_hi</span><span style="grid-column: 8 / 10; border-top: 1px solid;">clock sequence</span><span style="grid-column: 11; border-top: 1px solid;">node</span></div></div>
+
+you may have noticed that the time is split into three pieces, for the low 32 bits, the middle 16 bits, and the high 12 bits. why split it up like that? well, i don't know, but i suspect it makes a lot of things easier to split the 60 bit timestamp into at least 32 and 28 (and maybe splitting the 28 into 16 and 12 makes something easier in a way i'm not seeing?). for one, 64-bit CPUs weren't mainstream yet, and for two, they had creative alternate uses for that time_low field.
+
+## version 2
+
+like a lot of multi-user operating systems, UNIX has users and groups, and allows for permissions management based on those users and groups. users and groups both have textual names and numeric IDs, so if you want to stably refer to a specific user or group, you can use its user ID or group ID. however, different computers can have different sets of users and groups, so if you're making the Distributed Computing Environment, you need a way to refer to a specific user or group on a specific machine. you're building on top of UNIX, because you're the Open Software Foundation (later The Open Group), so you have user and group IDs locally already. and you already made this UUID format, which has fields that refer to a specific machine. the other fields are already taken for time and also-time, but you didn't promise they'd *always* be time, right?
+
+in UUIDv2 (which the [DCE spec](https://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01) calls the "security version"), the time_low field is literally just a UNIX user/group ID. the low byte of the clock sequence field is repurposed to specify whether it's a user or a group (or a secret third thing, an organization).
+
+i have several questions. for one, what about the other time fields? time_mid ticks up once every 7 minutes, if you construct your UUIDv2 out of a UUIDv1. do you just leave it at zero and let time_hi tick up every 325 days? do you leave mid and hi both at zero and party like it's 1582? for two, had they not invented MAC address spoofing yet? these days you can usually change your network card's MAC address to something else, so using that for anything security-related strikes me as highly dubious. for three,, what? just in general? why would you do this? this is some 5 Minute Crafts tier lifehackery. please refrain.
+
+presumably this worked well enough for DCE, but it has not withstood the test of time. i don't know that UUIDv2 even counts as a UUID, but it follows the UUID format and put a 2 in the version number slot, and so it lives on solely as negative space in the UUID version number range. (this is also apparently the deal with IPv5.)
+
+UUIDv2 may or may not have been a good idea, but the concept of "what if you had a UUID based on some specific value other than the current time" had legs.
+
+## version 3
+
+DCE was done being written, and then it kinda died, but people kept using UUIDs. DCE was a legacy-style Proper Goddamn Specification, written by the consortium that had since become The Open Group, who also run POSIX and the Single UNIX Specification and all that jazz (🌵 when the posix is sus !), but that sort of doorstopper spec was overkill for the humble UUID. what it needed, as a piece of computer bullshit, was an RFC. and so in 2005 the UUID was defined again in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122), which kept v1, reduced v2 to one sentence, and added some new versions.
+
+one way to think of the goal of UUIDv2 is that it's about referring to an object that already has a contextually unique ID. in v2, that object is either a user or a group, and that context is a machine. v3 is a little more flexible, but one of the contexts mentioned in the RFC is domain names, so let's look at that.
+
+say i want something in the format of a UUID that refers to the domain name `example.com`. one option would be to take the MD5 hash of `"example.com"`, look at the first 16 bytes, line that up with the UUID format definition, and set the version and variant to the right values. this is cool, and it basically already works for domain names, but we want flexibility. if you and i both want to do the UUIDv2 thing of referring to users on a machine, and my context is my machine and your context is your machine, and both of us have a user named `cactus`, oops, we have the same UUID, that's hardly Universally Unique. we need to include the context in what we're MD5ing, and we need to guarantee that different contexts have different values. and there's nothing computer people love more than recursion, so let's give the context a damn UUID.
+
+to make a UUIDv3, you need a name (which is just some text) and a UUID for your "name space" (which is the context in which your name is unique). take the binary representation of the namespace UUID, append the name, MD5 it, copy that into your UUID structure, set the version and variant, and you are done.
+
+the RFC defines some name space UUIDs already, like `6ba7b810-9dad-11d1-80b4-00c04fd430c8` for domain names, so we can check this ourself:
+
+```python
+>>> import hashlib
+>>> import uuid
+>>> dns_namespace = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
+>>> hashlib.md5(dns_namespace.bytes + b"example.com").hexdigest()
+'9073926b929fd1c26bc9fad77ae3e8eb'
+>>> uuid.uuid3(dns_namespace, "example.com")
+UUID('9073926b-929f-31c2-abc9-fad77ae3e8eb')
+```
+
+this is pretty damn neat. if you have something that's contextually unique and you want to turn it into something that's globally unique, this is a really cool way to do that. (spoilers, except for one thing, which you may have noticed if you know your hash functions.) but this is only sometimes a problem you have; other times, you don't have anything unique yet, and you want something ex nihilo. v1 is still good, if you've got a timestamp and a MAC address, but what if you're doing something like JS development where you can't exactly check the MAC address you're running on? well, satan help you. or what if that 100ns resolution isn't good enough like it was in the 90s?
+
+## version 4
+
+set the version and variant. fill the rest with random bits. there is no step 3.
+
+if you had asked me to guess last week what a UUIDv4 was, i'd have just guessed it was 128 random bits (if you made me count how long it was). and i'd have been wrong, but only by six bits. which is neat, but also a little bit bullshit, because like. me from last week wants those bits back!
+
+those six bits are for compatibility with the rest of the UUID universe, but if you're just looking for some random bytes to throw in your id column, you don't need compatibility with UUIDv1, you could just make some random bytes! and 16 of them is probably overkill for your use case anyway!
+
+wait hang on a minute, did that say MD5 earlier?
+
+## version 5
+
+turns out MD5 sucks. you know what's really cool? SHA-1.
+
+UUIDv5 is just UUIDv3 again, but with SHA-1 instead of MD5.
+
+```python
+>>> hashlib.sha1(dns_namespace.bytes + b"example.com").hexdigest()
+'cfbff0d193753685568c48ce8b15ae17d93cc34c'
+>>> uuid.uuid5(dns_namespace, "example.com")
+UUID('cfbff0d1-9375-5685-968c-48ce8b15ae17')
+```
+
+thankfully, SHA-1 is the last word in hashing algorithms, it's never had problems and it's known to be very good. now to take a big sip of my coffee and check the NIST website.
+
+<div class="cohost-style-embed">
+{% renderTemplate "webc" %}
+<opengraph-embed
+ href="https://www.nist.gov/news-events/news/2022/12/nist-retires-sha-1-cryptographic-algorithm"
+ site-href="https://www.nist.gov"
+ site-favicon="https://www.nist.gov/themes/custom/nist_www/favicon.ico"
+ img-src="https://www.nist.gov/sites/default/files/images/2022/12/14/SecureHashAltogirthm23_Released_960x600_v4_A.png"
+ datetime="2022-12-15T12:00:00.000Z"
+>
+<span slot="title">NIST Retires SHA-1 Cryptographic Algorithm</span>
+The venerable cryptographic hash function has vulnerabilities that make its further use inadvisable.
+<span slot="site-name">NIST</span>
+<span slot="site-domain">nist.gov</span>
+<span slot="date">Dec&nbsp;15,&nbsp;2022</span>
+</opengraph-embed>
+{% endrenderTemplate %}
+<div class="cohost-style-embed-link"><a href="https://www.nist.gov/news-events/news/2022/12/nist-retires-sha-1-cryptographic-algorithm">https://www.nist.gov/news-events/news/2022/12/nist-retires-sha-1-cryptographic-algorithm</a></div>
+</div>
+
+oh. that seems bad. when is UUIDv6?
+
+## version 6
+
+well. there's a [draft RFC](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format) making updates to the UUID RFC, but it doesn't solve that problem, it solves different problems.
+
+*edit 2025-03-04*: this draft has been adopted as [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562.html), obsoleting the original RFC 4122. hell yeah.
+
+one of the cool things about UUIDv1 is that you can decode the timestamp back out of it, and you don't need a separate field for the time when your object was created, because its ID tells you when it was created. however, the weird slicing and dicing that UUIDv1 does to the timestamp field means sorting by time is complicated, since the low 32 bits come first and the high 12 bits come last.
+
+UUIDv6 puts the whole timestamp in order, so that the most significant bit is first.
+
+<div style="display: grid; grid-template-rows: 1fr 1fr;"><div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 5; border-bottom: 1px solid;">version</span><span style="grid-column: 8; border-bottom: 1px solid;">variant</span></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><kbd style="font-weight: 600;">1edaa9b4</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">e919</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">6</kbd><kbd style="font-weight: 600;">172</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">a</kbd><kbd style="font-weight: 600;">0d0</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">6721ef312724</kbd></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 1; border-top: 1px solid;">time_high</span><span style="grid-column: 3; border-top: 1px solid;">time_mid</span><span style="grid-column: 6; border-top: 1px solid;">time_low</span><span style="grid-column: 8 / 10; border-top: 1px solid;">clock sequence</span><span style="grid-column: 11; border-top: 1px solid;">node</span></div></div>
+
+it keeps the clock sequence as-is from v1, but it explicitly recommends using random data instead of the MAC address for the node field, which is good.
+
+hang on, while we're messing with silly things from v1, what the hell is up with time since the gregorian calendar?
+
+## version 7
+
+what if we just did unix timestamp and randomness, so that we had easy sorting and decoding but also uniqueness, without any bullshit.
+
+well, v7 is that. 48 bits of unix timestamp in milliseconds (rolls over every 8920 years), 74 random bits, 6 control bits for version and variant.
+
+<div style="display: grid; grid-template-rows: 1fr 1fr;"><div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 3; border-bottom: 1px solid;">version</span><span style="grid-column: 6; border-bottom: 1px solid;">variant</span></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><kbd style="font-weight: 600;">0186443f-2c00</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">7</kbd><kbd style="font-weight: 600;">5fb</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">8</kbd><kbd style="font-weight: 600;">00a-7ec0f02a852d</kbd></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 1; border-top: 1px solid;">time</span><span style="grid-column: 4; border-top: 1px solid;">rand_a</span><span style="grid-column: 6 / 10; border-top: 1px solid;">rand_b</span></div></div>
+
+this is the real good one. the only reason not to use it is that the RFC isn't approved so it isn't quite official yet, but if you don't need to care, find an implementation in your language of choice and go to fuckin town.
+
+*edit 2025-03-04*: hi it’s me from the future. i have good news: the RFC was approved. i do not have more good news than that.
+
+but what if yolo?
+
+## version 8
+
+if yolo, then you can use version 8. all of the version-specific fields are defined to be custom, so you can put whatever the hell nonsense you need to there.
+
+<div style="display: grid; grid-template-rows: 1fr 1fr;"><div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 3; border-bottom: 1px solid;">version</span><span style="grid-column: 6; border-bottom: 1px solid;">variant</span></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><kbd style="font-weight: 600;">b00b5101-6969</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">8</kbd><kbd style="font-weight: 600;">420</kbd><kbd style="font-weight: 600;">-</kbd><kbd style="font-weight: 600;">9</kbd><kbd style="font-weight: 600;">a55-676179736578</kbd></div> <div style="display: grid; grid-column: 1 / 99; grid-template-columns: subgrid; text-align: center;"><span style="grid-column: 1; border-top: 1px solid;">custom_a</span><span style="grid-column: 4; border-top: 1px solid;">custom_b</span><span style="grid-column: 6 / 10; border-top: 1px solid;">custom_c</span></div></div>
+
+## so what did we learn
+
+UUIDs are pretty neat. if you need a database identifier and you get to pick something from scratch, use v7, it gives you timestamps for free. if you have things that are contextually unique and you want to turn them into universally unique IDs with a standard format, v5 is for you. if all you need is some random shit, v4 is that in the UUID format, but if you don't need the UUID format, you can also just use random bytes directly. if what you need is very specific shit nobody's thought of before, but in the UUID format, that's v8. v6 is the worse version of v7, v3 is the worse version of v5, v1 is the worse version of v6, and v2 was a mistake.
+
+see also: [creative use of v1's MAC address field](https://cohost.org/twilight-sparkle/post/1010833-related-reading-on), [current RFC draft status and other notes](https://cohost.org/iliana/post/1013189-some-notes-on-furthe), [bluetooth doing crimes](https://cohost.org/vikxin/post/1014175-and-then-bluetooth-h)
+
+in conclusion,
+
+# UUIDs nuts.
+
diff --git a/_posts/2023-05-23-two-heresies-about-link-rot.md b/_posts/2023-05-23-two-heresies-about-link-rot.md
new file mode 100644
index 0000000..2e9e076
--- /dev/null
+++ b/_posts/2023-05-23-two-heresies-about-link-rot.md
@@ -0,0 +1,42 @@
+---
+title: two heresies about link rot
+---
+
+
+the term itself is fascinating. there is nothing natural or organic about a hyperlink, i've never seen anyone call a working link alive, and yet a broken link is dead and links becoming broken over time is rot. the breaking of a link can, depending on the context, be frustrating, tragic, amusing; this it has in common with the more conventional kind of death. but we recognize that to live is to one day die, and there may be no justice in when or how but no justice can be asked of if. we will not last forever; why should our work? decay exists as an extant form of life, as they say. that iconic post itself doesn't exist anymore, it seems; we keep it alive through active preservation, and if it outlasts all its authors it will be because so many people found it compelling. a former drama youtuber recently removed several of his toxic edgelord videos, and of course they're not gone (and the links themselves may still work, i don't care enough to check) but it's good that he can do at least that much to clean up the cultural mess he helped make. maybe the fact that links can rot is a good thing, actually. may the worst of all our links rot before we do.
+
+but of course, there's a spectrum, and just because the median tumblr post or youtube video deserves ephemerality doesn't mean there's nothing to be said for permanence. if we really could recover dinosaur DNA from mosquitoes trapped in amber, that could be very neat as long as we didn't do anything unwise afterwards. there's always nonprofits volunteering to be the metaphorical amber, but it takes a lot of money to remember everything for all of time, and they have an unfortunate tendency to also use that money to pick losing fights with big industries and get into Web 3.0, plus you only need them after the link has already rotted, so the simplest solution would be for links to just always keep working. the issue with that is that internet domains are like real estate in that you can only ever rent, and if you don't pay then someone else will so goodbye. i've had several domains lapse and immediately be squatted, especially later in grad school when $20 was a silly amount of money to waste to preserve the domain for a side project i'd long since given up on. links rot because domains expire, and one of the reasons domains expire is that someone else will pay for them if you won't, therefore, if you want to get rid of link rot, you absolutely have to get rid of capitalism.
+
+computers really are a land of contrasts, huh.
+
+# god (derogatory) saw this post and laughed <small>(1 Jun 2023)</small>
+
+![Domain renewal failed - domains deleted
+Domains by Glauca](/assets/2023-05-23-two-heresies-about-link-rot-1.png)
+
+my domain registrar deleted pig.observer with no warning, after sending me an email a few weeks ago that said the renewal was successful. i contacted support and they gave it back, though.
+
+i hadn't decided whether i actually think domain expiration is a bad thing or not - i briefly had a digression about that in the original post but i took it out for concision - but now that it has happened to a thing i still care about and that still has users, i am officially anti-domain-expiration.
+
+also the site is still down, because i haven't been able to log in and fix the NS records, because i can't connect to their control panel. and i can't ping it from my home internet, or from a datacenter in Los Angeles, or from a datacenter in London, so i think they may have fucked up their network configuration. does anybody have DNS registrar recommendations that actually support a reasonable set of TLDs? if i can't have botto.ms and pig.observer on a single professional registrar i'm going to shit a brick and cry.
+
+# changed my mind again domain expiration is funny and therefore good <small>(2 Jun 2023)</small>
+
+<div class="cohost-style-embed">
+{% renderTemplate "webc" %}
+<opengraph-embed
+ href="https://www.vice.com/en/article/4a3xe9/maryland-license-plates-now-inadvertently-advertising-filipino-online-casino"
+ site-href="https://www.vice.com"
+ site-favicon="https://www.vice.com/wp-content/uploads/sites/2/2024/06/cropped-site-icon-1.png?w=32"
+ img-src="https://www.vice.com/wp-content/uploads/sites/2/2023/05/1685481116924-s-l1600-1.png"
+ datetime="2023-05-31T13:00:00+00:00"
+>
+<span slot="title">Maryland License Plates Now Inadvertently Advertising Filipino Online Casino</span>
+A URL on the license plates of 800,000 Maryland cars now redirects to an online casino based in the Philippines.
+<span slot="site-name">VICE</span>
+<span slot="site-domain">vice.com</span>
+<span slot="date">May&nbsp;5,&nbsp;2023</span>
+</opengraph-embed>
+{% endrenderTemplate %}
+<div class="cohost-style-embed-link"><a href="https://www.vice.com/en/article/4a3xe9/maryland-license-plates-now-inadvertently-advertising-filipino-online-casino">https://www.vice.com/en/article/4a3xe9/maryland-license-plates-now-inadvertently-advertising-filipino-online-casino</a></div>
+</div>
diff --git a/_posts/2023-06-15-the-derivative-gardens-license.md b/_posts/2023-06-15-the-derivative-gardens-license.md
new file mode 100644
index 0000000..d867f3f
--- /dev/null
+++ b/_posts/2023-06-15-the-derivative-gardens-license.md
@@ -0,0 +1,18 @@
+---
+title: the Derivative Gardens Misattribution-OnlyCommercial-ShareUnlike 6.9 Unportable License
+description: an anti-license
+---
+
+### You are free to:
+
+- **Share** — copy and redistribute the material in any medium or format
+
+- **Adapt** — remix, transform, and build upon the material
+
+### Under the following terms:
+
+- **Misattribution** — You must give slightly inaccurate credit and provide a link to the wrong license. You may do so in any unreasonable manner.
+
+- **OnlyCommercial** — You may not use the material for noncommercial purposes.
+
+- **Additional restrictions** — You must apply legal terms or technological measures that legally restrict others from doing one weirdly specific thing the license permits.
diff --git a/_posts/2023-08-17-25-hour-time.md b/_posts/2023-08-17-25-hour-time.md
new file mode 100644
index 0000000..da97d6f
--- /dev/null
+++ b/_posts/2023-08-17-25-hour-time.md
@@ -0,0 +1,54 @@
+---
+title: 25-Hour Time (and the Secret Third Half of the Day)
+---
+
+you know how if you're up past midnight, on some level it's still the same day until you go to bed, no matter what the calendar says? well, if you've ever wished that there was notation to unambiguously refer to a current date and time when the current date is somewhat vibes-based, you're actually in luck, because this is a solved problem, although depending on your preferred time notation it may require some creative extension.
+
+## It Came from Public Transit
+
+<small>(Opinions, as always, are my own and not that of my employer.)</small>
+
+Some public transit systems run 24-hour service, but many shut down for a bit overnight, which is less convenient for riders but gives staff some time to reset and maybe even take care of some small maintenance tasks without disrupting any scheduled trips. Denver's D line trains, for example, start at 3:48 AM and end at 12:21 AM the next morning (on weekdays, at least). This creates some interesting problems, because if you spell "12:21 AM the next morning" as `12:21 AM`, it looks like it's before `3:48 AM`, but it isn't, so you need to spell that differently.
+
+Conveniently, this problem is solved in the¹ standard for public transit schedule data, [GTFS](https://en.wikipedia.org/wiki/GTFS). It's a machine-facing interchange format, so it uses 24-hour time for parsing simplicity, so it spells "3:48 AM" as `03:48` and "1:30 PM" as `13:30`². The way it handles times the next morning is extremely simple: if "11:59 PM" is `23:59`, then "12:00 AM the next morning" is `24:00`, and "1:13 AM the next morning" is `25:13`, and I'm writing this sentence at "2:14 AM the next morning", or `26:14`. [Apparently](https://en.wikipedia.org/wiki/24-hour_clock#Times_after_24:00), this is occasionally done in countries that use 24-hour time by businesses that run past midnight.
+
+If you say "oh yeah, i wrote that at 2023-08-16 26:20", even if people aren't familiar with beyond-24-hour time, they'll probably figure out what you mean.
+
+## Welcome to Arco AM/PM Mini-Market
+
+Unless they don't grok 24-hour time at all, like me. A trick like that doesn't really work as nicely with 12-hour time - the minute after `11:59` is already `12:00`, and by the time `12:59` rolls over to `1:00` you're an hour deep into the next half of the day anyway. If you want to have times the next morning in 12-hour time, you have to reach a little deeper into the notation.
+
+A system I was dealing with at work has a really nice solution to this. For space efficiency, it spells "11:30 AM" as `1130a` and "9:15 PM" as `915p`, and then the minute after `1159p` is `1200x`, which I suppose would expand out to "12:00 XM" but I prefer with just the `x`. For sheer intuitive clarity, it doesn't match the 25-hour solution — `x` as an abbreviation for "AM the next morning" is not something you'd be likely to just guess — but if you see it in a list of times with some `a`s and then some `p`s and then some `x`s it's not hard to guess that that's what it means. I have no idea if this sort of thing (or some other solution to this problem) is common outside my specific job.
+
+If you say "I wrote this sentence on 2023-08-16 at 229x", people will only have a clue what you mean if they're already familiar with this concept. Most people won't be, but if you're taking notes for yourself, you will be.
+
+## Completely Missing the Point of Everything
+
+But this [was originally, RIP] Cohost, and we are no strangers to goofy time notation here. I am not, myself, a .beat time enjoyer, but I know it's a feature and I think it's got several users at varying levels of irony. One of the main things I don't like about .beat time is that it also alienates you from the current date, but we're aggressively solving that in other notations, so can we solve it here too?
+
+25-hour time demonstrates that it can make sense to reach past the bounds of the time system to express times past the edge of the calendar date; there is no authority who can stop us from doing the same thing to .beat time. I'm writing this on 2023-08-16 at @1400 (which mere mortals might write 2023-08-17 @400).
+
+Terrifyingly, on account of time zones stretching in both directions, this could allow for someone west of Switzerland to experience **negative .beat time**. By my math (and the math on [SwatchClock.com](http://www.swatchclock.com/convertTime.php)), someone waking up in Sydney, Australia on 2023-08-17 at 8:00 AM local time would conventionally write that as 2023-08-16 @958 but could instead write it as 2023-08-17 @-042.
+
+The entire point of .beat time is to be absolute and global, and the whole point of this endeavor of vibes-based current date is to be anthropocentric and subjective, so this borders on heresy. In addition, it is almost completely useless as a way of communicating anything to anybody - positive overflow is at least trivial mental math to do, but turning a negative .beat time into a normal .beat time would be, at least for me, in that perfect sweet spot where it's too simple to pull up a calculator but not simple enough to be confident that I'd get the right answer doing it mentally. If you have .beat time enjoyer friends, you can probably ruin their day with a negative .beat time, but that's as much use as anyone will get out of this concept.
+
+## Exiting the Target Audience
+
+“But Cactus,” I hear you ask, “isn't this lopsided towards people who stay up inadvisably late? What about people who wake up inadvisably early? Don't they deserve wacky time notation, too?”
+
+Nope!
+
+Well, that's not fair. To be more specific:
+
+1. If you want to do negative time in 25-hour time, what's the minute before midnight? `00:-01`? `-01:59`? Both of those make me sad.
+2. Coming up with a secret fourth abbreviation for "PM the night before" is probably not difficult, but there's no canonical right answer there.
+3. Negative .beat time could cover this use case anyway, and also very much shouldn't.
+4. You're into real sickos territory by the time you're waking up as early as 4am, and that's still four hours away from needing anything like this. By contrast, staying up until like 1230x, which is in the range that benefits from this, is not really that out there.
+
+Unfortunately, it is now 27:05 / 305x / @1420 (nice), and I need to go the fuck to bed.
+
+---
+
+1. or at least *a* standard: as I recall, there are several standards for public transit schedule data, and different ones are popular in different parts of the world; GTFS is big in the US
+
+2. strictly speaking, those would be `03:48:00` and `13:30:00` on account of seconds also existing, but that's not relevant here. also not relevant here is the way GTFS avoids getting pranked by DST transitions; if you're curious and/or bored, look that one up.
diff --git a/_posts/2023-11-26-tiny-cactus-cloudtest02.md b/_posts/2023-11-26-tiny-cactus-cloudtest02.md
new file mode 100644
index 0000000..3ff3c45
--- /dev/null
+++ b/_posts/2023-11-26-tiny-cactus-cloudtest02.md
@@ -0,0 +1,29 @@
+---
+title: i got ⋖Builder of the Realm⋗ on the cloud DC
+description: a journey
+---
+
+![FFXIV character
+Tiny Cactus «Lost»
+⋖Builder of the Realm⋗
+with all crafting and gathering classes at or above level 50](/assets/2023-11-26-tiny-cactus-cloudtest02-1.png)
+
+i had hoped to get ⋖Honorary Academic⋗ but obviously that's not happening. also, it's basically impossible to finish the lv50 quests since they all require tier 3 combat materia which nobody has (i tried transmuting but it's very inefficient so i didn't get anything good). however, i did still level all the DoH/DoL through ARR in less than a week.
+
+never do this.
+
+if you aren't aware, FFXIV is testing a new cloud data center, and it opened last Monday and it gets deleted this Monday (i.e. tomorrow). they give you a boost to lv 80 for all the combat classes, but not for the crafters and gatherers, so i somehow convinced myself it'd be funny to get an achievement for leveling up my crafters and gatherers on a data center that dies soon.
+
+i'm gonna miss Tiny Cactus🌸Cloudtest02.
+
+# update: i don't have to miss Tiny Cactus🌸Cloudtest02 <small>(27 Nov 2023)</small>
+
+![Tiny-cactus standing next to a Summoning Bell](/assets/2023-11-26-tiny-cactus-cloudtest02-2.png)
+
+with the power of retainer fantasia, Tiny-cactus lives to fish another day. almost cried a little bit hanging out in [the last area of ShB] for the last couple minutes
+
+# don't talk to me or my distant cousin i rescued from a crumbling world ever again <small>(1 Dec 2023)</small>
+
+![a Lalafell and a Miqo'te standing next to each other](/assets/2023-11-26-tiny-cactus-cloudtest02-3.png)
+
+shout out to my FC for letting me swap out one of the retainers in the front yard
diff --git a/_posts/2023-12-27-no-mans-sky-unless.md b/_posts/2023-12-27-no-mans-sky-unless.md
new file mode 100644
index 0000000..10e2f9a
--- /dev/null
+++ b/_posts/2023-12-27-no-mans-sky-unless.md
@@ -0,0 +1,20 @@
+---
+title: No Man’s Sky …unless?
+description: a review
+---
+
+![NO MAN’S SKY …unless?](/assets/2023-12-27-no-mans-sky-unless-1.png)
+
+i chose to build a hazard control room instead of a tavern in my settlement; my citizens aren’t very happy, but my colony was in debt. i don’t speak the language very well, though, so when i talk to the citizens i don’t know what they’re saying. and it’s not particularly my colony, so much as i saved them from Sentinels one time and was immediately elected mayor. the second decision i had to make as overseer of the colony was between the same pair of buildings again.
+
+i sent my freighter on a mission doing satan knows what. it said it’d take an hour and a half real time, so they’re probably back now. and it's not like i built or scavenged the ship; i saved the existing crew from pirates one time and the admiral was like “hello would you like to command my ship”. the last time this happened, i said to just pay me instead, because i could not fathom why anyone would turn over command of their ship to a random stranger whose sole qualifications are saving them from pirates one time, and i suspected the ship command mechanics would be thematically inconsistent and somewhat janky; it turns out you can command an entire fleet of freighters and a squadron of smaller ships.
+
+i’ve said before that half of Dragon Age: Inquisition would be the best video game of all time. i don’t know if i’d go that far at this point, but it’s certainly got a lot of good parts and a lot of extra cruft that distracts from those good parts. and when i think about Dan Olson’s [Alone in Public](https://youtu.be/8ymRN6cCd0I) (and, as i’ve just also discovered, Hbomb’s [THE NO MAN’S SKY RANT](https://youtu.be/4DhaUe6y-Co)) and i compare it to the experience i’m having seven years later, i think of cruft, and systems that add Content™ but don’t add anything meaningful, and potential wasted not by underdelivering but by adding filler to something good, and Dragon Age: Inquisition.
+
+i also think of George Lucas. because the game that Dan and Hbomb played doesn’t exist anymore. the settlement system was added in [Update 3.6, FRONTIERS](https://www.nomanssky.com/frontiers-update/), in September 2021. the Anomaly, a teleporting space station that acts as a hub for some of the overarching story and mechanical progression, has a dozen or so other players running around in it at any given time, sending the framerate and the themes equally to hell; i’m not quite sure how it worked at launch, but the player limit was raised to 16 in [Update 2.0, BEYOND](https://www.nomanssky.com/beyond-update/), in August 2019. the freighter fleet mechanics, including the real-time missions, were added in [Update 1.5, NEXT](https://www.nomanssky.com/next-update/), in July 2018. a lot of the bad filler has been added over time. not every patch has been a change for the worse, and i’m sure you could make a case that on balance the game as it stands now is better than it was at launch, but something has certainly been lost, and not merely lost, but erased after release by an author not content to stay dead. and that erasure began in [Update 1.1, FOUNDATION](https://www.nomanssky.com/foundation-update/), in November 2016, with base building.
+
+my home base employs four total NPCs. the most confusing was the first hire, the overseer; surely it would make more sense for an NPC to oversee the settlement full of NPCs and me to oversee my own base, rather than me overseeing a settlement full of NPCs whose language i do not speak but having to hire an NPC to oversee the base i am building my damn self. my science, weapons, and farming experts are all fairly reasonable by comparison, although they all always have quests for me to do and the rewards are frequently blueprints i already unlocked by other means. one of the first things the game suggested i build at my home base was a teleporter, which renders distance itself meaningless after the first journey to a new solar system. i’ve built several other bases consisting exclusively of teleporters, because the convenience of fast travel is too hard to ignore even in a game that so many people have loved for the slow pace of travel. i don’t think all of these systems were added in FOUNDATION, but the first step into base building was a step towards this.
+
+my weapons expert, when giving me some quest or other, said i had to fight the Sentinels so it’d be “no man’s sky but ours”. and yeah, having a home base in the first place, carving out a piece of the world in which i specifically belong, undermines not just the themes that people connected with in the game as it released but the very goddamn title of the video game. what Dan and Hbomb played and loved was a game that’s about an adventure that can never pause, always moving to the next place but not belonging there either, some planets more hostile but no planets really comfortable, permanently inbetween. a lonely liminality, if you will.
+
+what i’m playing is a game that used to be about all that. and it’s fine.
diff --git a/_posts/2023-12-31-the-browser-is-a-terrible-place-for-art.md b/_posts/2023-12-31-the-browser-is-a-terrible-place-for-art.md
new file mode 100644
index 0000000..f06cc99
--- /dev/null
+++ b/_posts/2023-12-31-the-browser-is-a-terrible-place-for-art.md
@@ -0,0 +1,24 @@
+---
+title: the browser is a terrible place for art
+---
+
+condensation on the shower door is a more stable platform to develop for than the web.
+
+i’ve grumbled repeatedly before about how the web’s lack of actual versioning is a nightmare hellscape from satan, but i may as well rehash that even though it’s not the thing i’m mad about now. Java 9, for example, adds some features that were not present in Java 8, and you can decide whether to develop for Java 9 and be able to use the new features or develop for Java 8 and be compatible with older runtimes. on the web, meanwhile, we have terms like “ES2023” that are purely decorative and carry basically no meaning. all the standards are living documents, new features go in one at a time whenever it’s convenient, and they get implemented one at a time at whatever pace happens, except when a browser can’t figure out how to put a good UI on a new feature so they decide that feature is actually evil¹. the standard way to manage this is to compile your new-feature-using code into older-feature-using code based on guessing which browsers people still use and checking massive tables of which browsers support which features as of which versions, but this sucks, and no amount of vehicular manslaughter² will let you do the same thing in CSS, where you just can’t use anything invented after like 2016 because some dipshit vendor still can’t be bothered to implement it. they actually kinda had “CSS3” (along with “HTML5” and “ES6” to some extent), but the CSS people actually noticed that named standards versions are fake and split CSS up into shit like CSS Media Queries Level 3 and CSS Color Level 4. unfortunately, that hasn’t actually solved any of this, because then Firefox 101 implements like 80% of CSS Values and Units Level 4 and there’s no way to reason about which 80% without just listing off each individual feature by name.
+
+the thing i’m mad about today is that even when you somehow manage to beat the odds and make something that actually works, browsers will break it before long anyway because they don’t give a shit. [an art game](https://boringcactus.itch.io/time-is-a-cruel-mistress) i made less than two years ago just completely fails to do anything now, despite having worked at the time. the game uses the `localStorage` API to do literally anything at all, and apparently using `localStorage` in a game that gets embedded in an iframe sets off a dozen warning sirens at Mozilla HQ and throws an exception that just breaks the game. this game has more CSS than JS (by lines, if not by characters), so it blew my mind when i went to check how i had built it and it just didn’t work at all. i have no idea if i have turned on an inadvisable Firefox setting to break everything all of the time, or if this issue exists the same way in Chrome, or if it's somehow itch.io’s fault for confusing CSP reasons and i’m too eager to assume it’s the browsers’ fault, but like. browsers already all did this on purpose with moving audio playback behind less than obvious thresholds of user interaction over the objections of a ton of people whose browser-based art would just detonate if they did that; they have not earned the benefit of the doubt here.
+
+it’s not just my dubious art games, either; you can’t play the equally sophisticated but much more fun [Room of 1000 Snakes](https://arcanekids.com/snakes) anymore, because steve jobs killed the unity web player when he killed flash and java³.
+
+browsers don’t give a shit about backwards compatibility⁴. operating systems do⁵. make actual executables that can be downloaded and then played. my next game will be distributed solely via retail CD⁶. good fucking luck.
+
+---
+
+1. Mozilla on Web NFC. this is uncharitable but i do not think it is inaccurate
+2. core-js. this is irrelevant but based on the way the guy talks about it this is my own personal [the onion john lennon](https://www.theonion.com/man-always-gets-little-rush-out-of-telling-people-john-1819578998)
+3. obviously we don’t get to check in on the universe where safari on ios supports plugins to see how flash and java are doing in there, and it’s possible that goddamn adobe and motherfucking oracle would ruin everything even if apple didn’t move first. but i’m right
+4. unless they’re doing API design and it’s contains/includes time. goddamn mootools or prototypejs or whichever. if it’s an opportunity to make things worse, they will absolutely care about backwards compatibility. don’t get me started on css if/when
+5. except when they don’t, like if they’re mobile operating systems run by bored corporations, or if apple hasn’t made enough unforced errors yet this year, or if maintaining a 32-bit userland is getting annoying. so basically it’s just windows and the kinds of linux distribution that solve backwards compatibility by never moving forwards in the first place
+6. no it won’t
+7. ⁸ one of my other browser-based games is also broken but because it uses a primary server for multiplayer and that primary server was on the heroku free tier and i hardcoded the url because the previous url was getting caught up in ad blocking because apparently third-party websockets connections to the goofy donuts ass tlds like .fun are inherently suspicious. it could be fixed if i cared, but. there’s the rub
+8. this footnote does in fact not actually come from anywhere, i just wanted to also mention it because it’s tangentially related
diff --git a/_posts/2024-09-08-abdication-is-not-simplicity.md b/_posts/2024-09-08-abdication-is-not-simplicity.md
new file mode 100644
index 0000000..05d1979
--- /dev/null
+++ b/_posts/2024-09-08-abdication-is-not-simplicity.md
@@ -0,0 +1,25 @@
+---
+title: Abdication Is Not Simplicity, or cat -v Considered Harmless
+---
+
+in some sense, the [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) programming language is extremely simple. it only has eight instructions! an intermediate programmer could write an interpreter in an afternoon! that's so simple it borders on trivial! specifically, Brainfuck is simple to *implement*.
+
+*using* Brainfuck, on the other hand, is so self-evidently a bad idea that it's the entire point. even [the Wikipedia “hello world” snippet](https://en.wikipedia.org/wiki/Brainfuck#Hello_World!) would be no small task to independently discover (and even [more direct ways to print “hello world”](https://github.com/ZakiPedio/HelloWorld-in-Brainfuck/blob/main/HelloWorldBrainFuck.bf) are a bit of a mess). there is still a lot of complexity, it’s just been moved downstream, out of the implementer's hands and into the user's.
+
+it'd be absurd to claim that Brainfuck is simple, because the only real way to get any nontrivial work done in Brainfuck would be to find or create a programming language that compiles into Brainfuck, at which point Brainfuck adds nothing. there are things we reasonably expect programming languages to do, and a simple language might do them simply but Brainfuck simply doesn’t do them. i think it'd be unhelpful and misleading to call that “simplicity” when it's just not fulfilling the responsibilities that we generally expect programming languages to fulfill.
+
+fortunately, nobody actually says Brainfuck is simple. unfortunately, however, the idea that “simple” means “simple to implement, regardless of how complicated it is to use” is a long-lived meme among certain kinds of programmers. one cluster of such programmers is called [suckless](https://suckless.org/), and their backwards understanding of simplicity leads to fascinating decisions like “[the way you change the settings in this software is by editing `config.h` and recompiling it](https://dwm.suckless.org/customisation/)”. it's [software that gatekeeps itself](https://dwm.suckless.org/):
+
+> Because dwm is customized through editing its source code, it's pointless to make binary packages of it. This keeps its userbase small and elitist. No novices asking stupid questions. There are some distributions that provide binary packages though.
+
+this sucks more than [systemd or cmake or any of the software they’re butthurt about](https://suckless.org/sucks/) ever could.
+
+there's a semi-overlapping cluster of annoying fossbro nonsense over on [cat-v.org](https://cat-v.org/). some of it is [the same boomer nonsense about old software good new software bad](https://harmful.cat-v.org/software/), and some of it is more directly venerating Rob Pike and The UNIX Philosophy™. the website is named after [a talk Rob Pike gave in 1983](https://harmful.cat-v.org/cat-v/), “UNIX Style, or cat -v Considered Harmful”. that page also links to [a paper](https://harmful.cat-v.org/cat-v/unix_prog_design.pdf) by Rob Pike and Brian Kernighan, which is a really interesting case study in The UNIX Philosophy™.
+
+what is `cat`? if you ask someone who knows a bit about the command line on macOS or Linux, they’ll tell you it displays the contents of a file, but if you ask a proper UNIX dweeb they’ll tell you it *concatenates* files (hence the name) and it’s a happy accident that it’ll display the contents of a single file. as Pike and Kernighan put it,
+
+> The fact that cat will also print on the terminal is a special case. Perhaps surprisingly, in practice it turns out that the special case is the main use of the program.
+
+and right away we have encountered a flaw in the UNIX Philosophy™ tenet that a program should do one thing well. `cat` does two things well: concatenating files and displaying them. a program that displays files well would very reasonably benefit from options to add line numbers or display non-printing characters (in BSD, `cat -s` and the titular `cat -v`), but Pike and Kernighan are right that a program that concatenates files well would need no such options.
+
+but if displaying a file is the main use of `cat`, then it is absurd to argue that displaying a file is not the one thing it should do well. Pike and Kernighan suggest adding line numbers with `awk` and displaying non-printing characters with a new program `vis`, to maintain the purity of `cat` for concatenating files, but that sucks. `cat` is responsible for displaying files, and as such if it can’t meet reasonable requests about displaying files, that’s not simplicity, that’s an abdication of that responsibility. `cat -v` is good, and 1983 Rob Pike was wrong, and The UNIX Philosophy™ is bogus.
diff --git a/_posts/2024-09-20-eggbug-forever-ffxiv.md b/_posts/2024-09-20-eggbug-forever-ffxiv.md
new file mode 100644
index 0000000..f916b5e
--- /dev/null
+++ b/_posts/2024-09-20-eggbug-forever-ffxiv.md
@@ -0,0 +1,28 @@
+---
+title: "Eggbug Forever: a custom minion mod for FFXIV"
+description: In memory of https://cohost.org. The real eggbug is the friends we made along the way. We carry him with us forever.
+---
+
+![a purple haired catgirl /showleft-ing to highlight eggbug floating in the air](/assets/2024-09-20-eggbug-forever-ffxiv-1.png)
+
+The real eggbug was the friends we made along the way. We carry him with us forever.
+
+A very simple import of Xenon Fossil’s [low-poly eggbug model](https://cohost.org/xenonfossil/post/7629261-https-xenonfossil) replacing the Wind-up Airship and/or the Great Serpent of Ronka (the Great Serpent animations are adorable but the rigging is a little janky so the Wind-up model may be better for gposing).
+
+[XIV Mod Archive](https://www.xivmodarchive.com/modid/117001) | [Heliosphere](https://heliosphere.app/mod/qbmgcyzy3n49dc90djpppegp0c)
+
+---
+
+# <small>(30 Sep 2024)</small>
+
+<div style="display: flex; flex-flow: row; align-items: center">
+<img alt="eggbug wiggling and bouncing around next to a catgirl sitting on the ground in ffxiv" src="/assets/2024-09-20-eggbug-forever-ffxiv-2.gif" style="max-width: 50%" /><img alt="eggbug bouncing along behind a catgirl running towards the camera in ffxiv" src="/assets/2024-09-20-eggbug-forever-ffxiv-3.gif" style="max-width: 50%" />
+</div>
+
+eggbug forever. see y’all around.
+
+<div style="font-family: 'Atkinson Hyperlegible', sans-serif; font-size: 0.875rem; line-height: 1.25rem; color: #686664">
+
+[#ffxiv](https://cohost.org/cactus/tagged/ffxiv) [#eggbug](https://cohost.org/cactus/tagged/eggbug) [#Goodbye cohost](https://cohost.org/cactus/tagged/Goodbye%20cohost) [#last one out hit the lights](https://cohost.org/cactus/tagged/last%20one%20out%20hit%20the%20lights) [#but for real this time](https://cohost.org/cactus/tagged/but%20for%20real%20this%20time)
+
+</div>
diff --git a/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md b/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md
new file mode 100644
index 0000000..8afc523
--- /dev/null
+++ b/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md
@@ -0,0 +1,1395 @@
+---
+title: A 2025 Survey of Rust GUI Libraries
+---
+
+I did this [in 2020](/_posts/2020-08-21-survey-of-rust-gui-libraries.md) and then again [in 2021](/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md), but I’m in the mood to look around again.
+Let’s look through [Are We GUI Yet?](https://www.areweguiyet.com/) and see what’s up these days.
+
+The task today is to have a text label and an input field that can change the text in the label.
+In React, for example, this is basically free:
+
+```jsx
+const Demo = () => {
+ let [state, setState] = useState("Hello, world!");
+
+ return (
+ <div>
+ <p>{state}</p>
+ <input type="text" value={state} onInput={evt => setState(evt.target.value)} />
+ </div>
+ );
+}
+```
+
+Choosing a task this simple means I can actually have a shot at completing this at a reasonable pace (although it took me two weeks), but it also means frameworks that prioritize scaling over initial setup will be at a disadvantage here.
+If you’ve found this post looking for specific guidance for a project that’s going to be substantially more complicated than this, don’t assume my conclusions are valid in your context.
+
+A few other reasons my context may not match yours:
+
+- I’m developing this, like nearly all my personal projects, on Windows.
+ I’m in good company there — per the [2024 Stack Overflow developer survey](https://survey.stackoverflow.co/2024/technology/#1-operating-system), “Windows is the most popular operating system for developers, across both personal and professional use” — but for a handful of reasons Windows is an afterthought in a lot of open source development.
+ I find some of those reasons more compelling than others, but for GUI libraries in particular I think avoiding Windows is avoiding success, and if Windows support is lower on your roadmap than trend chasing AI bullshit, you are not serious.
+- I am checking that the text label can be read out from Windows Narrator.
+ Screen reader accessibility is another frequent afterthought, and it’s not load-bearing for me personally but it’s a lot more important as a matter of principle (and potentially a matter of law, depending on the project).
+- New in the 2025 version of this exercise: I will be using the [Windows Japanese IME](https://go.microsoft.com/fwlink/?linkid=2007440) to type in the kanji for Tokyo, <span lang="ja">`東京`</span> (which on my US-layout-emulating keyboard I do with `toukyou<Tab><Return>`).
+ I don’t speak Japanese (although there are [more obscure IMEs](https://github.com/dec32/Ajemi) that I’m more interested in), and there are a lot of internationalization pieces that a minimal GUI library can reasonably decide to ignore, but if you’ve implemented text fields from scratch and you’re just appending keystrokes to a buffer then you have rejected compatibility with a lot of languages that are more complicated than that.
+
+I’m feeling very slightly more patient this time around, so I’m not going to give up instantly if something takes very slightly more setup than just `cargo add`, but I’ve got a lot of things to check, so that’s gotta be enough preamble.
+
+I’m writing this in linear order in parallel with my development, so it’s more of a journal than a reference and it probably reads best top to bottom, but if you want a TL;DR or you’re coming back for reference you can skip right to [the conclusion](#conclusion) or [the table](#the-table).
+
+## Azul
+
+[Azul](https://azul.rs/) is the first beneficiary of my newfound patience: you have to manually download the prebuilt `.dll` (via a link that doesn’t quite work, or directly off the GitHub release), and the last time I was here I balked at that request.
+It’s not a great sign that the [getting started guide](https://azul.rs/guide/1.0.0-alpha1/GettingStarted) has samples for C++ and Python but not Rust, but there are [examples in the repo](https://github.com/fschutt/azul/tree/master/examples/rust) that aren’t too hard to follow.
+However, the hello world sample doesn’t actually work if I copy and paste it into my `main.rs` - it looks like the API has changed somewhat since the latest release.
+
+There’s a bit of a theme of release versioning issues with Azul - following the guide appears to give me version `1.0.0-alpha4`, but the only Git tag is `1.0.0-alpha1` and it’s not clear what may have changed between alpha1 and alpha4.
+
+The broader issue, though, is that even if I download the `1.0.0-alpha1` examples off the GitHub release and try to run the same code myself, I am beset with `error LNK2019: unresolved external symbol __imp_AzCallbackInfo_getNodeIdOfRootDataset` and 47 other unresolved symbols.
+I made an honest attempt to get Azul working, but it still doesn’t work.
+I’ll see you in a couple years, Azul.
+
+## cacao
+
+[Cocoa](https://en.wikipedia.org/wiki/Cocoa_(API)) is some subset of the macOS API; it has [Rust bindings](https://github.com/ryanmcgrath/cacao) named Cacao.
+I could have *sworn* that the cocoa/cacao wordplay had been done forever ago in the macOS space, but maybe I just couldn’t fucking read, because the only things I’m able to find are this crate.
+Unsurprisingly, this does not work on Windows.
+
+## core-foundation
+
+[Core Foundation](https://en.wikipedia.org/wiki/Core_Foundation) is a different subset of the macOS API; it has [Rust bindings](https://github.com/servo/core-foundation-rs).
+Also not useful from Windows.
+
+## Crux
+
+[Crux](https://github.com/redbadger/crux/) is new, and I find it really intriguing.
+The idea of writing a shared library with business logic and then writing an ideally minimal native UI shell around it is also how Kotlin Multiplatform works (if you adopted it before Compose Multiplatform on iOS was out of alpha, at least), and I’ve been using that at my day job for a year and a half with only a handful of complaints.
+The [initial project setup](https://redbadger.github.io/crux/getting_started/core.html) accurately describes itself as a sharp edge that needs better tooling, but it’s not miserable.
+
+[Manually defining](https://redbadger.github.io/crux/getting_started/core.html#the-interface-definitions) the entire shared library interface feels like it would get old fast, but this task is faster.
+Unfortunately, though, I’m just now processing that Crux doesn’t actually support desktop GUI development, only mobile and web!
+It’s very interesting that this exists, though, and if my aim today was actually mobile development I’d be very curious if actual Swift bindings solve my Kotlin Multiplatform woes (I suspect that they might).
+
+Hang on, though, this isn’t even a GUI library, the whole point is that you still use the native GUI library directly from each platform, and in fact several of the web examples use libraries that will be coming back later in this very list.
+I’m not quite certain I agree with listing it on Are We GUI Yet? in the first place, although I guess the current design doesn’t have space for a separate section for non-GUI frameworks that would be useful for GUI applications.
+
+## Cushy
+
+[Cushy](https://crates.io/crates/cushy) is another new entrant - apparently there’s been a lot of movement in the space in the last four years.
+The README code snippet has a funny but inconsequential mistake:
+
+```rust
+// Create a dynamic usize.
+let count = Dynamic::new(0_isize);
+```
+
+Maybe one day clippy will be able to detect comments that don’t actually match the code.
+
+The README example actually works (it’s a Christmas miracle!) but it spams stderr with a ton of eyebrow-raising Vulkan/DirectX 12 errors:
+
+```text
+2025-04-03T04:20:03.810927Z ERROR wgpu_hal::auxil::dxgi::exception: ID3D12CommandQueue::ExecuteCommandLists: Using ClearRenderTargetView on Command List (0x0000026C511CF250:'Unnamed ID3D12GraphicsCommandList Object'): Resource state (0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT]) of resource (0x0000026C510E4EB0:'Unnamed ID3D12Resource Object') (subresource: 0) is invalid for use as a render target. Expected State Bits (all): 0x4: D3D12_RESOURCE_STATE_RENDER_TARGET, Actual State: 0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT], Missing State: 0x4: D3D12_RESOURCE_STATE_RENDER_TARGET. [ EXECUTION ERROR #538: INVALID_SUBRESOURCE_STATE]
+2025-04-03T04:20:03.812586Z ERROR wgpu_hal::auxil::dxgi::exception: ID3D12CommandQueue::ExecuteCommandLists: Using IDXGISwapChain::Present on Command List (0x0000026C51057910:'Internal DXGI CommandList'): Resource state (0x4: D3D12_RESOURCE_STATE_RENDER_TARGET) of resource (0x0000026C510E4EB0:'Unnamed ID3D12Resource Object') (subresource: 0) is invalid for use as a PRESENT_SOURCE. Expected State Bits (all): 0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT], Actual State: 0x4: D3D12_RESOURCE_STATE_RENDER_TARGET, Missing State: 0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT]. [ EXECUTION ERROR #538: INVALID_SUBRESOURCE_STATE]
+```
+
+It’s probably fine, though.
+The actual application is about as simple as you’d hope it’d be:
+
+```rust
+let text = Dynamic::new("Hello, world!".to_string());
+let label = text.map_each(|text| text.clone());
+let text_input = text.into_input();
+label.and(text_input).into_rows().run()
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-cushy.png)
+
+Unfortunately, Windows Narrator has no idea what’s inside this window.
+Kanji input with the IME sorta works, though; I don’t get to see the <span lang="ja">`とうきょう`</span> that my `toukyou` becomes as I type it, but when I press Return I do in fact get the kanji I was expecting.
+
+In IME jargon, turning `toukyou` into <span lang="ja">`とうきょう`</span> is the job of the *composer* and turning <span lang="ja">`とうきょう`</span> into <span lang="ja">`東京`</span> is the job of the *converter*, so the composer step is hidden but the converter step works fine.
+
+I’m not sure whether Cushy has done anything in particular to accept IME results in its text input widgets or if the IME just dispatches the selected kanji as though they were typed directly, but I suspect it’s the latter.
+I know Windows is pretty flexible with how it treats keyboard input — every curly quote in this blog post was hand-curled with [WinCompose](https://wincompose.info/) despite my Markdown renderer almost certainly doing smart quotes automatically — so maybe the IME result is just dispatched as though it’s a direct input of U+6771 CJK Unified Ideograph and then U+4EAC CJK Unified Ideograph.
+(I had forgotten about Han unification; I’m curious how if at all using the Japanese IME causes the Japanese kanji to be displayed instead of the theoretically-equivalent hanzi/hanja, but I suspect the answer is that it doesn’t, and that scares me.)
+
+## CXX-Qt
+
+[CXX-Qt](https://kdab.github.io/cxx-qt/book/) is a framework for using the well-established Qt C++ GUI library from Rust.
+I have been avoiding Qt for years, but it seems like it’s time to stop.
+
+It’s annoying that I have to make an account to install Qt, and it’s very annoying that they want my name and location before they’ll let me download it.
+I was right to hate this the whole time.
+
+Their sample code will not run due to 1058 linker errors:
+
+```text
+ Creating library C:\Users\Melody\Projects\misc\2025\rust-gui-survey\target\debug\deps\cxx_qt_demo.lib and object C:\Users\Melody\Projects\misc\2025\rust-gui-survey\target\debug\deps\cxx_qt_demo.exp␍
+0245cd17b2ba3548-com_kdab_cxx_qt_demo_plugin_init.o : error LNK2019: unresolved external symbol "__declspec(dllimport) void __cdecl qRegisterStaticPluginFunction(struct QStaticPlugin)" (__imp_?qRegisterStaticPluginFunction@@YAXUQStaticPlugin@@@Z) referenced in function "public: __cdecl Staticcom_kdab_cxx_qt_demo_pluginPluginInstance::Staticcom_kdab_cxx_qt_demo_pluginPluginInstance(void)" (??0Staticcom_kdab_cxx_qt_demo_pluginPluginInstance@@QEAA@XZ)␍
+libcxx_qt_lib-c91f193d907e83ae.rlib(badec7f11aadc5df-qcoreapplication.o) : error LNK2001: unresolved external symbol "__declspec(dllimport) void __cdecl qt_assert(char const *,char const *,int)" (__imp_?qt_assert@@YAXPEBD0H@Z)␍
+<...>
+libcxx_qt-464ae71fe424547a.rlib(0602fb52cb66f316-connection.o) : error LNK2019: unresolved external symbol "__declspec(dllimport) public: __cdecl QMetaObject::Connection::Connection(void)" (__imp_??0Connection@QMetaObject@@QEAA@XZ) referenced in function "class QMetaObject::Connection __cdecl rust::cxxqt1::qmetaobjectconnectionDefault(void)" (?qmetaobjectconnectionDefault@cxxqt1@rust@@YA?AVConnection@QMetaObject@@XZ)␍
+libcxx_qt-464ae71fe424547a.rlib(0602fb52cb66f316-connection.o) : error LNK2019: unresolved external symbol "__declspec(dllimport) public: static bool __cdecl QObject::disconnect(class QMetaObject::Connection const &)" (__imp_?disconnect@QObject@@SA_NAEBVConnection@QMetaObject@@@Z) referenced in function "bool __cdecl rust::cxxqt1::qmetaobjectconnectionDisconnect(class QMetaObject::Connection const &)" (?qmetaobjectconnectionDisconnect@cxxqt1@rust@@YA_NAEBVConnection@QMetaObject@@@Z)␍
+C:\Users\Melody\Projects\misc\2025\rust-gui-survey\target\debug\deps\cxx_qt_demo.exe : fatal error LNK1120: 1058 unresolved externals␍
+```
+
+I suspect it’s the entire Qt standard library that’s missing.
+The linker args all look reasonable, though — it’s looking for `C:/Qt/6.9.0/mingw_64/lib\libQt6Qml.a`, which aside from the mixed slashes is a real path that exists — so I’m not sure what the problem is.
+
+This is a complete bust.
+There’s a section in the docs about building projects with CMake instead of cargo, but it says it’s optional, and it’s not like I’m desperate for more opportunities to use CMake, so I’m not going to try it.
+
+## Dioxus
+
+I think I remember [Dioxus](https://dioxuslabs.com/) as being one of the Rust frontend web dev frameworks; apparently they’ve branched out.
+
+Their tutorial involves building “*HotDog* - basically Tinder, but for dogs!”, and by that they mean the app lets you swipe through a pile of dog photos and then view the ones you’ve swiped whichever direction is good on.
+That is not what I would expect “Tinder, but for dogs” to be, but maybe I don’t know what Tinder is.
+
+Apparently the way Dioxus supports desktop development is through WebView2/WebKitGTK, so they haven’t branched very far out.
+I’m a little bit skeptical that Diet Electron is really the future, but given that [Electron](https://en.wikipedia.org/wiki/Electron_(software_framework)) is the present, maybe I need to take what I can get.
+I also have some concerns about leaning this hard into cloning React — React hooks are a fascinating hack to almost build algebraic effects in JS, and it’s not like Rust really has algebraic effects, either, but maybe the real compilation step means they can do a little more magic and make it actually work reasonably.
+At this scale, though, it’s hard to argue with the results:
+
+```rust
+let mut text = use_signal(|| "Hello, world!".to_string());
+rsx! {
+ p { "{text}" }
+ input {
+ type: "text",
+ oninput: move |event| text.set(event.value()),
+ value: "{text}"
+ }
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-dioxus.png)
+
+Windows Narrator can even see what the text is, although it feels a little clumsy (it’s saying “Web content region” on its way into the body of the frame).
+The IME works perfectly, too.
+I guess there are benefits to letting Chrome-via-Edge-via-WebView2 be responsible for all the UI machinery.
+
+Dioxus did not invent the Diet Electron approach (that was Tauri, whose WebView2/WebKitGTK/macOS things library Dioxus builds its desktop support on), but the Rust-all-the-way-through approach feels like a way better idea than how Tauri works, which I’ll get into once I make it that far through my list.
+If Diet Electron is really the best thing out there, I may be a little bit sad, but it’s probably possible to use Dioxus for real work without constantly being miserable, and that’s a new high water mark for this blog series.
+
+## Dominator
+
+[Dominator](https://github.com/Pauan/rust-dominator) is a Web-only UI crate, and unlike Dioxus it does not also offer a blessed desktop stack.
+
+## egui
+
+[egui](https://www.egui.rs/), and its framework `eframe`, have been around for a while.
+The setup process has always been pretty straightforward, which is nice.
+It’s pretty simple to use, too:
+
+```rust
+egui::CentralPanel::default().show(ctx, |ui| {
+ ui.label(&self.label);
+ ui.text_edit_singleline(&mut self.label);
+});
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-egui.png)
+
+Windows Narrator can even see this text!
+It feels a little janky trying to get Narrator into the text field, though, but maybe that’s just the ceiling of how well Windows Narrator can work, or maybe I’m just holding it wrong and there’s a way to get behavior that feels more intuitive that I just can’t think of.
+
+The default font doesn’t have hiragana or kanji coverage, though, and if I manually load a system font (which requires loading the bytes directly instead of just specifying a system font name, which is suboptimal but probably common), my Tab press to select <span lang="ja">`東京`</span> as the kanji for <span lang="ja">`とうきょう`</span> gets eaten by egui and I’m stuck with the hiragana forever.
+
+I prefer this default appearance to Cushy’s or Dioxus’s, although it’s probably possible to make anything look like anything if you try hard and believe in yourself.
+I’m not sure I love immediate mode on principle, although at this scale it extremely doesn’t matter.
+
+<details>
+<summary>Digression: “Immediate mode” and “retained mode”</summary>
+<aside>
+
+In an “immediate mode” GUI, your code that builds your widgets runs every frame, and the widgets don’t really _exist_ per se.
+This can be simpler to work with — I can create the label with `&self.label` and the text edit widget with `&mut self.label` and there’s no issue because the label and the text field don’t actually exist at the same time.
+I think it’s also easier to integrate into a game engine, since the GUI library doesn’t need as much control over communication with the GPU; that doesn’t matter for me today, but it may matter for you.
+
+“Retained mode” is the opposite of that, where the code that builds the widgets only runs when it has something to do, and the widgets keep existing after they’re created.
+My understanding is that this can have better performance since you aren’t rebuilding your entire UI from scratch every frame, although presumably serious immediate mode libraries do some amount of caching under the hood so you aren’t actually rebuilding your entire UI from scratch every frame.
+I’m not sure how much of a difference it actually makes; it would be impossible to tell with anything this small.
+
+</aside>
+</details>
+
+The fact that it’s possible to render your UI yourself and still have real accessibility support is definitely a good thing, and if it weren’t for the weird IME issues this would be perfect.
+If you don’t need IME support and you want better styles out of the box or an immediate mode library you can plug into your existing game engine, egui seems like a perfectly reasonable choice, and that IME issue will probably get fixed eventually.
+
+## Floem
+
+[Floem](https://lap.dev/floem/) is the UI framework developed for [Lapce](https://lap.dev/lapce/), the cooler VSCode-but-in-Rust IDE.
+I haven’t used Lapce in a while — several years ago when I last checked, its support for non-Rust languages was pretty weak, and I don’t actually prefer VSCode-style lightweight IDEs anyway (I pay for the JetBrains suite despite not doing enough personal development to justify that expense) — but everything that exists and is good enough for me once existed and was not.
+It’s cool that it exists; let’s see if their UI framework is any good.
+
+Getting from zero to today’s sample is pretty straightforward:
+
+```rust
+let label = create_rw_signal("Hello, world!".to_owned());
+(
+ dyn_view(move || label),
+ text_input(label),
+).style(|s| s.flex_col())
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-floem.png)
+
+Windows Narrator can’t see any of this text, and the IME won’t even start, I’m stuck with “toukyou” forever.
+
+Building layouts out of tuples feels a little bit weird — you can only have [up to 16](https://docs.rs/floem/0.2.0/floem/trait.IntoView.html#impl-IntoView-for-(A,+B,+C,+D,+E,+F,+G,+H,+I,+J,+K,+L,+M,+N,+O,+P)) widgets directly within a container at once, due to tuple generics in Rust being [still obviously incomplete after 11 years and counting](https://github.com/rust-lang/rfcs/issues/376) — but it’s probably better than Cushy’s `.and()`.
+The complete lack of accessibility or IME support is the real issue, though.
+Maybe one day they’ll fix it, but for now, this is no good.
+
+## fltk
+
+[FLTK](https://www.fltk.org/) is a C++ library with [Rust bindings](https://github.com/fltk-rs/fltk-rs).
+Conveniently, the Rust bindings offer a `bundled` feature so I don’t have to figure out how to build FLTK from source on Windows.
+Unfortunately, FLTK doesn’t appear to have an idea of widgets having an inherent size, and its whole layout subsystem leaves something to be desired:
+
+```rust
+let app = App::default();
+let mut wind = Window::new(100, 100, 400, 300, "Hello from rust");
+let mut pack = Pack::default_fill().with_type(PackType::Vertical);
+let mut label = Frame::default();
+label.set_label("Hello, world!");
+let mut input = Input::default();
+input.set_value("Hello, world!");
+input.set_callback(move |input| label.set_label(&input.value()));
+input.set_trigger(CallbackTrigger::Changed);
+pack.end();
+pack.auto_layout();
+wind.end();
+wind.show();
+app.run().unwrap();
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-fltk.png)
+
+Windows Narrator has no idea what’s going on in here, but I did notice a two-year-stale [fltk-accesskit](https://github.com/fltk-rs/fltk-accesskit) repo under the `fltk-rs` org, and that works but it requires its own ugly setup (you have to pass it a redundant list of all your widgets, which is tough because I moved one of them into the callback for the other).
+The IME works perfectly, which I wasn’t expecting.
+
+The main issue here is the layout subsystem, which appears to have no concept of widgets having intrinsic sizes.
+You can manually position and size everything, or you can use one of the clumsy automatic layouts, but neither of those is particularly satisfying.
+The fact that adding widgets to containers happens in implicit global state and you have to `.end()` a container to stop adding items to it is a little bit horrifying, I can’t lie.
+I don’t like this API design one bit.
+
+## flutter_rust_bridge
+
+[Flutter](https://flutter.dev/) is a Google framework for cross-platform UI development, but you use it from [Dart](https://dart.dev/).
+Dart has [`switch` statements](https://dart.dev/language/branches#switch-statements) and [`switch` expressions](https://dart.dev/language/branches#switch-expressions) with completely different syntax.
+Dart sucks.
+Maybe using Flutter from Rust doesn’t suck?
+
+It’s very funny to me that Flutter for Windows [claims](https://docs.flutter.dev/get-started/install/windows/desktop#hardware-requirements) you absolutely need a 1366x768 display; as an act of spite I will be disabling my primary display and only using my 1024x768 secondary display for the remainder of this section.
+
+Oh god this is cramped, I regret this already.
+
+Flutter hates my MSVC toolchain for some reason, and the Visual Studio installer does not want to be this narrow, but if I tab offscreen or move the window to the side I can still add the right components.
+Apparently “MSVC v143 - VS 2022 C++ x64/x86 build tools” and “Windows 11 SDK” aren’t good enough, and Flutter absolutely insists on having specifically “MSVC v142 - VS 2019 C++ x64/x86 build tools” and “Windows 10 SDK”.
+
+If I install those, though, `flutter doctor` still doesn’t think I have them for some reason.
+It seems like my issue, which `flutter doctor` failed to detect for some reason, was that I didn’t have the “Desktop development with C++” workload installed in Visual Studio.
+
+Getting flutter_rust_bridge set up is easy once Flutter itself is working, although having to run `flutter_rust_bridge_codegen generate` explicitly is no good; it can’t hook into the Flutter/Dart build process because [the only piece that anything can hook into](https://dart.dev/tools/build_runner) runs completely outside the Flutter/Dart build process.
+However, what you actually get with flutter_rust_bridge is the opportunity to write your business logic in Rust and your UI in Dart still.
+You can write your UI state in Rust if you want, but you still have to define your widgets in Dart:
+
+```rust
+#[frb(ui_state)]
+pub struct RustState {
+ pub label: String,
+}
+
+impl RustState {
+ pub fn new() -> Self {
+ Self {
+ label: "Hello, world!".to_owned(),
+ base_state: Default::default(),
+ }
+ }
+
+ #[frb(ui_mutation)]
+ pub fn set_label(&mut self, label: String) {
+ self.label = label;
+ }
+}
+```
+
+```dart
+void main() => runRustApp(body: body, state: RustState.new);
+
+Widget body(RustState state) {
+ return Column(
+ children: [
+ Text(state.label),
+ TextField(
+ controller: TextEditingController(text: state.label),
+ onChanged: (text) => state.setLabel(label: text),
+ ),
+ ],
+ );
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-flutter-rust-bridge.png)
+
+This doesn’t even actually work, though: typing happens in reverse in the input field, because the `TextEditingController` contains the caret position but keeps getting reset.
+In pure Flutter, you’d probably solve this by moving the controller into the widget state, but it’s not obvious how to do that here since our widget state is being defined in Rust instead.
+Windows Narrator appears to be able to see this text, at least, although it feels janky trying to move between the label and the input.
+The IME sort of works, but for presumably the same `TextEditingController` reasons, the intermediate states aren’t being cleared as I type in the IME, so the actual value that I’ve entered with `toukyou<Tab><Return>` is `東京東京東京東京とうきょうとうきょうとうきょとうきょとうkyとうkyとうkとうkとうとうととt`.
+
+I’d get slightly better functionality if I moved this widget state to Flutter, I’m sure, but then there’d be no Rust code at all.
+If I wanted to write my UIs in Flutter, I’d just do that.
+If you want to write your UIs in Flutter and just some business logic in Rust, this might work alright for you, but that is not what I want.
+
+## Freya
+
+Per the README, [Freya](https://freyaui.dev/) is “a cross-platform GUI library for Rust powered by 🧬 Dioxus and 🎨 Skia.”
+[Evidently](https://book.freyaui.dev/differences_with_dioxus.html), it takes the logic and structure of Dioxus and but renders everything itself instead of using Diet Electron.
+I did grumble about the Diet Electron-hood of Dioxus, so maybe this is the exact thing I was hoping for all along.
+
+*Edit 2025-04-15*: I’ve been asked to run this again with the week-old release candidate rather than the year-old latest stable release, and I’ve ignored other year-old latest stable releases so it’s fair to do that here too.
+Next time, unless I forget to, I’m deciding up front when to use a stable release vs an unstable release vs the main branch.
+
+Freya’s README example uses `rsx!()` rather than the Dioxus docs’ `rsx! {}`, and it still feels weird to me that those are interchangeable, but aside from that, the only difference from the Dioxus code is some component names:
+
+```rust
+let mut text = use_signal(|| "Hello, world!".to_string());
+rsx!(
+ label { "{text}" }
+ Input {
+ value: text.read().clone(),
+ onchange: move |value| text.set(value),
+ }
+)
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-freya.png)
+
+Narrator sees the text label fine, but something’s gone wacky with the text input: Narrator can see that there’s a text input with those bounds on the screen, but it doesn’t seem like it actually knows the contents of the text input.
+The IME shows up in the corner of the screen, and the provisional states aren’t displayed at all, but the final kanji land correctly in the text input once I accept them from the converter.
+
+There are some drawbacks to rendering things yourself instead of letting Chrome-via-Edge-via-WebView2 do it for you, it seems.
+Regardless, it’s extremely cool that Dioxus is set up in a way that makes this possible, and it’s extremely cool that someone’s trying to do it.
+I’m not sure that Freya is quite ready for serious use yet, but it’s definitely promising.
+
+## fui
+
+[FUI](https://github.com/marek-g/rust-fui) does not have a lot of high-level documentation I can use to figure out what to put here.
+
+The example in the README has drifted from the examples in the code, which I suppose is natural but which is rarely auspicious.
+
+Even less auspicious is that I can’t build `fui_system`:
+
+```text
+qmake.stderr: Project ERROR: Cannot run compiler 'g++'. Output:
+===================
+===================
+Maybe you forgot to setup the environment?
+```
+
+I can find no documentation about how to set up my environment, and if I really need `g++` then that’s not great.
+
+## GemGui
+
+The last commit to [GemGui](https://github.com/mmertama/gemgui-rs) was two years ago; that’s rarely a good sign.
+
+GemGui leans even further into Diet Electron by actually running your frontend on an HTTP server and by default just opening it in your regular browser.
+There’s a setting to run it in its own application frame instead, but that appears to have a load-bearing dependency on Python being installed in a way that I currently don’t have it installed.
+It also looks like you’re only really intended to define the UI elements in HTML and wire up the business logic in Rust.
+
+If I give this a venv and then manually ensure `python3` will do the right thing, that isn’t enough, because the Python dependency doesn’t actually work or something.
+“Embed an HTTP server and then use a Web framework and open your server in the system default browser” is a very boring way to technically claim that you’re doing GUI development.
+
+This repo has four stars on GitHub.
+Why is it even listed in Are We GUI Yet?
+
+## GPUI
+
+[GPUI](https://gpui.rs/) is the UI framework developed for [Zed](https://zed.dev/), the other VSCode-but-in-Rust IDE.
+Are We GUI Yet? links to [someone squatting it on crates.io](https://crates.io/crates/gpui), which is silly.
+
+Remember this piece from the intro?
+
+> if Windows support is lower on your roadmap than trend chasing AI bullshit, you are not serious.
+
+Well, that’s Zed.
+It’s a heavily-LLM-focused IDE with [no Windows support](https://github.com/zed-industries/zed/issues/5391).
+GPUI appears to work alright on Windows, though.
+
+It looks like GPUI doesn’t have a basic text input widget, though; their [text input example](https://github.com/zed-industries/zed/blob/a2fbe82c42601221482e8422d7f8db5fee649b8e/crates/gpui/examples/input.rs) is over 700 lines of code.
+It’s possible to shuffle that example around to at least get something that’ll meet the task I’m working on, though.
+
+```rust
+// this isn’t even the bad part!
+div()
+ .flex()
+ .key_context("TextInput")
+ .track_focus(&self.focus_handle(cx))
+ .cursor(CursorStyle::IBeam)
+ .on_action(cx.listener(Self::backspace))
+ .on_action(cx.listener(Self::delete))
+ .on_action(cx.listener(Self::left))
+ .on_action(cx.listener(Self::right))
+ .on_action(cx.listener(Self::select_left))
+ .on_action(cx.listener(Self::select_right))
+ .on_action(cx.listener(Self::select_all))
+ .on_action(cx.listener(Self::home))
+ .on_action(cx.listener(Self::end))
+ .on_action(cx.listener(Self::show_character_palette))
+ .on_action(cx.listener(Self::paste))
+ .on_action(cx.listener(Self::cut))
+ .on_action(cx.listener(Self::copy))
+ .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
+ .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
+ .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
+ .on_mouse_move(cx.listener(Self::on_mouse_move))
+ .bg(rgb(0xeeeeee))
+ .line_height(px(30.))
+ .text_size(px(24.))
+ .child(
+ div()
+ .h(px(30. + 4. * 2.))
+ .w_full()
+ .p(px(4.))
+ .bg(white())
+ .child(TextElement {
+ input: cx.entity().clone(),
+ }),
+ )
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-gpui.png)
+
+Narrator has no idea what’s going on inside this window (and it’s in good company there).
+The IME works fine, though.
+
+I’m not sure you’re actually supposed to use GPUI at this stage; the documentation is spotty, the installation is janky, and the standard library is woefully inadequate.
+But at least you can generate bad code way faster, and clearly that’s enough to get your Series A in.
+Was inflicting Electron on us all by way of Atom not enough?
+
+## GTK 3
+
+> UNMAINTAINED Rust bindings for the GTK+ 3 library (use gtk4 instead).
+
+OK then.
+
+## GTK 4
+
+[GTK](https://gtk.org/) is the GNOME toolkit (although apparently that’s not actually what it stands for); it’s got [Rust bindings](https://gtk-rs.org/).
+Conveniently, there are [specific installation instructions for Windows](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html).
+It’s not clear whether or not just downloading the prebuilt binaries would work, and it’d certainly save a lot of time if they would, but I’m going to assume building the binaries myself will be more likely to work.
+That only took five minutes, astonishingly.
+
+I find it a little bit counterintuitive that the single-line text widget is named `Entry`, but it does kinda rule that GTK’s property bindings mean I don’t have to keep any state at all:
+
+```rust
+let label = Label::builder()
+ .label("Hello, world!")
+ .build();
+
+let entry = Entry::builder()
+ .text("Hello, world!")
+ .build();
+
+entry
+ .bind_property("text", &label, "label")
+ .build();
+
+let r#box = Box::builder()
+ .orientation(Orientation::Vertical)
+ .build();
+r#box.append(&label);
+r#box.append(&entry);
+
+let window = ApplicationWindow::builder()
+ .application(app)
+ .title("My GTK App")
+ .child(&r#box)
+ .build();
+
+window.present();
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-gtk4.png)
+
+Not only can Narrator not see this text, it can’t even see the minimize/maximize/close buttons, and that’s not a failure state I had realized was possible.
+The IME works fine.
+
+You may have noticed that this is not the idiomatic Windows window decoration; these minimize/maximize/close buttons are very GNOMEy, which makes sense but isn’t really what I want.
+Maybe Adwaita will solve this?
+For some reason, `gvsbuild build libadwaita librsvg` (as [recommended](https://gtk-rs.org/gtk4-rs/stable/latest/book/libadwaita.html#if-using-gvsbuild) by the `gtk-rs` book) is building fucking libsass; not the actually maintained stop-trying-to-make-Dart-happen rewrite dart-sass, but the old and busted last-commit-two-years-ago libsass.
+rsvg also appears to depend on some yanked crate versions, which is concerning but not my problem today.
+Well, Adwaita certainly makes it look different:
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-gtk4-adwaita.png)
+
+I’ve got way more drop shadow or something that’s extending the ShareX window capture way beyond the actual boundary of the window, and I’ve got dark mode and a purple emphasis color (which I’m not sure if they’re reading from somewhere or if the default was just picked out by someone with good taste).
+It still doesn’t look like a Windows window, though, and Adwaita has not magically fixed the accessibility.
+
+Using GTK4 from Rust on Windows works, I guess, but I would have to lower my standards a lot to call this good enough.
+
+## Iced
+
+[Iced](https://iced.rs/) says it’s inspired by Elm, and that’s cool.
+[Elm](https://elm-lang.org/) was your favorite programming language’s favorite programming language.
+I miss it sometimes.
+
+```rust
+struct State {
+ label: String,
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self { label: "Hello, world!".to_owned() }
+ }
+}
+
+impl State {
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::SetLabel(label) => self.label = label
+ }
+ }
+
+ fn view(&self) -> Column<Message> {
+ column![
+ text(&self.label),
+ text_input("", &self.label)
+ .on_input(Message::SetLabel),
+ ]
+ }
+}
+
+#[derive(Clone, Debug)]
+enum Message {
+ SetLabel(String),
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-iced.png)
+
+Windows Narrator can’t see into this window, and the IME won’t even switch into active mode when I try to switch it into active mode, which may actually be a better failure state than having it just not work.
+
+Apparently System76 is all in on Iced for Pop!_OS’s COSMIC shell, so for their users’ sake I hope accessibility and IME support are actually happening at some point.
+
+## imgui
+
+[Dear ImGui](https://github.com/ocornut/imgui) is a minimalist C++ GUI library with [Rust bindings](https://github.com/imgui-rs/imgui-rs).
+I will always think of it as being called `dear imgui,`, even though it hasn’t been canonically spelled that way since [2018](https://github.com/ocornut/imgui/commit/84d1ce39584161dfd027613b0defe305637f3867); the trailing comma is delightful in an [e e cummings](https://www.poetryfoundation.org/poetrymagazine/poems/49493/i-carry-your-heart-with-mei-carry-it-in) sort of way, and they should bring it back.
+
+Unfortunately, since Dear ImGui is designed to be plugged into an existing game engine, starting with it from scratch is a little bit annoying.
+There’s a downright Linux-desktop-environment number of different ways to use imgui-rs, but apparently “The most tested platform/renderer combination is `imgui-glow-renderer` + `imgui-winit-support` + `winit`”, so that’s what I’ll use.
+Or at least it would be what I’d use if the [examples](https://github.com/imgui-rs/imgui-examples) didn’t all use `imgui-glium-renderer` instead; that one’s [deprecated](https://github.com/imgui-rs/imgui-glium-renderer/issues/1) and it doesn’t appear to work with the latest version of `glium` but I can’t figure out how to get `glow` working instead.
+Open source!
+
+```rust
+// eliding the 160 lines of glue i copied and pasted without understanding
+let mut label = "Hello, world!".to_string();
+support::simple_init(file!(), move |_, ui| {
+ ui.window("Hello world")
+ .size([300.0, 110.0], Condition::FirstUseEver)
+ .build(|| {
+ ui.text_wrapped(&label);
+ ui.input_text("Text", &mut label)
+ .build();
+ });
+});
+```
+
+![a screenshot of a text label and a text field both saying Hello, world! but the text field is labeled “Text”](/assets/2025-04-13-imgui.png)
+
+Windows Narrator can’t see this text, and the IME refuses to activate.
+
+The tiny window within the huge window is hilarious, and it makes sense if you’re actually doing game dev and there’s a game in the rest of the window, but I am not, and the massive white void is not something I would tolerate even in an application I was building solely for myself.
+If I had a graphics stack picked out already because I was doing game dev, I might not mind the flexibility of supporting what feels like hundreds of different renderers and backends, but since that is not my current situation I very much do mind the flexibility.
+This is what Sartre meant by being “condemned to freedom”: you have innumerable options available to you, but nobody can rescue you from the responsibility of deciding between them.
+
+<details>
+<summary>digression: the irony you may have noticed</summary>
+<aside>
+
+This blog post offers you, in some sense, an escape from the responsibility of deciding between the innumerable (43) options available to people trying to develop desktop GUIs in Rust.
+However, deciding whether or not to listen to me is as much an exercise of that responsibility as it is a surrender of it, and part of the reason I frontloaded so much context is that I want you to take that responsibility seriously.
+If you decide for yourself that you value Windows support, screen reader accessibility, and IME handling like I do, then you have done the important part already.
+
+</aside>
+</details>
+
+## KAS
+
+[KAS](https://github.com/kas-gui/kas), the toolKit Abstraction System, is written from scratch in Rust.
+The tutorials are a bit out of date — some things appear to have been moved around between when the tutorials were written and the most recent stable release — but the examples in the actual repo appear to work.
+I’m not sure I quite understand how the state management is designed, but after a bit of fumbling I can at least complete the task:
+
+```rust
+let tree = column![
+ format_value!("{}"),
+ EditBox::instant_parser(|x: &String| x.clone(), SetLabel),
+];
+
+Adapt::new(tree, "Hello, world!".to_string())
+ .on_message(|_, label, SetLabel(text)| *label = text)
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-kas.png)
+
+Narrator can’t see this text, and the IME won’t activate.
+
+The part that confuses me the most is why the `EditBox` needs to be explicitly told that it’s a `String` that’s being edited; that seems like it shouldn’t require an explicit declaration.
+Maybe if the tutorial were up to date it’d be easier to understand.
+Regardless, it seems like this isn’t really ready for prime time yet.
+
+## kittest
+
+[kittest](https://github.com/rerun-io/kittest) is an AccessKit-driven testing library that only supports egui.
+This is cool, but it’s not in the same category as the sort of thing I’m actually looking for.
+If I maintained Are We GUI Yet? I’d probably split the list up into separate categories like [Are We Web Yet?](https://www.arewewebyet.org/) has.
+
+## Leptos
+
+[Leptos](https://github.com/leptos-rs/leptos) is a Web frontend framework that is for some reason on the Are We GUI Yet? list.
+The README [has an FAQ about native GUIs](https://github.com/leptos-rs/leptos/tree/v0.7.8?tab=readme-ov-file#can-i-use-this-for-native-gui) that says it’d be possible to build native GUIs with Leptos but it’s not actually supported because it sent the whole codebase into generics hell when they tried it.
+
+## lvgl
+
+[LVGL](https://lvgl.io/), the Lightweight and Versatile Graphics Library, is a C GUI library designed for embedded use; it has [Rust bindings](https://github.com/lvgl/lv_binding_rust) that are `#![no_std]` compatible by default, which is neat if you need that.
+Unfortunately, after copying around the C header files that define the configuration, I’m getting C compiler errors:
+
+```text
+ C:\Users\Melody\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\lvgl-sys-0.6.2\vendor\lv_drivers\display\fbdev.c(13): fatal error C1083: Cannot open include file: 'unistd.h': No such file or directory
+```
+
+It seems like the LVGL configuration in the Rust binding samples is not designed to work on Windows, and if I figure out the flag to disable the framebuffer driver I get similar errors about not finding SDL, and if I disable SDL I get bindgen not being able to find libclang, and if I come back after fixing bindgen for a later library I get errors about the linker not finding SDL even though I already turned off SDL.
+I wouldn’t be surprised if doing desktop development with LVGL is missing the point, though, and I’m holding it wrong by not cross compiling to some slightly cursed embedded Linux target.
+
+## Makepad
+
+[Makepad](https://github.com/makepad/makepad) is another novel Rust GUI framework.
+They’re publishing versions to crates.io but not creating Git tags to match those versions, so it’s hard to find the examples that are supposed to work with the published crates, and it’s easier to just point the dependency right at the Git repo so the examples from their main branch will work.
+They’ve got a macro DSL with no documentation I can find, but a bit of persistence is all it takes to turn the simplest example into something that works:
+
+```rust
+live_design! {
+ import makepad_widgets::base::*;
+ import makepad_widgets::theme_desktop_dark::*;
+ App = {{App}} {
+ ui: <Root> {
+ main_window = <Window> {
+ body = <ScrollXYView> {
+ flow: Down,
+ spacing: 10,
+ align: { x: 0.5, y: 0.5 },
+ label1 = <Label> {
+ text: "Hello, world!",
+ }
+ input1 = <TextInput> {
+ text: "Hello, world!",
+ }
+ }
+ }
+ }
+ }
+}
+
+impl MatchEvent for App {
+ fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
+ let input1 = self.ui.text_input(id!(input1));
+ if let Some(new_text) = input1.changed(&actions) {
+ let label1 = self.ui.label(id!(label1));
+ label1.set_text(&new_text);
+ label1.redraw(cx);
+ }
+ }
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-makepad.png)
+
+Narrator can’t even see the minimize/maximize/close buttons, much less the content of the window.
+The IME opens in the corner of the display and has its own wrapper to show the kana while I’m typing, but when I press Enter the kanji get correctly entered into the text field.
+
+I kinda like the Blender aesthetic they’ve got for their stock widgets, but the lack of accessibility support is a downer, and the lack of documentation around the load-bearing DSL may be worse.
+Maybe the documentation exists and I just can’t find it (they’ve got a Discord server that I’m not joining), but I suspect that Makepad is built for the Makepad team right now, and any utility anyone else can get out of it is coincidental.
+A lot of projects start in that stage, and most of mine never leave, so that’s not a bad thing, but I do kinda wish Are We GUI Yet? was doing a little more pruning.
+I’ve been working on this post for almost a week already, and I’m only 58% of the way through the list.
+
+## masonry
+
+[Masonry](https://github.com/linebender/xilem/tree/main/masonry) is a pure-Rust GUI library that’s the successor to the discontinued Druid framework that I found really impressive early on.
+It primarily serves as the foundation for Xilem, a kinda Elm-ish kinda SwiftUI-ish framework I’m looking forward to trying once I get all the way down the list.
+
+The last release of Masonry was 11 months ago, and it looks like a lot has changed since then, so I’m going to just point to the `main` branch directly.
+
+```rust
+fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) {
+ match action {
+ Action::TextChanged(new_text) => {
+ ctx.render_root().edit_widget(self.label_id, |mut label| {
+ let mut label = label.downcast::<Label>();
+ Label::set_text(&mut label, new_text);
+ });
+ }
+ _ => {}
+ }
+}
+
+let main_widget = Portal::new(
+ Flex::column()
+ .with_child(Label::new("Hello, world!").with_id(label_id))
+ .with_child(Textbox::new("Hello, world!"))
+ .with_spacer(VERTICAL_WIDGET_SPACING),
+);
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-masonry.png)
+
+Confusingly, Narrator can tell that there’s a text field in here, but it’s wrong about where in the window it is.
+Kanji input via the IME works, but the default font appears to not support the fullwidth Latin characters that show up in provisional states before the hiragana replace them, so there’s a bit of [tofu](https://en.wikipedia.org/wiki/Unicode_input#Availability) in the intermediate states `☐`, `とう☐`, and `とう☐☐` (after the `t`, `k`, and `y` in `toukyou`).
+
+I think it’s fair to say that you aren’t really supposed to use Masonry directly, the intent is to build an architecture on top of Masonry.
+The API ergonomics feel like they match that: you probably could build a nontrivial application directly in Masonry, but I wouldn’t recommend it.
+
+## Maycoon
+
+[Maycoon](https://maycoon-ui.github.io/) is another new from scratch pure Rust framework.
+It looks like it’s still pretty new.
+The quick start guide is out of date, but the in-repo examples are still good.
+(It’s very nice that Cargo defaults to compiling examples as part of running tests.)
+
+Maycoon doesn’t appear to have a text input widget.
+GPUI didn’t, either, but at least they had an example that involved building one from scratch that I could just copy.
+Maycoon is too new to be usable for this task.
+
+## Pax
+
+[Pax](https://www.pax.dev/) is “a revolutionary new canvas for building apps & websites with AI.”
+🤮🤮🤮
+Shipping a real visual editor for your GUI DSL is a good idea, though, although I think I broke the fancy editor somehow because it stopped responding to anything and started logging a bunch of index out of bounds errors.
+
+Alas, the only desktop target Pax supports [is macOS](https://docs.pax.dev/installation/app-targets/desktop/), which is useless to me today.
+Maybe one day an AI bro who pays for Twitter will contribute something positive to society, but this is not that day.
+
+## qmetaobject
+
+[QMetaObject](https://github.com/woboq/qmetaobject-rs󱦘) is a different approach to Qt bindings than [CXX-Qt](#cxx-qt) that appears to try to put even more code into Rust.
+Unfortunately, it appears to not work nicely with the `windows-msvc` target, and I don’t appear to have a gcc toolchain installed in a place that it likes.
+
+## relm
+
+[relm](https://github.com/antoyo/relm) is named after Elm, which is neat.
+Unfortunately, relm is built on the unmaintained GTK 3 bindings, and even if I did install GTK 3 I’m not certain it would’ve worked, because some load-bearing components are making incorrect UNIX-centric assumptions about how lists of paths work:
+
+```text
+ pkg-config exited with status code 1
+ > PKG_CONFIG_PATH=C:\Users\Melody\Projects\_resources\gtk-build\gtk\x64\release\lib\pkgconfig PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags gdk-3.0 'gdk-3.0 >= 3.22'
+ The system library `gdk-3.0` required by crate `gdk-sys` was not found.
+ The file `gdk-3.0.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.
+ PKG_CONFIG_PATH contains the following:
+ - C
+ - \Users\Melody\Projects\_resources\gtk-build\gtk\x64\release\lib\pkgconfig
+```
+
+I wonder if there’s a library that’s like relm but built on GTK 4?
+
+## Relm4
+
+[Relm4](https://relm4.org/) is like relm but built on GTK 4.
+
+```rust
+view! {
+ gtk::Window {
+ set_title: Some("Simple app"),
+ set_default_size: (300, 100),
+
+ gtk::Box {
+ set_orientation: gtk::Orientation::Vertical,
+ set_spacing: 5,
+ set_margin_all: 5,
+
+ gtk::Label {
+ #[watch]
+ set_label: &model.label,
+ set_margin_all: 5,
+ },
+
+ gtk::Entry {
+ set_text: &model.label,
+ connect_changed[sender] => move |x| sender.input(Msg::SetLabel(x.text().into())),
+ },
+ }
+ }
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-relm4.png)
+
+As was the case with the GTK 4 test, Narrator can’t even see the window chrome, and the IME works fine.
+
+This architecture is probably easier to work with at scale than throwing GTK 4 widgets around directly would be, but it also inherits all of the problems of GTK 4 on Windows, of which there are many.
+Also, that `view!` macro is (like all Rust macros) really annoying to try to debug if your input is slightly incorrect, which mine was while I tried to figure out how to get `connect_changed` working.
+If you love the widgets of GTK 4 but want a nicer architecture, you might like Relm4, but if you are not in that situation, this library has nothing to offer you.
+
+## Ribir
+
+[Ribir](https://ribir.org/) is a bespoke Rust GUI framework that markets itself as being “non-intrusive”, [meaning](https://ribir.org/docs/introduction#non-intrusive-programming-model) that your GUI components read and write your model objects directly, with no intermediate state layers or constraints on model objects.
+I’m not sure that this is actually a problem with other frameworks, and I feel like there might be a clearer way to pitch that, but it’s in principle good to be trying to stand out in what I had not quite realized was this crowded of a landscape.
+
+The [website docs](https://ribir.org/docs/introduction) only have options for 0.2.x and main despite 0.3.0 coming out last August.
+
+Well, I can’t get the pile of macro magic to work correctly, and I’m not sure why. This code looks like it should work, but the text I type into the input field doesn’t actually appear in the label:
+
+```rust
+App::run(fn_widget! {
+ let label = State::value("Hello, world!".to_string());
+ let input = @Input { auto_focus: true };
+ $input.write().set_text("Hello, world!");
+ @Column {
+ @Text { text: pipe!($label.to_string()) }
+ @ $input {
+ on_key_up: move |_| {
+ label.set($input.text().to_string());
+ },
+ }
+ }
+});
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-ribir.png)
+
+Windows Narrator can’t see any of the text inside this window.
+The IME opens in a random corner of my monitor, the composer state is hidden, and the default font doesn’t have kanji, but there are two missing-glyph symbols after I confirm from the IME converter, so it seems like the IME technically a little bit works.
+
+If the pitch was that you don’t need to think about the mechanisms of state going into and out of the UI, then the pitch was overstated, because I have an app that looks like it should work but doesn’t because that glue has gone wrong.
+
+## Rinf
+
+[Rinf](https://github.com/cunarist/rinf) is another framework for using Rust for business logic and Flutter for UIs.
+I want that even less now than I did a few days ago when I wrote the [flutter_rust_bridge](#flutter_rust_bridge) section.
+
+## rui
+
+[rui](https://github.com/audulus/rui) is a Rust GUI library “inspired by SwiftUI”; as someone who’s worked with SwiftUI a lot at my day job, I’m reading that as meaning that this will be almost good but its state management will be clumsy and annoying to work with.
+
+It’s pretty alright at scales this small, though, so that may not be fair:
+
+```rust
+rui(state(
+ || "Hello, world!".to_string(),
+ |label, cx| {
+ vstack((
+ text(&cx[label]).padding(Auto),
+ text_editor(label).padding(Auto),
+ ))
+ },
+));
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-rui.png)
+
+Narrator can’t see this text, and the IME won’t even activate.
+
+I think I kinda like the aesthetic — a strong [default font](https://www.brumale.xyz/anodina/) goes a long way towards giving a GUI toolkit a cohesive look — and I also like the lack of load-bearing macros, but I’m of course less wild about the lack of accessibility or IME support.
+
+## Slint
+
+[Slint](https://slint.dev/) is like if Qt had been invented 30 years later.
+Like Qt, it’s got its own bespoke DSL, and like Qt, the business model is to be GPL and sell exceptions, but unlike Qt, desktop and web exceptions are free and it’s only embedded development that they want you to pay for.
+There’s no inherent guarantee that that’ll always be the case, but also, doing this stuff right is hard and I am in favor of people getting paid for the work that they do.
+
+This DSL kinda rules, I can’t lie:
+
+```slint
+export component AppWindow inherits Window {
+ property <string> label: "Hello, world!";
+ VerticalBox {
+ Text {
+ text: root.label;
+ }
+
+ LineEdit {
+ text <=> root.label;
+ }
+ }
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-slint.png)
+
+Narrator works perfectly.
+The IME renders all its provisional states as just missing-character glyphs `☐`, which makes me think the default font just doesn’t have fullwidth Latin or hiragana or kanji, but once I accept the kanji from the converter I get the actual kanji displaying correctly, so that may actually be a bug and not just a font selection issue.
+
+Using `<=>` as the operator to create a [two-way data binding](https://docs.slint.dev/latest/docs/slint/guide/language/concepts/reactivity/#two-way-bindings) is really clever, I hope they’re proud of that.
+Also, since this DSL is a standalone language and not just a pile of Rust macros, the ceiling on error message quality is way higher.
+I don’t think they’re actually hitting that ceiling yet, though: before I stumbled into the two-way data binding operator I had a callback with a syntax error and the Slint compiler wasn’t a ton of help.
+They’ve even got C++, JS, and Python bindings, and it’s cool that you can write a library once in Rust and then use it from that many different languages.
+
+I [suggested](/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md#summary) towards the end of my previous excursion here that Slint (then still called SixtyFPS) would merit a more thorough look once it had better accessibility support, and it certainly does.
+It’s not perfect, but it’s come a long way in the last four years, and I’m curious what the next couple years will look like for Slint.
+
+## Tauri
+
+Do you like Electron but wish it was Rust?
+[Tauri](https://tauri.app/) is that.
+To their credit, they’ve also swapped out the bundled Chromium for just binding to the system’s inherent web browser, whether that’s [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) on Windows, WebKit on macOS, or WebKitGTK on Linux, so Tauri applications don’t fill your hard drive with a dozen copies of the same web browser.
+Unfortunately, they have not touched the architecture; you still have a host process running outside of the browser and an independent frontend running inside the browser.
+
+Building that frontend is not something Tauri is concerned with; you have to decide for yourself what you want your stack to look like.
+I want to write Rust, so in the new project wizard I pick Rust as my frontend language (rather than JavaScript/TypeScript or, for all three diehard Blazor fans, C#).
+I’m then asked which Rust frontend framework I want, and I don’t really want to have to pick between Dioxus and Leptos and Sycamore and Yew right now, so I pick “Vanilla” assuming that it’ll give me bare [web-sys](https://rustwasm.github.io/wasm-bindgen/web-sys/index.html) to make the same DOM API calls as vanilla JS but in Rust; I’ve done this before, and it’s not very good, but at this scale it’d be completely tolerable.
+Instead, though, “Vanilla” means vanilla JS even if I selected Rust, and since I selected Rust I don’t even get an option of vanilla TS instead.
+This was [reported a year and a half ago](https://github.com/tauri-apps/create-tauri-app/issues/550) and ignored.
+
+If I’m stuck making a choice, I need an excuse to ignore three of the four provided options.
+I already looked at Dioxus, so that’d be boring to use again.
+Yew’s 0.22 release was [announced in October 2024](https://yew.rs/blog/2024/10/14/release-0-22), [added to the changelog in December 2024](https://github.com/yewstack/yew/commit/d77cf0196b65486a35b9d1a02aaaed6faafbf8cc), and released on crates.io [literally never (as of April 2025)](https://crates.io/crates/yew/versions); that’s not great.
+The [Leptos book](https://book.leptos.dev/) says that it’s “most similar to frameworks like Solid and Sycamore”; maybe that’s a sign that it doesn’t matter, or maybe it’s a sign that I should try both.
+
+```rust
+// leptos
+#[component]
+pub fn App() -> impl IntoView {
+ let (label, set_label) = signal("Hello, world!".to_string());
+
+ let update_label = move |ev| {
+ let v = event_target_value(&ev);
+ set_label.set(v);
+ };
+
+ view! {
+ <p>{ move || label.get() }</p>
+ <input type="text" value={ move || label.get() } on:input=update_label />
+ }
+}
+
+// sycamore
+#[component]
+pub fn App() -> View {
+ let label = create_signal("Hello, world!".to_string());
+
+ view! {
+ p { (label) }
+ input(r#type="text", bind:value=label)
+ }
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-tauri.png)
+
+Narrator can see all this text, and the IME provisional states are drawn in the corner of the screen rather than inline but they work correctly and the final kanji look fine.
+
+I guess the main difference between these two Web frameworks that we can see from here is that Sycamore has a `bind:` modifier for attributes (very good!) but can’t quite handle `input type=` because `type` is a Rust keyword and requires `r#type` instead (very bad!).
+The largest issue, though, is something I haven’t captured here, because today’s task is so trivial it can be done entirely within the frontend, and I let that count for GTK so I have to let it count here too.
+
+If I add the requirement that the new value of the label be printed to standard output when the label changes (as a stand-in for, say, performing some file I/O), in most frameworks it suffices to either add a `println!` to the existing event handler or subscribe to a two-way-bound state with a `println!`.
+Even Dioxus, built on the same WebView2/WebKitGTK library as Tauri, will do the right thing if I `println!` from an event handler.
+In Tauri, though, a `println!` from the frontend will be completely ignored, and if we want to be able to `println!` we need inter-process communication between the frontend and the host process.
+
+In the host, this is nice and easy due to the magic of proc macros:
+
+```rust
+#[tauri::command]
+fn print(text: &str) {
+ println!("{}", text);
+}
+```
+
+In the frontend, however, there is a lot more boilerplate:
+
+```rust
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
+ async fn invoke(cmd: &str, args: JsValue) -> JsValue;
+}
+
+#[derive(Serialize, Deserialize)]
+struct PrintArgs<'a> {
+ text: &'a str,
+}
+
+#[component]
+pub fn App() -> View {
+ // ...
+
+ create_effect(move || {
+ let label = label.get_clone();
+ spawn_local_scoped(async move {
+ let args = serde_wasm_bindgen::to_value(&PrintArgs { text: &label }).unwrap();
+ invoke("print", args).await;
+ })
+ });
+
+ // ...
+}
+```
+
+This makes me sad for two reasons.
+The first is a question of principle: this arbitrary boundary drawn through the middle of my application means that if I discover I need a new piece of functionality I may need to move a substantial chunk of code from the frontend to the backend, and if it’s something load-bearing within the frontend I’m going to have a real motherfucker of a time pivoting my architecture on short notice for no reason.
+The second is a question of type safety: as you may have noticed, the IPC interface in the frontend takes an `&str` for the command name and a `JsValue` for the command arguments, meaning frontend IPC calls have *no type checking*.
+Indeed, if I rename the argument in the host from `text` to `text_to_print` and don’t update the `PrintArgs` in the frontend, I get not even a warning at compile time, and at runtime I get
+
+```text
+panicked at src\app.rs:6:1:
+unexpected exception: JsValue("invalid args `textToPrint` for command `print`: command print missing required key textToPrint")
+
+Uncaught RuntimeError: unreachable
+```
+
+If I then change the label, I get
+
+```text
+panicked at C:\Users\Melody\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\wasm-bindgen-futures-0.4.50\src\task\singlethread.rs:103:37:
+already borrowed: BorrowMutError
+```
+
+and this is all just misery upon misery.
+Half the point of Rust is the sheer quantity of bugs that it can catch at compile time, and if your IPC is just tossing strings around and praying at runtime, you may as well be just writing vanilla JavaScript.
+(I checked, and even if your frontend is TypeScript, the `invoke` IPC boundary just takes a `string` rather than a union of the actual legal values.)
+
+Hilariously, the Tauri docs [claim](https://tauri.app/develop/calling-rust/) that the command mechanism is “for reaching Rust functions with type safety”, as distinct from their event system, which is even less type safe.
+With events, you’re tossing strings and arbitrary JavaScript payloads around in both the frontend and the host, so technically it’s not false to claim that only doing that on one end is more type safe, but type checking only at one end is like putting a lock on your bike but not running it through the bike rack: you aren’t tying two things together, you’re just tying one thing to itself and praying.
+Even to the limited extent that half of type safety could be useful, though, they’ve picked the wrong half: there’s inherently only one implementation of the command, but there can be many calls to it, so it’d be far more valuable to have type safety at the call sites than at the implementation site.
+Commands only go from the frontend to the host, so type checking in the host and YOLOing in the frontend is being picky at the receive end and sloppy at the send end, which is the exact opposite of [Postel’s law](https://en.wikipedia.org/wiki/Robustness_principle).
+
+<details>
+<summary>digression: Postel’s law</summary>
+<aside>
+
+The common formulation of Postel’s law is “be conservative in what you emit and liberal in what you accept”, and it’d be more recognizable if I used those terms instead of “picky” and “sloppy”.
+I haven’t done that, though, because you should be conservative never and liberal very rarely.
+Be radically leftist in everything, even technical blog posts that aren’t intrinsically political.
+
+</aside>
+</details>
+
+If there was an unnecessary IPC boundary but it was type safe, or if there was bad stringly typed nonsense somewhere but no unnecessary IPC boundary, I might find it within myself to forgive that, but the combination of the entirely unnecessary split-brain architecture with the absolute lack of type safety at the boundary means that I think I genuinely hate Tauri.
+
+## tinyfiledialogs
+
+[tinyfiledialogs](https://sourceforge.net/projects/tinyfiledialogs/󱦘) is a C library with [Rust bindings](https://github.com/jdm/tinyfiledialogs-rs) providing a handful of basic prompts, including message boxes, text input a la JS’s `window.prompt`, and, as the name implies, file dialogs.
+I can’t actually complete today’s task with those.
+
+## Tk
+
+[Tk](https://www.tcl-lang.org/) is the GUI framework for [Tcl](https://www.tcl-lang.org/), a fascinating late-80s programming language that I would describe as weird in a Lua way, weird in a LISP way, and weird in its own way all at once.
+[Evidently](https://github.com/oooutlk/tcltk) it has Rust bindings.
+
+The docs for the Rust bindings [recommend](https://oooutlk.github.io/tk/installing_tk_on_windows.html) ActiveState’s ActiveTcl, but I can’t find an actual way to download ActiveTcl, and ActiveState’s [pricing page](https://www.activestate.com/pricing/) has an FAQ entry that doesn’t inspire confidence:
+
+> **Can I still get ActivePerl, ActivePython, or ActiveTcl?**
+>
+> If you still need access to our legacy releases, please get in touch with us via our Contact us page.
+
+I’m installing [Magicsplat Tcl](https://www.magicsplat.com/tcl-installer/index.html) instead.
+It doesn’t appear to have been enough, though:
+
+```text
+ thread 'main' panicked at C:\Users\Melody\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\bindgen-0.64.0\lib.rs:2393:31:
+ Unable to find libclang: "couldn't find any valid shared libraries matching: ['clang.dll', 'libclang.dll'], set the `LIBCLANG_PATH` environment variable to a path where one of these files can be found (invalid: [])"
+```
+
+[Apparently](https://rust-lang.github.io/rust-bindgen/requirements.html#windows) this can be fixed with just `winget install LLVM.LLVM`.
+
+```rust
+let tk = make_tk!()?;
+let root = tk.root();
+
+let c = root
+ .add_ttk_frame("c" -padding((3, 3, 12, 12)))?
+ .grid(-column(0) -row(0) -sticky("nwes"))?;
+
+root.grid_columnconfigure(0, -weight(1))?;
+root.grid_rowconfigure(0, -weight(1))?;
+
+let label = c
+ .add_ttk_label("label" -text("Hello, world!"))?
+ .grid(-column(1) -row(1))?;
+
+let entry = c
+ .add_ttk_entry("entry" -textvariable("label"))?
+ .grid(-column(1) -row(2))?;
+
+entry.insert(0, "Hello, world!")?;
+
+entry.bind(
+ event::any_key_release(),
+ tclosure!(tk, || label.configure(-text(entry.get()?))),
+)?;
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-tk.png)
+
+Windows Narrator can’t see any of this text, and the IME works fine.
+
+I guess translating `ttk::label "label" -text "Hello, world!"` (which is how Tcl works, I’m fairly certain) to `add_ttk_label("label" -text("Hello, world!"))` makes sense for existing Tcl users, but it feels really weird, and `cargo fmt` is understandably confused by it.
+It’s also hard to tell which options are actually supported on which widgets, because of all the trait magic that’s going on; it took me a while to discover that `.insert()` is the way you set the initial text of the text field.
+There’s also a bit of jank in the example — [apparently](https://oooutlk.github.io/tk/a_first_real_example.html) attaching a callback to a button requires `unsafe`.
+Plus, after I uninstalled Tcl/Tk, the binary stopped working, so you can only ship a binary to people who also manually install Tcl/Tk.
+
+If you already know and love Tcl/Tk and just wish (heh) you could use it from Rust, maybe you’d find this useful, but I am not in that situation and I do not.
+
+## Vizia
+
+[Vizia](https://github.com/vizia/vizia) is another novel Rust GUI library.
+The [book](https://book.vizia.dev/quickstart/setup.html) says to depend on the Git repo rather than the latest release, which I usually try to avoid, but sure.
+Conveniently, the counter example is pretty easy to adapt for what I need here, and there’s no list of widgets in the book (at least that I could find) but there is a list of widgets [in the docs](https://docs.vizia.dev/vizia/views/index.html#structs) so I don’t have to spend fifteen minutes guessing what they call their text field.
+
+```rust
+#[derive(Lens)]
+pub struct AppData {
+ label: String,
+}
+
+pub enum AppEvent {
+ SetText(String),
+}
+
+impl Model for AppData {
+ fn event(&mut self, _: &mut EventContext, event: &mut Event) {
+ event.map(|app_event, _| match app_event {
+ AppEvent::SetText(text) => {
+ self.label = text.clone();
+ }
+ });
+ }
+}
+
+fn main() -> Result<(), ApplicationError> {
+ Application::new(|cx| {
+ AppData {
+ label: "Hello, world!".to_string(),
+ }
+ .build(cx);
+
+ VStack::new(cx, |cx| {
+ Label::new(cx, AppData::label);
+
+ Textbox::new(cx, AppData::label).on_edit(|cx, text| cx.emit(AppEvent::SetText(text)));
+ })
+ .alignment(Alignment::Center);
+ })
+ .title("Counter") // Configure window properties
+ .inner_size((400, 100))
+ .run()
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-vizia.png)
+
+Something weird happens to the internal padding in the text field when I focus it, though:
+
+![the same window but with the text within the field further down](/assets/2025-04-13-vizia-focused.png)
+
+Narrator can see that there’s a text label and a text input within this window, but it doesn’t appear to be able to see what the text actually is in either widget, which is a new one.
+The IME converter dropdown appears in the corner of the screen, but the provisional states draw correctly in the text field, and the final kanji go in correctly.
+
+The structure that’s present here seems promising, but Vizia doesn’t seem to be quite ready for serious use yet.
+
+## WebRender
+
+[WebRender](https://github.com/servo/webrender) is part of the guts of Servo, the Rust-based web browser engine that Mozilla founded and then [abandoned](https://www.zdnet.com/article/mozilla-lays-off-250-employees-while-it-refocuses-on-commercial-products/) (an event which prompted [my biggest hit on this blog](/_posts/2020-08-13-post-open-source.md)).
+It’s been picked back up and is [some amount of back](https://servo.org/), although quite how back its new team wants it to be [is still up in the air](https://github.com/servo/servo/discussions/36379).
+
+The in-repo [examples](https://github.com/servo/webrender/tree/beccb13247f1c3799957e4c85468d79ba47033c2/examples) are so old they’re [still using the 2018 language edition](https://github.com/servo/webrender/blob/beccb13247f1c3799957e4c85468d79ba47033c2/examples/Cargo.toml#L7), and the [latest version published on crates.io](https://crates.io/crates/webrender/versions) is from 2020, so I guess I’m depending on the Git repo again.
+There’s a `main` branch last updated four months ago and an `upstream` branch updated last week; presumably the `upstream` branch is the right one to use, but it’d be nice if that was actually explained somewhere.
+
+Looking a bit at the examples, WebRender does not have widgets, it just has shapes; this is a low-level graphics crate and not a high-level GUI crate, and it’s not clear why it’s on the Are We GUI Yet? list.
+
+## windows
+
+[Windows](https://www.microsoft.com/en-us/windows/) is an operating system created by Microsoft in the mid-1980s; its APIs have [Rust bindings](https://github.com/microsoft/windows-rs).
+I do not know how to do GUI development with bare Win32 API calls, though, so doing them from Rust is not exciting.
+XAML, which is to my understanding the shiny new way to do desktop GUI development in specifically Windows, is [explicitly not included in the `windows` crate](https://kennykerr.ca/rust-getting-started/what-apis-are-included.html) because “Xaml is also focused and tailored for C# app development so this API isn't applicable to Rust developers.”
+This is confusing, because the XAML-based [WinUI 3](https://github.com/microsoft/microsoft-ui-xaml) is also supported on C++, not just C#.
+Ah, Microsoft.
+
+## WinSafe
+
+[WinSafe](https://github.com/rodrigocfd/winsafe) is apparently a set of Rust wrappers around the Win32 GUI API.
+
+```rust
+#[derive(Clone)]
+pub struct MyWindow {
+ wnd: gui::WindowMain,
+ label: gui::Label,
+ field: gui::Edit,
+}
+
+impl MyWindow {
+ pub fn new() -> Self {
+ let wnd = gui::WindowMain::new(
+ gui::WindowMainOpts {
+ title: "My window title".to_owned(),
+ size: (300, 150),
+ ..Default::default()
+ },
+ );
+
+ let label = gui::Label::new(
+ &wnd,
+ gui::LabelOpts {
+ text: "Hello, world!".to_string(),
+ position: (20, 20),
+ ..Default::default()
+ },
+ );
+
+ let field = gui::Edit::new(
+ &wnd,
+ gui::EditOpts {
+ text: "Hello, world!".to_string(),
+ position: (20, 50),
+ ..Default::default()
+ },
+ );
+
+ let new_self = Self { wnd, label, field };
+ new_self.events(); // attach our events
+ new_self
+ }
+
+ pub fn run(&self) -> AnyResult<i32> {
+ self.wnd.run_main(None) // simply let the window manager do the hard work
+ }
+
+ fn events(&self) {
+ let ready = Arc::new(AtomicBool::new(false));
+
+ let ready2 = ready.clone();
+ self.wnd.on().wm_create(move |_| {
+ ready2.store(true, Ordering::SeqCst);
+ Ok(0)
+ });
+
+ let self2 = self.clone();
+ self.field.on().en_change(move || {
+ if ready.load(Ordering::SeqCst) {
+ self2.label.set_text(&self2.field.text());
+ }
+ Ok(())
+ });
+ }
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-winsafe.png)
+
+Unsurprisingly, Narrator and the IME both work.
+
+Manual positioning is no good, and the alternatives involve Win32 `.res` file editing and other Pandora’s boxen I don’t want to open.
+Also, I had to add that `ready` tracking myself, because the callback was firing for the first time before the window had been created, which was causing crashes.
+This isn’t great, and of course it’s only useful on Windows.
+
+## Xilem
+
+[Xilem](https://github.com/linebender/xilem/) is another novel pure-Rust framework, built on top of the previously discussed [masonry](#masonry), the successor to Druid, which I thought was really promising when I first got started with this series.
+It hasn’t had a numbered release in almost a year, so I’m pointing at the Git repo again.
+
+Conveniently, they’ve got a todo list example that I can sculpt into what I need today mostly by deleting.
+
+```rust
+struct State {
+ label: String,
+}
+
+fn app_logic(state: &mut State) -> impl WidgetView<State> + use<> {
+ flex((
+ label(state.label.clone()),
+ textbox(state.label.clone(), |state: &mut State, new_value| {
+ state.label = new_value;
+ }),
+ ))
+}
+
+fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> {
+ let data = State {
+ label: "Hello, world!".to_string(),
+ };
+
+ let app = Xilem::new(data, app_logic);
+ app.run_windowed(event_loop, "First Example".into())
+}
+
+fn main() -> Result<(), EventLoopError> {
+ run(EventLoop::with_user_event())
+}
+```
+
+![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-xilem.png)
+
+As was the case when using Masonry directly, Narrator sees the text but is wrong about its position, and some IME provisional states have missing glyphs but the IME behavior works fine.
+
+This architecture seems pretty neat, although of course this is not sufficient to know how well it works at any reasonable scale.
+Honestly, aside from the screen reader jank, my only gripe is the lack of versioning, and presumably that’ll be coming as Xilem matures.
+
+## Conclusion
+
+What a list, huh?
+If I had counted how many of these there were before I got started, I might’ve never started, because 43 is a lot.
+It’s good that there are this many different people working in this space, but the more options there are, the more important it is for people to be sifting through them to distinguish the ones that aren’t ready to use from the ones that are.
+
+Let’s pick some winners.
+If you’d rather take the quirks of CSS layout over the quirks of some other layout engine, [Dioxus](#dioxus) seems like a pretty reasonable choice; Diet Electron is definitely better than regular Electron, and that may be good enough for you, but it feels not-better-enough to me (although my sense of not-better-enough is [non-standard](https://en.wikipedia.org/wiki/Obsessive%E2%80%93compulsive_personality_disorder)).
+If you like DSL-driven UIs that are putting serious effort into developer tooling, [Slint](#slint) might be for you.
+If you want to avoid DSLs and macros and write only regular Rust, [egui](#egui) offers that.
+If you’re looking for something to invest in early, [Freya](#freya) and [Xilem](#xilem) are both basically usable now if you don’t mind living on the bleeding edge and putting up with some jank.
+There are open issues for improving accessibility in [Floem](https://github.com/lapce/floem/issues/8) and [iced](https://github.com/iced-rs/iced/issues/552), so it may be worth at least keeping an eye on those issues, although the iced issue has been open for 4½ years now.
+
+I would not describe any of these as a super easy slam dunk obviously correct choice, but there are a lot of reasonable options available, and that’s better than 2021, where I was [longing for wxPython](/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md#whining) by the end of the post.
+Maybe better things are possible after all.
+
+Now if you’ll excuse me, I’ve got some `cargo clean`s to run:
+
+![a folder taking up 67.8 GB of disk space](/assets/2025-04-13-properties.png)
+
+## The Table
+
+| library | works at all? | screen reader accessible? | IME works? |
+|---------------------------------------------|---------------------------|----------------------------------|------------------------------------------------------------------------------------------|
+| [Azul](#azul) | linker hell | | |
+| [cacao](#cacao) | macOS-specific | | |
+| [core-foundation](#core-foundation) | macOS-specific | | |
+| [Crux](#crux) | no desktop targets | | |
+| [Cushy](#cushy) | yes! | nope | composer hidden, converter works |
+| [CXX-Qt](#cxx-qt) | linker hell | | |
+| [Dioxus](#dioxus) | yes! | yes! | yes! |
+| [Dominator](#dominator) | web-specific | | |
+| [egui](#egui) | yes! | yes! | composer works, Tab press stolen from converter |
+| [Floem](#floem) | yes! | nope | nope |
+| [fltk](#fltk) | yes! | with extra crate | yes! |
+| [flutter_rust_bridge](#flutter_rust_bridge) | kinda, but state hell | yes! | kinda, but state hell |
+| [Freya](#freya) | yes! | mostly, but some content missing | composer hidden, converter works |
+| [fui](#fui) | qmake hell | | |
+| [GemGui](#gemgui) | technically | | |
+| [GPUI](#gpui) | yes! | nope | yes! |
+| [GTK 3](#gtk-3) | unmaintained | | |
+| [GTK 4](#gtk-4) | yes! | nope | yes! |
+| [Iced](#iced) | yes! | nope | nope |
+| [imgui](#imgui) | yes! | nope | nope |
+| [KAS](#kas) | yes! | nope | nope |
+| [kittest](#kittest) | only for testing | | |
+| [Leptos](#leptos) | web-specific | | |
+| [lvgl](#lvgl) | C dependency hell | | |
+| [Makepad](#makepad) | yes! | nope | composer outside window, converter works |
+| [masonry](#masonry) | yes! | content but not position | yes! but some temporary tofu |
+| [Maycoon](#maycoon) | no text input widget | | |
+| [Pax](#pax) | no Windows support | | |
+| [qmetaobject](#qmetaobject) | no windows-msvc | | |
+| [relm](#relm) | uses unmaintained GTK 3 | | |
+| [Relm4](#relm4) | yes! | nope | yes! |
+| [Ribir](#ribir) | kinda, but state hell | nope | composer hidden, converter works |
+| [Rinf](#rinf) | does not use Rust for GUI | | |
+| [rui](#rui) | yes! | nope | nope |
+| [Slint](#slint) | yes! | yes! | missing glyphs in provisional states but logic works and final kanji displayed correctly |
+| [Tauri](#tauri) | yes! | yes! | composer outside window, converter works |
+| [tinyfiledialogs](#tinyfiledialogs) | not general-purpose | | |
+| [Tk](#tk) | yes! | nope | yes! |
+| [Vizia](#vizia) | yes! | structure but not content | converter outside window, everything works |
+| [WebRender](#webrender) | too low-level | | |
+| [windows](#windows) | i don’t know Win32 | | |
+| [WinSafe](#winsafe) | yes! | yes! | yes! |
+| [Xilem](#xilem) | yes! | content but not position | yes! but some temporary tofu |
diff --git a/_posts/_posts.11tydata.js b/_posts/_posts.11tydata.js
index 2e07a0e..bc6ccb4 100644
--- a/_posts/_posts.11tydata.js
+++ b/_posts/_posts.11tydata.js
@@ -38,6 +38,7 @@ export default {
}
switch (child.type) {
case "text":
+ case "code_inline":
firstParagraph += child.content;
break;
case "softbreak":
diff --git a/assets/2023-05-23-two-heresies-about-link-rot-1.png b/assets/2023-05-23-two-heresies-about-link-rot-1.png
new file mode 100644
index 0000000..f212352
--- /dev/null
+++ b/assets/2023-05-23-two-heresies-about-link-rot-1.png
Binary files differ
diff --git a/assets/2023-11-26-tiny-cactus-cloudtest02-1.png b/assets/2023-11-26-tiny-cactus-cloudtest02-1.png
new file mode 100644
index 0000000..407a93c
--- /dev/null
+++ b/assets/2023-11-26-tiny-cactus-cloudtest02-1.png
Binary files differ
diff --git a/assets/2023-11-26-tiny-cactus-cloudtest02-2.png b/assets/2023-11-26-tiny-cactus-cloudtest02-2.png
new file mode 100644
index 0000000..840a30d
--- /dev/null
+++ b/assets/2023-11-26-tiny-cactus-cloudtest02-2.png
Binary files differ
diff --git a/assets/2023-11-26-tiny-cactus-cloudtest02-3.png b/assets/2023-11-26-tiny-cactus-cloudtest02-3.png
new file mode 100644
index 0000000..800fea7
--- /dev/null
+++ b/assets/2023-11-26-tiny-cactus-cloudtest02-3.png
Binary files differ
diff --git a/assets/2023-12-27-no-mans-sky-unless-1.png b/assets/2023-12-27-no-mans-sky-unless-1.png
new file mode 100644
index 0000000..7fa7fd9
--- /dev/null
+++ b/assets/2023-12-27-no-mans-sky-unless-1.png
Binary files differ
diff --git a/assets/2024-09-20-eggbug-forever-ffxiv-1.png b/assets/2024-09-20-eggbug-forever-ffxiv-1.png
new file mode 100644
index 0000000..78902af
--- /dev/null
+++ b/assets/2024-09-20-eggbug-forever-ffxiv-1.png
Binary files differ
diff --git a/assets/2024-09-20-eggbug-forever-ffxiv-2.gif b/assets/2024-09-20-eggbug-forever-ffxiv-2.gif
new file mode 100644
index 0000000..86dc4b7
--- /dev/null
+++ b/assets/2024-09-20-eggbug-forever-ffxiv-2.gif
Binary files differ
diff --git a/assets/2024-09-20-eggbug-forever-ffxiv-3.gif b/assets/2024-09-20-eggbug-forever-ffxiv-3.gif
new file mode 100644
index 0000000..b89d970
--- /dev/null
+++ b/assets/2024-09-20-eggbug-forever-ffxiv-3.gif
Binary files differ
diff --git a/assets/2025-04-13-cushy.png b/assets/2025-04-13-cushy.png
new file mode 100644
index 0000000..2bd7d8b
--- /dev/null
+++ b/assets/2025-04-13-cushy.png
Binary files differ
diff --git a/assets/2025-04-13-dioxus.png b/assets/2025-04-13-dioxus.png
new file mode 100644
index 0000000..2a735cc
--- /dev/null
+++ b/assets/2025-04-13-dioxus.png
Binary files differ
diff --git a/assets/2025-04-13-egui.png b/assets/2025-04-13-egui.png
new file mode 100644
index 0000000..cdfa243
--- /dev/null
+++ b/assets/2025-04-13-egui.png
Binary files differ
diff --git a/assets/2025-04-13-floem.png b/assets/2025-04-13-floem.png
new file mode 100644
index 0000000..b8335d4
--- /dev/null
+++ b/assets/2025-04-13-floem.png
Binary files differ
diff --git a/assets/2025-04-13-fltk.png b/assets/2025-04-13-fltk.png
new file mode 100644
index 0000000..d6e7953
--- /dev/null
+++ b/assets/2025-04-13-fltk.png
Binary files differ
diff --git a/assets/2025-04-13-flutter-rust-bridge.png b/assets/2025-04-13-flutter-rust-bridge.png
new file mode 100644
index 0000000..381fa5b
--- /dev/null
+++ b/assets/2025-04-13-flutter-rust-bridge.png
Binary files differ
diff --git a/assets/2025-04-13-freya.png b/assets/2025-04-13-freya.png
new file mode 100644
index 0000000..a96d1fa
--- /dev/null
+++ b/assets/2025-04-13-freya.png
Binary files differ
diff --git a/assets/2025-04-13-gpui.png b/assets/2025-04-13-gpui.png
new file mode 100644
index 0000000..8fe54f0
--- /dev/null
+++ b/assets/2025-04-13-gpui.png
Binary files differ
diff --git a/assets/2025-04-13-gtk4-adwaita.png b/assets/2025-04-13-gtk4-adwaita.png
new file mode 100644
index 0000000..a64b5f6
--- /dev/null
+++ b/assets/2025-04-13-gtk4-adwaita.png
Binary files differ
diff --git a/assets/2025-04-13-gtk4.png b/assets/2025-04-13-gtk4.png
new file mode 100644
index 0000000..3d566d2
--- /dev/null
+++ b/assets/2025-04-13-gtk4.png
Binary files differ
diff --git a/assets/2025-04-13-iced.png b/assets/2025-04-13-iced.png
new file mode 100644
index 0000000..4bf7f89
--- /dev/null
+++ b/assets/2025-04-13-iced.png
Binary files differ
diff --git a/assets/2025-04-13-imgui.png b/assets/2025-04-13-imgui.png
new file mode 100644
index 0000000..e7dd3ec
--- /dev/null
+++ b/assets/2025-04-13-imgui.png
Binary files differ
diff --git a/assets/2025-04-13-kas.png b/assets/2025-04-13-kas.png
new file mode 100644
index 0000000..834e1b8
--- /dev/null
+++ b/assets/2025-04-13-kas.png
Binary files differ
diff --git a/assets/2025-04-13-makepad.png b/assets/2025-04-13-makepad.png
new file mode 100644
index 0000000..20d9341
--- /dev/null
+++ b/assets/2025-04-13-makepad.png
Binary files differ
diff --git a/assets/2025-04-13-masonry.png b/assets/2025-04-13-masonry.png
new file mode 100644
index 0000000..d20e11c
--- /dev/null
+++ b/assets/2025-04-13-masonry.png
Binary files differ
diff --git a/assets/2025-04-13-properties.png b/assets/2025-04-13-properties.png
new file mode 100644
index 0000000..5ff3f23
--- /dev/null
+++ b/assets/2025-04-13-properties.png
Binary files differ
diff --git a/assets/2025-04-13-relm4.png b/assets/2025-04-13-relm4.png
new file mode 100644
index 0000000..e669cf1
--- /dev/null
+++ b/assets/2025-04-13-relm4.png
Binary files differ
diff --git a/assets/2025-04-13-ribir.png b/assets/2025-04-13-ribir.png
new file mode 100644
index 0000000..fc52434
--- /dev/null
+++ b/assets/2025-04-13-ribir.png
Binary files differ
diff --git a/assets/2025-04-13-rui.png b/assets/2025-04-13-rui.png
new file mode 100644
index 0000000..d29c14b
--- /dev/null
+++ b/assets/2025-04-13-rui.png
Binary files differ
diff --git a/assets/2025-04-13-slint.png b/assets/2025-04-13-slint.png
new file mode 100644
index 0000000..a300c88
--- /dev/null
+++ b/assets/2025-04-13-slint.png
Binary files differ
diff --git a/assets/2025-04-13-tauri.png b/assets/2025-04-13-tauri.png
new file mode 100644
index 0000000..b5a10e6
--- /dev/null
+++ b/assets/2025-04-13-tauri.png
Binary files differ
diff --git a/assets/2025-04-13-tk.png b/assets/2025-04-13-tk.png
new file mode 100644
index 0000000..4f98fa2
--- /dev/null
+++ b/assets/2025-04-13-tk.png
Binary files differ
diff --git a/assets/2025-04-13-vizia-focused.png b/assets/2025-04-13-vizia-focused.png
new file mode 100644
index 0000000..5a9d0a9
--- /dev/null
+++ b/assets/2025-04-13-vizia-focused.png
Binary files differ
diff --git a/assets/2025-04-13-vizia.png b/assets/2025-04-13-vizia.png
new file mode 100644
index 0000000..69b8e32
--- /dev/null
+++ b/assets/2025-04-13-vizia.png
Binary files differ
diff --git a/assets/2025-04-13-winsafe.png b/assets/2025-04-13-winsafe.png
new file mode 100644
index 0000000..48651e4
--- /dev/null
+++ b/assets/2025-04-13-winsafe.png
Binary files differ
diff --git a/assets/2025-04-13-xilem.png b/assets/2025-04-13-xilem.png
new file mode 100644
index 0000000..d787104
--- /dev/null
+++ b/assets/2025-04-13-xilem.png
Binary files differ
diff --git a/assets/site.css b/assets/site.css
index d57334c..36c408b 100644
--- a/assets/site.css
+++ b/assets/site.css
@@ -18,9 +18,12 @@ code {
pre {
line-height: 1.25;
}
+img {
+ max-width: 100%;
+}
a {
- border-bottom: 1px solid #444444;
- color: #444444;
+ border-bottom: 1px solid currentColor;
+ color: inherit;
text-decoration: none;
}
a:hover {
@@ -31,6 +34,9 @@ blockquote {
border-left: 2px solid #444444;
padding-left: 1em;
}
+main {
+ margin-bottom: 1em;
+}
table {
border: 1px solid currentColor;
@@ -53,6 +59,25 @@ table th, table td {
justify-content: center;
}
+details aside {
+ border-left: 1px solid currentColor;
+ padding-top: 1em;
+ padding-left: 1em;
+}
+
+details aside p:first-child {
+ margin-top: 0;
+}
+
+.cohost-style-embed {
+ background-color: #fff1df;
+}
+
+.cohost-style-embed-link {
+ padding: 0.75em;
+ text-align: right;
+}
+
/* https://github.com/PrismJS/prism/blob/master/themes/prism.css */
code[class*="language-"],
pre[class*="language-"] {
diff --git a/eleventy.config.js b/eleventy.config.js
index f187c91..fd9d653 100644
--- a/eleventy.config.js
+++ b/eleventy.config.js
@@ -1,10 +1,13 @@
-import { InputPathToUrlTransformPlugin } from "@11ty/eleventy";
+import { EleventyRenderPlugin, InputPathToUrlTransformPlugin } from "@11ty/eleventy";
import pluginRss from "@11ty/eleventy-plugin-rss";
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
+import pluginWebc from "@11ty/eleventy-plugin-webc";
import anchor from "markdown-it-anchor";
export default function (eleventyConfig) {
+ eleventyConfig.addPlugin(EleventyRenderPlugin);
eleventyConfig.addPlugin(InputPathToUrlTransformPlugin);
+ eleventyConfig.addPlugin(pluginWebc);
eleventyConfig.addPlugin(syntaxHighlight, {
errorOnInvalidLanguage: true,
@@ -59,6 +62,16 @@ export default function (eleventyConfig) {
operator: /=>/,
punctuation: /[(){},]/,
};
+ // This may take some actual work, though.
+ Prism.languages.slint = {
+ keyword: /component|export|inherits|property|root/,
+ "class-name": /AppWindow|VerticalBox|LineEdit/,
+ builtin: /Text|\bWindow|string/,
+ string: /"[^"]+"/,
+ operator: /:|<=>|\./,
+ punctuation: /[{}<>;]/,
+ property: /label|text/,
+ }
Prism.languages.text = {};
},
});
diff --git a/package-lock.json b/package-lock.json
index 59aab48..e9cf571 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,9 +9,10 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
- "@11ty/eleventy": "^3.0.0-beta.1",
+ "@11ty/eleventy": "^3.0.0",
"@11ty/eleventy-plugin-rss": "^2.0.2",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
+ "@11ty/eleventy-plugin-webc": "^0.11.2",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"prettier": "^3.3.3"
@@ -48,15 +49,15 @@
}
},
"node_modules/@11ty/eleventy": {
- "version": "3.0.0-beta.1",
- "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.0.0-beta.1.tgz",
- "integrity": "sha512-iJT7vekH11l8PAUPBfUAcb5oWbYK0w4ijgwDTutUsk6tX9rp4ZRL1jdhVWvZq04/rkc55mczNFPPhHB/XO1/qw==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.0.0.tgz",
+ "integrity": "sha512-0P0ZsJXVW2QiNdhd7z+GYy6n+ivh0enx1DRdua5ta6NlzY2AhbkeWBY6U+FKA8lPS3H4+XsTpfLLfIScpPZLaQ==",
"dev": true,
"dependencies": {
"@11ty/dependency-tree": "^3.0.1",
"@11ty/dependency-tree-esm": "^1.0.0",
- "@11ty/eleventy-dev-server": "^2.0.2",
- "@11ty/eleventy-plugin-bundle": "^2.0.2",
+ "@11ty/eleventy-dev-server": "^2.0.4",
+ "@11ty/eleventy-plugin-bundle": "^3.0.0",
"@11ty/eleventy-utils": "^1.0.3",
"@11ty/lodash-custom": "^4.17.21",
"@11ty/posthtml-urls": "^1.0.0",
@@ -66,20 +67,21 @@
"chardet": "^2.0.0",
"chokidar": "^3.6.0",
"cross-spawn": "^7.0.3",
- "debug": "^4.3.6",
+ "debug": "^4.3.7",
"dependency-graph": "^1.0.0",
+ "entities": "^5.0.0",
"fast-glob": "^3.3.2",
- "filesize": "^10.1.4",
+ "filesize": "^10.1.6",
"graceful-fs": "^4.2.11",
"gray-matter": "^4.0.3",
"is-glob": "^4.0.3",
- "iso-639-1": "^3.1.2",
+ "iso-639-1": "^3.1.3",
"js-yaml": "^4.1.0",
"kleur": "^4.1.5",
- "liquidjs": "^10.16.1",
- "luxon": "^3.4.4",
+ "liquidjs": "^10.17.0",
+ "luxon": "^3.5.0",
"markdown-it": "^14.1.0",
- "micromatch": "^4.0.7",
+ "micromatch": "^4.0.8",
"minimist": "^1.2.8",
"moo": "^0.5.2",
"node-retrieve-globals": "^6.0.0",
@@ -134,12 +136,13 @@
}
},
"node_modules/@11ty/eleventy-plugin-bundle": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-2.0.2.tgz",
- "integrity": "sha512-zGyPp1g6bi+VC2I5ylwj4w29nivDmx4Uki5gWY6v3MT/1muK0JTtnc1KOMC7yUurv6YwtwdiLYyFK2eFyKv2wg==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.0.tgz",
+ "integrity": "sha512-JSnqehT+sWSPi6e44jTXUW+KiV9284YF9fzPQvfGB4cXlk/m/SJk17CavHCleIvKXDN+jrUw9TZkwAwr85ONWQ==",
"dev": true,
"dependencies": {
- "debug": "^4.3.4"
+ "debug": "^4.3.4",
+ "posthtml-match-helper": "^2.0.2"
},
"engines": {
"node": ">=18"
@@ -178,6 +181,39 @@
"url": "https://opencollective.com/11ty"
}
},
+ "node_modules/@11ty/eleventy-plugin-webc": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-webc/-/eleventy-plugin-webc-0.11.2.tgz",
+ "integrity": "sha512-oa/XlAqI5KtVO7M14qaN92D2yJfBEMMSb66YWY6YZVbRqFSVbjO4WmRJ2Ti2ZZb1FNvxj4ypGNV8VJleGE69xw==",
+ "dev": true,
+ "dependencies": {
+ "@11ty/eleventy-plugin-bundle": "^1.0.4",
+ "@11ty/webc": "^0.11.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
+ "node_modules/@11ty/eleventy-plugin-webc/node_modules/@11ty/eleventy-plugin-bundle": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-1.0.5.tgz",
+ "integrity": "sha512-Esv97j+mOo/yfxjaWl4j8CyszOBsRjU/DOUWOBqVnnDLM8VDXeus2LTJUxF70nAU0g+z+b6fRn8fKnm6b2a/UQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
"node_modules/@11ty/eleventy-utils": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-1.0.3.tgz",
@@ -200,6 +236,18 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/@11ty/eleventy/node_modules/entities": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz",
+ "integrity": "sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/@11ty/eleventy/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -258,6 +306,61 @@
"slash": "^1.0.0"
}
},
+ "node_modules/@11ty/webc": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/@11ty/webc/-/webc-0.11.4.tgz",
+ "integrity": "sha512-q1GMcjNnx9PxUr6jyTT5CdDFma3JWkT5D45wRNYUQ/B4cxTTxpC15b2rYdzNaGuSqB6tsArQ9Qh4BPqg6Xo9cA==",
+ "dev": true,
+ "dependencies": {
+ "@11ty/eleventy-utils": "^1.0.1",
+ "css-tree": "^2.3.1",
+ "dependency-graph": "^0.11.0",
+ "entities": "^4.4.0",
+ "fast-glob": "^3.2.12",
+ "is-glob": "^4.0.3",
+ "nanoid": "^4.0.1",
+ "node-retrieve-globals": "^2.0.7",
+ "parse5": "^7.1.2"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/11ty"
+ }
+ },
+ "node_modules/@11ty/webc/node_modules/dependency-graph": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
+ "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/@11ty/webc/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/@11ty/webc/node_modules/node-retrieve-globals": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-2.0.8.tgz",
+ "integrity": "sha512-mVimS/m8H28kyMdvOIfyMCM8wFNiKXM83ag1yHYP297iVmlCSmCh7Ih4b+ig9/DZ2+LbXZCPLDSZO4yRa5ttyg==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.8.2",
+ "acorn-walk": "^8.2.0"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -672,6 +775,19 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
+ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.0.30",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -1494,9 +1610,9 @@
}
},
"node_modules/liquidjs": {
- "version": "10.16.7",
- "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.16.7.tgz",
- "integrity": "sha512-vjlBDyPxFgUc6vJB+TbAMcxKKKcm4Ee0rj9Je9lcG1I0lr9xvtHgB/ZdNMNAgsPUvJLkLfdrKRd+KzQ5opPfNg==",
+ "version": "10.18.0",
+ "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.18.0.tgz",
+ "integrity": "sha512-gCJPmpmZ3oi2rMMHo/c+bW1LaRF+ZAKYTWQmKXPp0uK9EkWMFRmgbk3+Io4LSJGAOnpCZSgHJbNzcygx3kfAAQ==",
"dev": true,
"dependencies": {
"commander": "^10.0.0"
@@ -1594,6 +1710,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.0.30",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+ "dev": true
+ },
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
@@ -1697,6 +1819,24 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
+ "node_modules/nanoid": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz",
+ "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ }
+ },
"node_modules/node-retrieve-globals": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.0.tgz",
@@ -1798,6 +1938,30 @@
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
"dev": true
},
+ "node_modules/parse5": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz",
+ "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==",
+ "dev": true,
+ "dependencies": {
+ "entities": "^4.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -2213,6 +2377,15 @@
"node": ">=8.0.0"
}
},
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
diff --git a/package.json b/package.json
index d8ac74e..f996456 100644
--- a/package.json
+++ b/package.json
@@ -12,9 +12,10 @@
"type": "module",
"license": "ISC",
"devDependencies": {
- "@11ty/eleventy": "^3.0.0-beta.1",
+ "@11ty/eleventy": "^3.0.0",
"@11ty/eleventy-plugin-rss": "^2.0.2",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
+ "@11ty/eleventy-plugin-webc": "^0.11.2",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"prettier": "^3.3.3"
diff --git a/projects.md b/projects.md
index cdcfc4b..702d568 100644
--- a/projects.md
+++ b/projects.md
@@ -6,7 +6,7 @@ permalink: /projects/
* August - September 2024: [solipsist.social](https://solipsist.social/), a work of conceptual art in the form of a set of single-user fediverse instances running in allowlist mode that only federate with each other
-* September 2023 - now: announcement pending
+* September 2023 - now: [MBTA Go](https://www.mbta.com/goapp), a mobile app displaying schedules and predictions for the Massachusetts Bay Transportation Authority, with an [Elixir backend](https://github.com/mbta/mobile_app_backend) and [Kotlin Multiplatform / SwiftUI / Jetpack Compose frontend](https://github.com/mbta/mobile_app)
* July - September 2023: [Fountain 1917](https://somnolentsucculent.studio/fountain-1917/), the video game adaptation of the infamous Dadaist anti-art sculpture