nandi/frqpublic Fork 0
49fb82fc159ac50a064a9617c79e01d16d55641c
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 · 238 lines · 12.5 KBmarkdown Blame HistoryRaw
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago1# The ClojureDart half
2
3Nothing here builds yet. This is the boundary, drawn before the port rather
4than after it, so that the question "can this file go on the phone?" has a
5filesystem answer.
6
7## The three trees
8
9```
10common/ .cljc both compilers. No jolt, no glimmer, no dart.
11src/ .clj jolt: glimmer, jolt.ffi, the cosmic and tui backends.
12flutter/ .cljd ClojureDart: Flutter widgets, dart:io, dart:ffi.
13```
14
15The extension is the boundary and the compilers enforce it. ClojureDart reads
16`.cljd` and `.cljc` and never `.clj`, so a namespace that reaches for
17`jolt.host` cannot accidentally end up in the APK — it is a `.clj` and the Dart
18compiler cannot see it. jolt reads all three, which is why `common/` works at
19all: one copy of `frq.clock`, compiled twice.
20
21Where both need a namespace but the answer differs, `.cljd` wins over `.cljc`
22in ClojureDart's own resolution, so a file here shadows a shared one without
23either side knowing. Reader conditionals work too, with one trap from
24ClojureDart's FAQ: the `:clj` feature is always on under cljd, so `:clj` goes
25**last** in a conditional, and macro code that wants the Clojure path during
26host evaluation asks for `:cljd/clj-host`.
27
28## What has crossed
29
30`frq.io` is the seam — the host's job named once, with `frq.io.jolt` answering
31it on the desktop and `frq.io.dart` here. It carries the filesystem, the
32environment, the config directory and the clock.
33
34Moved to `common/` and running under jolt today:
35
36| namespace | lines | note |
37|---------------|-------|-------------------------------------------------|
38| `frq.emoji` | 1,914 | data; nothing to port |
39| `frq.glyphs` | 84 | data |
40| `frq.av.dial` | 117 | already touched neither jolt nor glimmer |
41| `frq.clock` | 93 | zone-hunting moved into the backends |
42| `frq.store` | 122 | `install -m 600` became `write-private-file!` |
43
44`frq.clock` is the shape the rest should follow. It used to open with four
45guesses at the reader's zone — `TZ`, the target of `/etc/localtime`, the file
46itself by path, then Android's `persist.sys.timezone` — and then convert days
47to a date by printing one with `jolt.time.local` and taking a `subs` of the
48result. Both are gone: the guessing is a libc question and lives in
49`frq.io.jolt`, where Dart answers it in one call instead; the conversion is
50eleven lines of Hinnant's algorithm, checked against `java.time.LocalDate` for
51every day from 1901 to 2052.
52
53## What has not
54
55Roughly 4,000 lines are portable in substance and still `.clj` because the seam
56does not reach far enough yet. In the order worth doing them:
57
581. **`frq.wire`, `frq.msgsig`** — need a crypto seam beside the io one.
592. **`frq.irc`** (433) — the parser is pure; the reader is a blocking thread in
60 a `future`, and Dart has no threads. It becomes a `Stream` over
61 `SecureSocket`, which is also what makes TLS work on the phone at all.
623. **`frq.atproto`** (209), **`frq.oauth`** (182) — hand-rolled HTTPS over
63 `jolt.mvn-http`'s OpenSSL bindings, which is why sign-in is desktop-only
64 today. `dart:io` has TLS in the runtime; this is the single biggest thing
65 the port buys.
664. **`frq.state`** (1,930) — mostly portable logic, but its ratoms are
67 glimmer's. Needs the reactive layer decided first.
685. **`frq.app`** (1,834) — not a port. Flutter brings its own reconciler, so
69 the screens are rewritten against `cljd.flutter`.
70
71Not coming: `frq.tui` (no terminal Flutter), `frq.cosmic` (libcosmic is
72desktop-only), and the media plane — `moq/`, `codec/`, `capture/`, `av/`, about
733,800 lines of FFI against C libraries that do not exist on Android either way.
74`dart:ffi` does not conjure V4L2; that half wants Flutter's camera and audio
75plugins and is its own project.
76
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago77## Building it
78
79```bash
80just apk # the debug APK
81just apk install # and onto a connected device
82just apk run # and launched
83just apk log # logcat
84```
85
86Impure on purpose. Gradle resolves its own dependencies over the network and
87installs build-tools and a platform into `ANDROID_HOME` as it goes, so it can
88neither run in a sandbox nor write to the store. What nix gives is the
89toolchain — clojure, a JDK, Flutter, and an SDK composed by androidenv — and
90the recipe copies that SDK to `flutter/.home` for Gradle to finish off. That
91copy and everything Gradle leaves behind are gitignored.
92
93Two things the Flutter template wanted that are deliberately not here. There is
94no `ndkVersion` in `android/app/build.gradle.kts`: setting it makes Gradle
95fetch that exact NDK, and there is no native code to need one — the app is
96Dart, and path_provider is platform channels rather than JNI. And `ios/`,
97`macos/`, `windows/`, `web/` are deleted; `android/` and `linux/` are the
98targets.
99
100It is signed with `~/.android/debug.keystore`, through the template's
101`signingConfig = signingConfigs.getByName("debug")` — which release builds also
102use, so `flutter build apk --release` is not shippable until a real
103`signingConfigs.release` is wired up. The jolt APK's key was generated inside
104its own nix derivation and never written anywhere, which is why the first
105install over it needed an uninstall: Android will not update a package across a
106signature change.
107
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago108## The screens are not rewritten
109
110`frq.hiccup` is a glimmer backend, the same way glimmer-cosmic and glimmer-tui
111are. It walks the hiccup `frq.app` already produces and emits Flutter widgets,
112so the screens are shared rather than forked.
113
114This is worth being precise about, because the first read of the port said
115otherwise. Measured against the source:
116
117* `frq.state` is 1,930 lines and makes **zero** glimmer calls. Its whole
118 dependency on glimmer is `:refer [atom]` — it shadows core's `atom` with a
119 ratom, and everything after that is `swap!`, `reset!` and `deref`.
120* `frq.app` is 1,834 lines and makes **one**: `r/reaction`. The rest is data —
121 `[:vbox {:spacing 6} ...]` over about twenty tags, naming no toolkit.
122
123So what a Flutter port needs is an interpreter for that data, not a rewrite of
124it. What genuinely has to be ported is the other end: `frq.irc`, `frq.atproto`,
125`frq.oauth`, `frq.avatars`, `frq.media`, `frq.profile`, `frq.platform` — the
126namespaces that touch the host. Which is what `frq.io` is for, and where
127`dart:io` pays for the whole exercise.
128
129What `frq.hiccup` does not do is glimmer's reconciliation: Flutter rebuilds
130from the top and diffs its own element tree, so a cell firing rebuilds the
131screen rather than the subtree that read it. Fine at this size.
132
Draw frq.app's own connect screen on the phone 0bc64b5 nandi 7d ago133`frq.main` paints `frq.app`'s own connect screen, out of
134`common/frq/screens/connect.cljc` — the same file the desktop renders. What it
135reads is `frq.cells` and what it calls is `frq.actions`, and each platform
136fills those in: `frq.state`'s reducers on the desktop, dart:io here.
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago137
Share Discover and Settings, and the frame around them 49fb82f nandi 7d ago138## Five screens shared, all drawn on both
Draw the conversation on the phone c0121fe nandi 7d ago139
Share Discover and Settings, and the frame around them 49fb82f nandi 7d ago140`frq.screens.connect`, `frq.screens.chats`, `frq.screens.chat` and
141`frq.screens.settings` — the last carrying Discover, Settings and the frame the
142tab bar sits in — are in `common/`, with the cells under them in `frq.cells`, the derivations in
Draw the conversation on the phone c0121fe nandi 7d ago143`frq.rooms`, the backend metrics in `frq.metrics` and everything a screen
Share Discover and Settings, and the frame around them 49fb82f nandi 7d ago144cannot do itself behind `frq.actions`. `frq.app` is 565 lines and was 1,744.
Draw the conversation on the phone c0121fe nandi 7d ago145
146What `Length::Fill` means took four goes to get right, and the rule it ended
147at is worth stating once: a child that fills is Flutter's `Expanded`, the
148question is recursive — a plain `:vbox` holding a `:scroll` fills too — a Row
149holding a filling column must `stretch` and be given a height, and a pane that
150fills a column takes the row's width as well, or it is as wide as its longest
151line. Prose in a row is `Flexible` rather than `Expanded`, because Expanded
152hands out equal shares and a button label then wraps down the middle of a
153word.
154
155And `:width-request 0` means no request. Every number is truthy in Clojure, so
156taking it at face value gave the message list a `SizedBox` of zero width and
157an empty screen.
158
159## The older note, kept because the lesson is general
Share the chats screen, and the four things under it 1f55e68 nandi 7d ago160
161`frq.screens.connect` and `frq.screens.chats` are in `common/` now, with the
162cells under them in `frq.cells`, the derivations in `frq.rooms`, the backend
163metrics in `frq.metrics` and the things a screen cannot do itself behind
164`frq.actions`. `frq.app` requires both and the desktop draws them — verified in
165the TUI, including the conversation list with its rooms and previews.
166
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago167The phone draws both.
168
169`Length::Fill` is the whole of what the renderer was missing, in two
170directions. `:fill-height` down a column and a width-less `:entry` across a
171row are the same instruction — *take what is left* — and that is Flutter's
172`Expanded`, not a bigger `mainAxisSize`. A band that says only `max` is handed
173loose constraints by its parent, asks for infinity, and takes the screen with
174it.
175
176So `flexed` wraps whichever children fill, with `fills-column?` for a column
177and `fills-row?` for a row, and a Row holding one is `max` so it has width to
178divide. A `:page` scrolls itself and a `:vbox :fill-height` takes the bounded
179height the Scaffold gives it — which is why nothing wraps the screen any more:
180a scroll view around the tree is exactly what takes that bound away.
181
182What made this expensive was looking for it as an exception. A layout error
183happens after the build: no `try` sees it, `(catch Object ...)` sees it, the
184red error box does not appear, and the log stays empty. `FlutterError.onError`
185is where they go, and installing that handler in `frq.main` should have been
186the first move rather than the tenth.
Share the chats screen, and the four things under it 1f55e68 nandi 7d ago187
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago188## The order to do the rest in
189
Reach irc.freeq.at over TLS from the phone e80514e nandi 7d ago1901. ~~**`frq.irc`**~~ — started. The parser is `common/frq/irc/parse.cljc` now,
191 shared, with `frq.irc` re-exporting it so the twenty-three `irc/tag-value`
192 and `irc/nick-of` call sites in `frq.state` and `frq.av` did not move. The
193 transport is `frq.net.dart`: `SecureSocket`, a `Stream`, no thread and no
194 outbox. **TLS reaches irc.freeq.at:6697 from the phone** — registration and
195 MOTD, which is the thing the jolt APK could never do. What is left of this
196 one is the protocol half: CAP, SASL and the idle-ping logic still live in
197 `src/frq/irc.clj` and want `frq.msgsig` and `frq.atproto` under them first.
Resolve an identity from the phone, over the shared AT Protocol core f735240 nandi 7d ago1982. ~~**`frq.atproto`**~~ — done. `common/frq/atproto/core.cljc` is the JSON,
199 the base64url, the SASL payloads, and a `-req`/`-parse` pair per step of the
200 flow; `frq.atproto` and `frq.atproto.dart` supply the middle. **handle → DID
201 → PDS resolves on the phone**, over `HttpClient`.
202
203 **`frq.oauth`** — half done. `common/frq/oauth/core.cljc` has the URL, the
204 handoff payload and the session refresh. What has no Android answer yet is
205 the capture: the desktop binds a loopback socket and serves a page the
206 browser redirects to, and an Android app cannot listen on localhost for a
207 browser it does not own. That wants an app link or a custom scheme, an
208 intent filter, and a redirect URI the broker will accept — a decision about
209 freeq's broker, not a porting problem.
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 7d ago2103. **`frq.msgsig`** (268), **`frq.wire`** (81) — need a crypto seam beside the
211 io one.
2124. **`frq.avatars`**, **`frq.media`**, **`frq.profile`**, **`frq.platform`** —
213 small, and mostly fetch-and-cache.
2145. **`frq.state`** moves to `common/` as `.cljc`, with `atom` resolved per
215 platform by reader conditional.
2166. **`frq.app`** follows it, and the tags it uses that `frq.hiccup` does not
217 cover yet paint as an orange `?tag` until they do.
Draw frq.app's own connect screen on the phone 0bc64b5 nandi 7d ago218
219## What a missing tag property looks like
220
221Worth writing down, because it cost an evening. `frq.hiccup` ignored
222`:width-request`, and the connect screen puts two entries side by side in an
223`:hbox` with one. A TextField takes its width from its parent and a Row offers
224unbounded width, so that is a hard layout error — and a layout error happens
225after the build, so it is not an exception anything can catch, paints nothing
226at all rather than Flutter's red box, and takes every sibling in the same
227`children` vector down with it. The screen was blank and the log was empty.
228
229The way through was a harness that renders each candidate in turn with a
230labelled marker between them, so the last label standing says where it died.
231Not guesswork: four wrong theories went past before that — `Center` in an
232unbounded height, `fn*` as a binding name, qualified symbols in `:watch`, a
233`Builder` boundary — each one a three-minute deploy.
234
235It also found that two `:entry` nodes with no `:key` shared one
236TextEditingController, so the host field showed the port. glimmer matches
237children by position when there is no key; a backend holding a controller per
238field needs a name for it.