nandi/frqpublic Fork 0
87a28ccbc1ad41565d24fa8c4f68e1f9426fd61e
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md · 280 lines · 14.5 KBmarkdown Blame HistoryRaw
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago1# The ClojureDart half
2
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 7d ago3This is the boundary, drawn before the port rather than after it, so that the
4question "can this file go on the phone?" has a filesystem answer. It builds:
5see "Building it" below.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago6
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago7## The two trees
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago8
9```
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago10common/ .cljc portable. No dart: library, no host call except through frq.io.
11flutter/ .cljd Flutter widgets, dart:io, dart:ffi, and the host's answers.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago12```
13
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago14The extension is the boundary and the compiler enforces half of it: ClojureDart
15reads `.cljd` and `.cljc` and never `.clj`. `tools/check-common.py` enforces the
16rest, on every push — a `dart:` library named under `common/` is a namespace
17that compiles for one target and not the others.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago18
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago19Where a shared namespace needs a different answer per target, `.cljd` wins over
20`.cljc` in ClojureDart's own resolution, so a file here shadows a shared one
21without either side knowing. Reader conditionals work too, with one trap from
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago22ClojureDart's FAQ: the `:clj` feature is always on under cljd, so `:clj` goes
23**last** in a conditional, and macro code that wants the Clojure path during
24host evaluation asks for `:cljd/clj-host`.
25
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago26## The seam
27
28`frq.io` is the host's job named once. `frq.io.dart` answers it on Android and
29the Linux desktop; `frq.io.web` answers it in a browser. It carries the
30filesystem, the environment, the config directory and the clock.
31
32`frq.clock` is the shape the rest follows. It used to open with four guesses at
33the reader's zone — `TZ`, the target of `/etc/localtime`, the file itself by
34path, then Android's `persist.sys.timezone` — and then convert days to a date
35by printing one and taking a `subs` of the result. Both are gone: the guessing
36is a libc question and lives behind `local-offset-seconds`, which Dart answers
37in one call; the conversion is eleven lines of Hinnant's algorithm, checked
38against `java.time.LocalDate` for every day from 1901 to 2052.
39
40Adding a host call means adding it to `common/frq/io.cljc` and to every
41implementation. Name it for the result rather than the mechanism — the seam has
42`write-private-file!` and not a chmod, because Dart has no chmod.
43
44## The port, as it finished
45
46This tree began as the phone half of a client whose desktop was jolt: glimmer
47components painted by libcosmic, with `src/` holding the half that could not
48cross. That half is gone now, and what was a migration plan is the whole
49program. The namespaces that made the trip:
50
51| namespace | note |
52|--------------------|-------------------------------------------------------|
53| `frq.emoji` | data; nothing to port |
54| `frq.glyphs` | data |
55| `frq.clock` | zone-hunting moved behind the seam |
56| `frq.store` | `install -m 600` became `write-private-file!` |
57| `frq.irc.parse` | the parser was always pure |
58| `frq.irc.handshake`| SASL, driven from shared code |
59| `frq.msgsig` | needed a crypto seam beside the io one |
60| `frq.atproto.core` | hand-rolled HTTPS became `dart:io`, which has TLS |
61| `frq.oauth.core` | the broker flow, as far as it is portable |
62| `frq.rooms`, `frq.cells`, `frq.screens/*` | rewritten against `cljd.flutter` |
63
64What did not come: the terminal frontend (there is no terminal Flutter), the
65libcosmic one (it is Wayland, X11 and wgpu, and does not cross to a phone), and
66the media plane — `moq/`, `codec/`, `capture/`, `av/`, about 3,800 lines of FFI
67against C libraries Android does not have either way. `dart:ffi` does not
68conjure V4L2; that half wants Flutter's camera and audio plugins and is its own
69project. The Call controls in `frq.screens.chat` are still wired to actions no
70target installs, which is the visible edge of that.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago71
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago72## Building it
73
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago74Three targets out of one tree. The ClojureDart compile is the same command for
75all of them — `clojure -M:cljd compile` over `src/` and `../common` — and what
76differs is only what Flutter is asked to wrap it in.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago77
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago78```bash
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago79just apk # the debug APK
80just apk install # and onto a connected device
81just apk run # and launched
82just apk log # logcat
83
84just flutter-desktop # the debug Linux bundle
85just flutter-desktop run # and the window
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago86
87just flutter-web # the web bundle
88just flutter-web serve # and served on :8080
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago89```
90
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago91### The desktop one
92
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago93`just flutter-desktop` is this tree under Flutter's Linux target — the same
94screens out of `common/frq/screens/` as the APK, with `frq.hiccup` emitting
95Flutter widgets. There used to be a second desktop GUI beside it, libcosmic
96under jolt, walking the same hiccup through a different renderer; it is gone.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago97
98Its toolchain is `devShells.flutter-desktop`, which is the APK shell with the
99Android half swapped out: clojure and Flutter are the same two packages at the
100same pinned rev, and CMake, Ninja, pkg-config and GTK stand where the JDK and
101the SDK do. Kept separate rather than merged into one shell because the halves
102are disjoint — a desktop build has no use for a few hundred megabytes of
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago103Android SDK.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago104
105Still impure, for one of the two reasons the APK is: pub.dev resolution and
106Flutter's engine artifacts are network. What it does *not* need is the
107writable-`ANDROID_HOME` dance, since nothing here writes into the store — so
108there is no `flutter/.home` on this path.
109
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago110nixGL off NixOS: Flutter paints through GL and the driver that can do that is
111the host's.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago112
113`linux/` is the Flutter template's GTK runner, renamed — `frq` rather than
114`cljd_flutter`, and `uk.nandi.frq` rather than `com.example.cljd_flutter`, so
115the binary, the window title and the GTK application id agree with the APK's
116`applicationId`.
117
118Two things the desktop target changed in the Dart, both of them cases where
119"the phone" had been assumed rather than asked:
120
121* `frq.io.dart/write-private-file!` was a plain write, on the grounds that
122 Android storage is already private to the app. On a Linux desktop it is not:
123 the file lands under the XDG data directory with the process umask, and it
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago124 holds a broker token. The desktop branch creates the file, restricts it, then
125 writes — and Dart having no chmod is why that is a three-step process rather
126 than a mode argument.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago127* `frq.oauth.dart` handed the capture page `frq://auth` unconditionally, to
128 raise the app from behind Chrome. Nothing on a desktop claims that scheme, so
129 it is now nil there — which `core/capture-html` already documented as the
130 desktop case and already handled.
131
132### The APK
133
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago134Impure on purpose. Gradle resolves its own dependencies over the network and
135installs build-tools and a platform into `ANDROID_HOME` as it goes, so it can
136neither run in a sandbox nor write to the store. What nix gives is the
137toolchain — clojure, a JDK, Flutter, and an SDK composed by androidenv — and
138the recipe copies that SDK to `flutter/.home` for Gradle to finish off. That
139copy and everything Gradle leaves behind are gitignored.
140
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 7d ago141All of it is the flake's, which it did not used to be. The toolchain was
142`nix shell nixpkgs#clojure nixpkgs#jdk17 nixpkgs#flutter` and the SDK was a
143`nix build --impure --expr` around `builtins.getFlake
144"github:NixOS/nixpkgs/nixos-unstable"` — two references to an *unlocked*
145nixpkgs, so the Flutter that compiled the APK and the nixpkgs under everything
146else could drift apart without flake.lock changing a line. They are
147`devShells.<system>.flutter` and `packages.<system>.android-sdk` now, at the
148pinned rev, and the recipe is `nix develop .#flutter --command` over
149`nix build .#android-sdk`.
150
151The SDK needs `allowUnfree` and `android_sdk.accept_license`, which cannot be
152set on a `legacyPackages` attribute after the fact — hence `androidPkgsFor` in
153the flake, a second `import` of the same locked input rather than a second
154nixpkgs. The Flutter toolchain is kept out of the default dev shell: it brings
155its own Dart and a JDK's worth of closure, and a desktop build wants none of
156it.
157
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago158Two things the Flutter template wanted that are deliberately not here. There is
159no `ndkVersion` in `android/app/build.gradle.kts`: setting it makes Gradle
160fetch that exact NDK, and there is no native code to need one — the app is
161Dart, and path_provider is platform channels rather than JNI. And `ios/`,
162`macos/`, `windows/`, `web/` are deleted; `android/` and `linux/` are the
163targets.
164
165It is signed with `~/.android/debug.keystore`, through the template's
166`signingConfig = signingConfigs.getByName("debug")` — which release builds also
167use, so `flutter build apk --release` is not shippable until a real
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago168`signingConfigs.release` is wired up.
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago169
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago170## The screens are not rewritten
171
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago172`frq.hiccup` is an interpreter, not a port. It walks the hiccup the screens
173already produce — `[:vbox {:spacing 6} ...]` over about twenty tags, naming no
174toolkit — and emits Flutter widgets, so `common/frq/screens/` is shared across
175all three targets rather than forked per target.
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago176
177This is worth being precise about, because the first read of the port said
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago178otherwise. The screens were data all along; what genuinely had to be written
179was the other end — the namespaces that touch the host. Which is what `frq.io`
180is for, and where `dart:io` paid for the whole exercise.
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago181
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago182What `frq.hiccup` does not do is reconciliation of its own: Flutter rebuilds
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago183from the top and diffs its own element tree, so a cell firing rebuilds the
184screen rather than the subtree that read it. Fine at this size.
185
Share the root, and with it the last of the screens 433c3c8 nandi 7d ago186## Every screen shared, and the root that picks between them
Draw the conversation on the phone c0121fe nandi 7d ago187
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago188All of them are in `common/frq/screens/``connect`, `chats`, `chat`,
189`settings` (with Discover and the tab frame) and `app`, which carries the split
190view, the three dialogs and the decision about which screen is showing. Each
191entry point renders `[screens/app]` and nothing else.
192
193Under them: the cells in `frq.cells`, the derivations in `frq.rooms`, the
194chrome metrics in `frq.metrics`, and everything a screen cannot do itself
195behind `frq.actions` — which each target fills in for itself, the way it fills
196in `frq.io`.
Draw the conversation on the phone c0121fe nandi 7d ago197
198What `Length::Fill` means took four goes to get right, and the rule it ended
199at is worth stating once: a child that fills is Flutter's `Expanded`, the
200question is recursive — a plain `:vbox` holding a `:scroll` fills too — a Row
201holding a filling column must `stretch` and be given a height, and a pane that
202fills a column takes the row's width as well, or it is as wide as its longest
203line. Prose in a row is `Flexible` rather than `Expanded`, because Expanded
204hands out equal shares and a button label then wraps down the middle of a
205word.
206
207And `:width-request 0` means no request. Every number is truthy in Clojure, so
208taking it at face value gave the message list a `SizedBox` of zero width and
209an empty screen.
210
211## The older note, kept because the lesson is general
Share the chats screen, and the four things under it 1f55e68 nandi 7d ago212
213`frq.screens.connect` and `frq.screens.chats` are in `common/` now, with the
214cells under them in `frq.cells`, the derivations in `frq.rooms`, the backend
215metrics in `frq.metrics` and the things a screen cannot do itself behind
216`frq.actions`. `frq.app` requires both and the desktop draws them — verified in
217the TUI, including the conversation list with its rooms and previews.
218
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago219The phone draws both.
220
221`Length::Fill` is the whole of what the renderer was missing, in two
222directions. `:fill-height` down a column and a width-less `:entry` across a
223row are the same instruction — *take what is left* — and that is Flutter's
224`Expanded`, not a bigger `mainAxisSize`. A band that says only `max` is handed
225loose constraints by its parent, asks for infinity, and takes the screen with
226it.
227
228So `flexed` wraps whichever children fill, with `fills-column?` for a column
229and `fills-row?` for a row, and a Row holding one is `max` so it has width to
230divide. A `:page` scrolls itself and a `:vbox :fill-height` takes the bounded
231height the Scaffold gives it — which is why nothing wraps the screen any more:
232a scroll view around the tree is exactly what takes that bound away.
233
234What made this expensive was looking for it as an exception. A layout error
235happens after the build: no `try` sees it, `(catch Object ...)` sees it, the
236red error box does not appear, and the log stays empty. `FlutterError.onError`
237is where they go, and installing that handler in `frq.main` should have been
238the first move rather than the tenth.
Share the chats screen, and the four things under it 1f55e68 nandi 7d ago239
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago240## What is still open
241
242The port is finished in the sense that matters — there is no other tree left to
243move from. What remains is work the port never covered:
244
2451. **The OAuth capture on Android.** `common/frq/oauth/core.cljc` has the URL,
246 the handoff payload and the session refresh. What has no Android answer is
247 the capture itself: the flow was written for a client that binds a loopback
248 socket and serves a page the browser redirects to, and an Android app cannot
249 listen on localhost for a browser it does not own. That wants an app link or
250 a custom scheme, an intent filter, and a redirect URI the broker will accept
251 — a decision about freeq's broker, not a porting problem. The web build has
252 its own answer in `frq.oauth.web`, and `just web-local` is why it only
253 completes on localhost.
2542. **Calls.** The signaling is IRC and is still in the screens; the media plane
255 it drove was `libjoltmoq` — Opus, H.264, V4L2, ALSA, MoQ over QUIC — under
256 the retired jolt half, and none of it crosses. The Call controls are wired
257 to actions no target installs, so they are dead buttons today. Flutter's
258 camera and audio plugins are the way back in, and that is its own project:
259 either remove the controls or build behind them.
Draw frq.app's own connect screen on the phone 0bc64b5 nandi 7d ago260
261## What a missing tag property looks like
262
263Worth writing down, because it cost an evening. `frq.hiccup` ignored
264`:width-request`, and the connect screen puts two entries side by side in an
265`:hbox` with one. A TextField takes its width from its parent and a Row offers
266unbounded width, so that is a hard layout error — and a layout error happens
267after the build, so it is not an exception anything can catch, paints nothing
268at all rather than Flutter's red box, and takes every sibling in the same
269`children` vector down with it. The screen was blank and the log was empty.
270
271The way through was a harness that renders each candidate in turn with a
272labelled marker between them, so the last label standing says where it died.
273Not guesswork: four wrong theories went past before that — `Center` in an
274unbounded height, `fn*` as a binding name, qualified symbols in `:watch`, a
275`Builder` boundary — each one a three-minute deploy.
276
277It also found that two `:entry` nodes with no `:key` shared one
278TextEditingController, so the host field showed the port. glimmer matches
279children by position when there is no key; a backend holding a controller per
280field needs a name for it.