nandi/frqpublic Fork 0
5db1a03a9f5085fd57ae3a6aead15da5cc66e55c
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 · 273 lines · 13.9 KBmarkdown Blame HistoryRaw
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago1# frq
2
3A **[freeq](https://github.com/codegod100/freeq)** client written in
4**[jolt](https://github.com/jolt-lang/jolt)**, as
5[glimmer](https://github.com/jolt-lang/glimmer) components painted by
6**[Vidya](https://tangled.org/nandi.uk/vidya)**/egui.
7
8It is a proof of concept port of [sleek](../sleek), which is the same client in
9Rust against egui directly. The screens are sleek's — connect, chats, chat,
10discover, settings, under a tab bar — but each is hiccup over glimmer's widget
11tags rather than immediate-mode drawing code, and state lives in ratoms instead
12of an `AppState` struct.
13
14```
Rename the source files from .jolt to .clj 3c3a947 nandi 11d ago15src/frq/atproto.clj handle → DID → PDS → session, and the SASL payloads
16src/frq/oauth.clj the broker flow: login URL, loopback capture, /session
17src/frq/store.clj the saved sign-in, mode 600 in the config directory
18src/frq/avatars.clj profile pictures, by DID or handle
19src/frq/profile.clj who someone is: the Bluesky profile behind a nick
20src/frq/media.clj image links: spot them, fetch them once, cache on disk
21src/frq/upload.clj a pasted picture to freeq's media endpoint, as multipart
22src/frq/av.clj calls: the signaling, and a handle on the media plane
23src/frq/clock.clj the reader's own zone, twelve-hour times, day headings
24src/frq/emoji.clj the picker's catalog: every drawable emoji and its name
25src/frq/irc.clj IRC over TLS or TCP: parser, reader thread, SASL, PRIVMSG
26src/frq/state.clj the ratoms every screen reads, and `apply-msg!`
27src/frq/app.clj the screens
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago28```
29
Stop mistaking a quiet connection for a closed one a225fb1 nandi 20d ago30## Tracing
31
32`FRQ_TRACE=1` prints every IRC line sent and received to stderr, which on
33Android is logcat.
34
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago35## Running
36
Build the desktop libraries rather than fetching a release of them 0b81161 nandi 15d ago37The app:
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago38
39```bash
40just run
41```
42
Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago43Every recipe lives in the `justfile` itself. Each one that runs frq re-enters
44`nix develop` and comes back to the same recipe, so `just run` and
45`nix develop --command just run` are one code path rather than two. Nothing has
46to be installed for that but Nix.
Take bb from the flake, so a checkout needs nix and nothing else 270bee0 nandi 16d ago47
Build the desktop libraries rather than fetching a release of them 0b81161 nandi 15d ago48`just run` is `jolt -M:frq` inside `nix develop`, with `LD_LIBRARY_PATH`
49pointed at the shell's `JOLT_NATIVE_LIB` — the flake's build of
50[jolt-native](https://gitlab.com/nandithebull/jolt-native), which is both
51shared objects this client loads: `libvidya`, the retained-tree ABI glimmer
52paints through, and `libjoltmoq`, the AV media plane. The source frq runs is
53the working tree; everything under it is built rather than fetched, at the revs
54`flake.lock` names. Nothing has to be installed but Nix, and no jolt-native
55checkout beside this one.
56
57The APK is the other half of that: it takes the two libraries out of
58jolt-native's *release* instead, by the digests in `nix/android.nix`, because a
59derivation's inputs have to be fetchurl. `just bump` moves those pins — and
60deps.edn's glimmer-vidya sha — to the latest release at once, and
61`just bump v0.1.2` to a named one.
62
63`jolt` on its own does not work in this tree: `deps.edn` names both libraries
64under `:jolt/native`, so every invocation loads them before it reads a line and
65dies if the loader cannot find them. `just repl` is that jolt with the shell
66under it — a REPL, or `just repl nrepl-server` for an editor.
67
68frq connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick TLS on
69the connect screen (or point it at `127.0.0.1`) for a local server's
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago70plain listener:
71
72```bash
73cargo run --release --bin freeq-server # in the freeq checkout
74```
75
Paint the same screens into a terminal ab83b42 nandi 17d ago76## In a terminal
77
78The screens are hiccup over glimmer's reconciler, and the reconciler does not
79know what is under it — so the same tree paints into a terminal through
80jolt-native's `libjolttui`, which exports libvidya's retained-tree ABI over a
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago81grid of cells instead of a GPU window.
82
83It is the client, not a preview of it. `frq.app/start!` is what a launch does —
84the saved settings, the rooms this client has been in, the sign-in that
Rename the source files from .jolt to .clj 3c3a947 nandi 11d ago85connects itself — and `src/frq/tui.clj` hands it the terminal's timers instead
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago86of the window's. Nothing in `frq.app` changed.
Paint the same screens into a terminal ab83b42 nandi 17d ago87
88```bash
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago89nix run .#tui # or: just tui
Paint the same screens into a terminal ab83b42 nandi 17d ago90just tui --headless --cols=90 --rows=60 # one screenshot on stdout
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago91just tui --headless --demo # a buffer of its own, no server
92just tui --headless --wait=9000 # long enough to have connected
Fit a conversation on the screen, not two messages of one 8a86eb3 nandi 17d ago93just tui --headless --dump # and the tree the library holds
Paint the same screens into a terminal ab83b42 nandi 17d ago94```
95
96The headless one is `tui_headless` — the same layout and the same painting with
97the writer taken off the end — which is what a screenshot in a bug report or a
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago98CI check should be. It paints once and prints, so `--wait=` is how long the
99client is given first: the default is a picture of the connect screen, because
100that is where a client is a moment after launch, and `--demo` fills a `#tui`
101buffer for a screenshot that is not waiting on a server at all.
102
103Logs go to stderr, which in a terminal session is the screen frq is painting.
104Send them somewhere: `nix run .#tui 2>/tmp/frq.log`.
105
106What a terminal has not got, frq does without: pictures, avatars and the
107lightbox draw nothing, and calls are off — the media plane paints frames into
Fit a conversation on the screen, not two messages of one 8a86eb3 nandi 17d ago108a texture, and there is no texture here.
109
110The spacing is written in points, for a window, and a cell is about eight of
111them across and sixteen down — so the backend is handed both numbers and each
112prop is divided by the axis it measures. A gap of half a cell rounds to
113nothing, which is what `:spacing 8` against a 16-point row is: thirteen
114messages fit where rounding it up left room for two.
115
Rename the source files from .jolt to .clj 3c3a947 nandi 11d ago116The two reserves in `src/frq/app.clj` are the one thing a scale cannot
Fit a conversation on the screen, not two messages of one 8a86eb3 nandi 17d ago117answer, because they are counted in rows of chrome rather than in lengths: a
118window's row is 34 points and a terminal's is one cell. `chrome-row` is where
119that is said, and `frq.tui` sets it.
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago120
Merge origin/main, and let one jolt-native answer for both backends 1e8b590 nandi 16d ago121`just tui` is `just run`'s two halves with the other backend under them: this
122tree's source on the flake's everything-else, in the dev shell. jolt-native
123carries both native libraries and both Jolt sides — glimmer-vidya for the
124window, glimmer-tui for the terminal — so one input answers for either, and
125nothing here needs a checkout beside the tree.
Paint the same screens into a terminal ab83b42 nandi 17d ago126
Sign in with a Bluesky identity 5192124 nandi 20d ago127## Signing in
128
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago129Three modes on the connect screen.
Sign in with a Bluesky identity 5192124 nandi 20d ago130
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago131**Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a
132loopback port, puts it in `return_to`, and opens
133`auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the
134PDS and redirects back to that port with the handoff in the URL *fragment*, so
135it never reaches a server as a query string. The page frq serves there has one
136job: POST the fragment back to itself. What comes back is a single-use SASL
137`web-token` and a durable `broker_token`; later connections mint a fresh token
138from the durable one at `/session` and skip the browser.
Sign in with a Bluesky identity 5192124 nandi 20d ago139
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago140The durable token is saved to `$XDG_CONFIG_HOME/frq/session.edn` (mode 600) so
Connect on launch when an account is remembered 9237cf4 nandi 20d ago141a restart resumes without one, along with the handle and nick it belongs to —
142and it connects on its own at launch when one is there.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago143The web-token beside it is single-use and deliberately not saved. A token the
144broker no longer honours is dropped — from disk and memory — and the browser
145flow runs once more, rather than failing the same way on every Connect.
146
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago147**App password** signs in without a browser, straight to the user's own PDS:
148`resolveHandle` → DID → PDS from the DID document → `createSession`. The
149password goes to that PDS and nowhere else, is never written to disk, and is
150dropped once the session exists.
Sign in with a Bluesky identity 5192124 nandi 20d ago151
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago152Either way freeq sees only a token. The SASL mechanism is
153`ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server
154resolves through its own token store, or `method: "pds-session"` with the
155server's nonce echoed back so the token cannot be replayed elsewhere.
156
157A refused sign-in is reported and the connection carries on as a guest.
Sign in with a Bluesky identity 5192124 nandi 20d ago158
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago159## Android
160
Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago161An APK whose native halves come from jolt-native's release and whose Jolt half
162is frq's: `libvidya.so` (the Rust/egui C ABI, which owns the event loop as the
163NativeActivity's own library), `libjoltmoq.so` (the media plane) and
164`libjoltapp.so` (frq compiled to a Chez boot image, linked against both).
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago165
166```bash
Build the APK as derivations only, and retire the buck2 graph 3bc3aaa nandi 16d ago167just apk run # build, install, launch on a connected device
168just apk log # logcat, filtered
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago169```
170
Build the APK as derivations only, and retire the buck2 graph 3bc3aaa nandi 16d ago171Needs nothing on the machine but Nix and an `adb`: the build is
172[`nix/android.nix`](nix/android.nix), and the SDK, the NDK, the arm64 Chez
173cross target and the OpenSSL the app carries are all built or fetched there.
174`nix build .#apk` is the same thing without adb; on a machine with a remote
175builder, hand it the store rather than a `builders` entry —
176`FRQ_NIX_STORE=ssh-ng://eu.nixbuild.net just apk`, and see the header of
177`nix/android.nix` for why.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago178
179TLS does not work there: jolt reaches OpenSSL through the dynamic loader, and
180Android has no public `libssl` to load. The connect screen falls back to the
181plain `:6667` listener on its own, which is why the plain transport is the raw
182`socket`/`connect`/`send`/`recv` calls rather than jolt's `java.net.Socket`
183surface — that surface does not work on Android either, while the syscalls do.
184
185## What the PoC covers
186
187* TLS (`:6697`, via jolt.mvn-http's OpenSSL bindings) or plain TCP (`:6667`)
188* Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive
189* Auto-joins `#test` on `irc.freeq.at`
190* Join channels, channel buffers with unread counts, send and receive `PRIVMSG`
Ask for the backlog freeq restores channels without 94e59a2 nandi 20d ago191* Backlog on join, and `CHATHISTORY` for the channels freeq restores instead
Say when each thing was said dbf3b86 nandi 20d ago192* Twelve-hour timestamps from the server's own clock, with a heading wherever
193 the day changes
Answer a message, not just read that one was answered 4876db8 nandi 20d ago194* A chip above a reply quoting what it answers, and a click that goes there;
195 ↩ beside a sender to answer them, with `+draft/reply` on the way out
Pick any emoji, in colour 18c5ccd nandi 19d ago196* Emoji reactions: colour pills under a message, ☺ beside the sender to open a
197 picker over every emoji Vidya can draw (popular first, then Unicode's own
198 groups, searchable by name), and a second click on a pill to take yours off
199 — sent as `TAGMSG`, and restored from the server's own tally when the
200 backlog comes back
Show the pictures people paste 56cdac3 nandi 20d ago201* Inline previews for PNG links, fetched once and cached under
Click a picture to see it full size 563ada3 nandi 20d ago202 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
Send a picture by pasting it 8a491ba nandi 19d ago203* Ctrl+V in the draft attaches the picture on the clipboard: it is previewed
204 under the box and uploaded to freeq's media endpoint while you write the line
205 it goes with, and only on the way out does it become the link — which is the
206 whole of what sending an image over IRC means. The draft itself is never
207 written into. Text pastes as text, as it always did: the picture path is the
208 keystroke the field had no text to answer with
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago209* Join/part notices, DMs bucketed under the sender's nick
210* Discover list, search over buffers, disconnect
Remember which rooms this client has been in 1d062fd nandi 20d ago211* The rooms you have opened, remembered across runs and listed in the order
212 you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`)
Order the chat list by what you were last in 75e557e nandi 20d ago213* Conversations listed most recently opened first
Show who is talking, with their Bluesky picture f5548bd nandi 20d ago214* Bluesky avatars beside the sender, resolved from the DID freeq tags each
215 message with
Calls f31ad3d nandi 19d ago216* Calls: a Call button opens one in a channel, a banner offers Join where
217 somebody already has, and in one there is mute, deafen, video and leave. Mute
218 and deafen are separate — a deafened microphone still carries your voice.
219 Whoever turns a camera on appears as a tile; the self-view is labelled You
220 and sits last, where it cannot push a face you are talking to off the row
221
222## Calls
223
224Signaling is IRC and lives here: `+freeq.at/av-start`, `av-join` and `av-leave`
225go out as TAGMSGs and the server broadcasts `+freeq.at/av-state` back, which is
226what actually moves this client's state — a press is optimistic, and the server
227settles it. Losing a race to open a call (`start-collision`) is answered by
228joining the call that won rather than by reporting an error, since the person
229asked to be in a call in that room and there is one.
230
231Media is not IRC and is not here. Audio and video ride MoQ — Media over QUIC —
232through freeq's SFU, and that is `libjoltmoq`: Opus, H.264, capture and
233transport, lifted out of sleek rather than written a second time in jolt.
Rename the source files from .jolt to .clj 3c3a947 nandi 11d ago234`src/frq/av.clj` is the whole of what frq says to it, and two of its rules
Calls f31ad3d nandi 19d ago235shape this side:
236
237* **Nothing calls back.** Status and video are polled, drained by a timer that
238 glimmer runs on the loop thread — the only thread allowed to touch a node.
239* **A frame is borrowed.** The decoder's own buffer is handed to Vidya as a
240 pointer and painted by an `:image` with a `:feed`. The pixels never become a
241 jolt value and are never copied on this side, which is the only way thirty
242 frames a second is affordable here.
243
244The SFU is dialled once the server has minted a token, not when we ask to join:
245a remote SFU refuses a connection without one, and the MoQ client then retries
246in a loop that looks exactly like a hang.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago247
248## Limits
249
250* **TLS and plain TCP only** — no WebSocket, no iroh. On Android, plain only.
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago251* **No `did:key` signing, no credential gates, no E2EE.** Sign-in of either
252 kind needs TLS, so it is desktop-only — the Android build connects as a
253 guest.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago254* **Only the broker token is persisted**, and only for OAuth. An app-password
255 sign-in is not remembered.
Show the pictures people paste 56cdac3 nandi 20d ago256* **Previews are PNG only** — the tree backend's decoder reads no other
257 format, and a fetch needs TLS, so the phone shows links. The link is left in
258 place either way.
259* **Nothing evicts the media cache.**
Calls f31ad3d nandi 19d ago260* **Calls are desktop-only.** `libjoltmoq` is not built for Android here, and
261 the camera and microphone paths that are would still need the runtime
262 permissions the APK does not ask for.
263* **One call at a time**, which is the media plane's rule and the microphone's.
264* **No call is offered in a DM** — freeq's AV signaling is a channel's.
Send a picture by pasting it 8a491ba nandi 19d ago265* **Pasting a picture needs a sign-in and a desktop.** The upload is filed
266 under the DID of a live session, so a guest cannot make one; and it is read
267 off the clipboard through the ABI's `vidya_clipboard_image_png`, which
268 arboard backs on desktop and nothing backs on Android. It also shares
269 nothing to your PDS and posts nothing to Bluesky — those fields are opt-in
270 and this client does not send them.
Calls f31ad3d nandi 19d ago271* **No scrollback trimming or threads.**
Stop mistaking a quiet connection for a closed one a225fb1 nandi 20d ago272* A sent line waits up to 200ms for the reader thread to flush it.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago273* Message lists are keyed vboxes; glimmer-vidya has no `:listbox` yet.