nandi/frqpublic Fork 0
4dfc71908cc3f12174bb0d6dd8688ff3164879c3
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 · 264 lines · 13.6 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
Six verbs, and the last of the nix 2e24e64 nandi 14h ago79just build apk # the debug APK
80just run apk # onto a connected device, and launched
81just run log # logcat
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago82
Six verbs, and the last of the nix 2e24e64 nandi 14h ago83just build desktop # the debug Linux bundle
84just run desktop # and the window
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago85
Six verbs, and the last of the nix 2e24e64 nandi 14h ago86just build web # the web bundle
87just run web # and served on :8080
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago88```
89
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago90### The desktop one
91
Six verbs, and the last of the nix 2e24e64 nandi 14h ago92`just build desktop` is this tree under Flutter's Linux target — the same
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago93screens out of `common/frq/screens/` as the APK, with `frq.hiccup` emitting
94Flutter widgets. There used to be a second desktop GUI beside it, libcosmic
95under jolt, walking the same hiccup through a different renderer; it is gone.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago96
Six verbs, and the last of the nix 2e24e64 nandi 14h ago97Its toolchain is the same `.toolchain/` every other target uses — clojure and
98Flutter, at the same pins — with CMake, Ninja, pkg-config and GTK coming from
99the host where the JDK and
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago100the SDK do. Kept separate rather than merged into one shell because the halves
101are 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 ago102Android SDK.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago103
104Still impure, for one of the two reasons the APK is: pub.dev resolution and
Six verbs, and the last of the nix 2e24e64 nandi 14h ago105Flutter's engine artifacts are network. What it does *not* need is the Android
106SDK, so `just build desktop` never asks `tools/toolchain.sh` for one.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago107
Six verbs, and the last of the nix 2e24e64 nandi 14h ago108GL is the host's, as is the driver that can do it.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago109
110`linux/` is the Flutter template's GTK runner, renamed — `frq` rather than
111`cljd_flutter`, and `uk.nandi.frq` rather than `com.example.cljd_flutter`, so
112the binary, the window title and the GTK application id agree with the APK's
113`applicationId`.
114
115Two things the desktop target changed in the Dart, both of them cases where
116"the phone" had been assumed rather than asked:
117
118* `frq.io.dart/write-private-file!` was a plain write, on the grounds that
119 Android storage is already private to the app. On a Linux desktop it is not:
120 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 ago121 holds a broker token. The desktop branch creates the file, restricts it, then
122 writes — and Dart having no chmod is why that is a three-step process rather
123 than a mode argument.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago124* `frq.oauth.dart` handed the capture page `frq://auth` unconditionally, to
125 raise the app from behind Chrome. Nothing on a desktop claims that scheme, so
126 it is now nil there — which `core/capture-html` already documented as the
127 desktop case and already handled.
128
129### The APK
130
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago131Impure on purpose. Gradle resolves its own dependencies over the network and
Six verbs, and the last of the nix 2e24e64 nandi 14h ago132has sdkmanager install build-tools and a platform into `ANDROID_HOME` as it
133goes, so it cannot run in a sandbox and the SDK it writes to has to be ours.
134`just tools android` fetches Google's command-line tools by pinned sha256 into
135`.toolchain/android-sdk` and accepts the licences; Gradle finishes the job from
136there. All of it is gitignored.
137
138The platform and build-tools versions are deliberately not pinned here: they
139come from whatever Flutter asks Gradle for, and pinning them in a second place
140is how the two drift apart.
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 7d ago141
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago142Two things the Flutter template wanted that are deliberately not here. There is
143no `ndkVersion` in `android/app/build.gradle.kts`: setting it makes Gradle
144fetch that exact NDK, and there is no native code to need one — the app is
145Dart, and path_provider is platform channels rather than JNI. And `ios/`,
146`macos/`, `windows/`, `web/` are deleted; `android/` and `linux/` are the
147targets.
148
149It is signed with `~/.android/debug.keystore`, through the template's
150`signingConfig = signingConfigs.getByName("debug")` — which release builds also
151use, 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 ago152`signingConfigs.release` is wired up.
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago153
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago154## The screens are not rewritten
155
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago156`frq.hiccup` is an interpreter, not a port. It walks the hiccup the screens
157already produce — `[:vbox {:spacing 6} ...]` over about twenty tags, naming no
158toolkit — and emits Flutter widgets, so `common/frq/screens/` is shared across
159all three targets rather than forked per target.
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago160
161This 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 ago162otherwise. The screens were data all along; what genuinely had to be written
163was the other end — the namespaces that touch the host. Which is what `frq.io`
164is for, and where `dart:io` paid for the whole exercise.
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago165
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago166What `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 ago167from the top and diffs its own element tree, so a cell firing rebuilds the
168screen rather than the subtree that read it. Fine at this size.
169
Share the root, and with it the last of the screens 433c3c8 nandi 7d ago170## Every screen shared, and the root that picks between them
Draw the conversation on the phone c0121fe nandi 7d ago171
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago172All of them are in `common/frq/screens/``connect`, `chats`, `chat`,
173`settings` (with Discover and the tab frame) and `app`, which carries the split
174view, the three dialogs and the decision about which screen is showing. Each
175entry point renders `[screens/app]` and nothing else.
176
177Under them: the cells in `frq.cells`, the derivations in `frq.rooms`, the
178chrome metrics in `frq.metrics`, and everything a screen cannot do itself
179behind `frq.actions` — which each target fills in for itself, the way it fills
180in `frq.io`.
Draw the conversation on the phone c0121fe nandi 7d ago181
182What `Length::Fill` means took four goes to get right, and the rule it ended
183at is worth stating once: a child that fills is Flutter's `Expanded`, the
184question is recursive — a plain `:vbox` holding a `:scroll` fills too — a Row
185holding a filling column must `stretch` and be given a height, and a pane that
186fills a column takes the row's width as well, or it is as wide as its longest
187line. Prose in a row is `Flexible` rather than `Expanded`, because Expanded
188hands out equal shares and a button label then wraps down the middle of a
189word.
190
191And `:width-request 0` means no request. Every number is truthy in Clojure, so
192taking it at face value gave the message list a `SizedBox` of zero width and
193an empty screen.
194
195## The older note, kept because the lesson is general
Share the chats screen, and the four things under it 1f55e68 nandi 7d ago196
197`frq.screens.connect` and `frq.screens.chats` are in `common/` now, with the
198cells under them in `frq.cells`, the derivations in `frq.rooms`, the backend
199metrics in `frq.metrics` and the things a screen cannot do itself behind
200`frq.actions`. `frq.app` requires both and the desktop draws them — verified in
201the TUI, including the conversation list with its rooms and previews.
202
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago203The phone draws both.
204
205`Length::Fill` is the whole of what the renderer was missing, in two
206directions. `:fill-height` down a column and a width-less `:entry` across a
207row are the same instruction — *take what is left* — and that is Flutter's
208`Expanded`, not a bigger `mainAxisSize`. A band that says only `max` is handed
209loose constraints by its parent, asks for infinity, and takes the screen with
210it.
211
212So `flexed` wraps whichever children fill, with `fills-column?` for a column
213and `fills-row?` for a row, and a Row holding one is `max` so it has width to
214divide. A `:page` scrolls itself and a `:vbox :fill-height` takes the bounded
215height the Scaffold gives it — which is why nothing wraps the screen any more:
216a scroll view around the tree is exactly what takes that bound away.
217
218What made this expensive was looking for it as an exception. A layout error
219happens after the build: no `try` sees it, `(catch Object ...)` sees it, the
220red error box does not appear, and the log stays empty. `FlutterError.onError`
221is where they go, and installing that handler in `frq.main` should have been
222the first move rather than the tenth.
Share the chats screen, and the four things under it 1f55e68 nandi 7d ago223
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago224## What is still open
225
226The port is finished in the sense that matters — there is no other tree left to
227move from. What remains is work the port never covered:
228
2291. **The OAuth capture on Android.** `common/frq/oauth/core.cljc` has the URL,
230 the handoff payload and the session refresh. What has no Android answer is
231 the capture itself: the flow was written for a client that binds a loopback
232 socket and serves a page the browser redirects to, and an Android app cannot
233 listen on localhost for a browser it does not own. That wants an app link or
234 a custom scheme, an intent filter, and a redirect URI the broker will accept
235 — a decision about freeq's broker, not a porting problem. The web build has
Six verbs, and the last of the nix 2e24e64 nandi 14h ago236 its own answer in `frq.oauth.web`, and `just serve` is why it only
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago237 completes on localhost.
2382. **Calls.** The signaling is IRC and is still in the screens; the media plane
239 it drove was `libjoltmoq` — Opus, H.264, V4L2, ALSA, MoQ over QUIC — under
240 the retired jolt half, and none of it crosses. The Call controls are wired
241 to actions no target installs, so they are dead buttons today. Flutter's
242 camera and audio plugins are the way back in, and that is its own project:
243 either remove the controls or build behind them.
Draw frq.app's own connect screen on the phone 0bc64b5 nandi 7d ago244
245## What a missing tag property looks like
246
247Worth writing down, because it cost an evening. `frq.hiccup` ignored
248`:width-request`, and the connect screen puts two entries side by side in an
249`:hbox` with one. A TextField takes its width from its parent and a Row offers
250unbounded width, so that is a hard layout error — and a layout error happens
251after the build, so it is not an exception anything can catch, paints nothing
252at all rather than Flutter's red box, and takes every sibling in the same
253`children` vector down with it. The screen was blank and the log was empty.
254
255The way through was a harness that renders each candidate in turn with a
256labelled marker between them, so the last label standing says where it died.
257Not guesswork: four wrong theories went past before that — `Center` in an
258unbounded height, `fn*` as a binding name, qualified symbols in `:watch`, a
259`Builder` boundary — each one a three-minute deploy.
260
261It also found that two `:entry` nodes with no `:key` shared one
262TextEditingController, so the host field showed the port. glimmer matches
263children by position when there is no key; a backend holding a controller per
264field needs a name for it.