nandi/frqpublic Fork 0
8a86eb395d5f8ac215916c6f849042aa1b481c03
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 · 257 lines · 13.1 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```
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago15src/frq/atproto.jolt handle → DID → PDS → session, and the SASL payloads
16src/frq/oauth.jolt the broker flow: login URL, loopback capture, /session
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago17src/frq/store.jolt the saved sign-in, mode 600 in the config directory
Show who is talking, with their Bluesky picture f5548bd nandi 20d ago18src/frq/avatars.jolt profile pictures, by DID or handle
Copy sleek's profiles b79ebc4 nandi 19d ago19src/frq/profile.jolt who someone is: the Bluesky profile behind a nick
Show the pictures people paste 56cdac3 nandi 20d ago20src/frq/media.jolt image links: spot them, fetch them once, cache on disk
Send a picture by pasting it 8a491ba nandi 20d ago21src/frq/upload.jolt a pasted picture to freeq's media endpoint, as multipart
Calls f31ad3d nandi 19d ago22src/frq/av.jolt calls: the signaling, and a handle on the media plane
Say when each thing was said dbf3b86 nandi 20d ago23src/frq/clock.jolt the reader's own zone, twelve-hour times, day headings
Pick any emoji, in colour 18c5ccd nandi 20d ago24src/frq/emoji.jolt the picker's catalog: every drawable emoji and its name
Sign in with a Bluesky identity 5192124 nandi 20d ago25src/frq/irc.jolt IRC over TLS or TCP: parser, reader thread, SASL, PRIVMSG
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago26src/frq/state.jolt the ratoms every screen reads, and `apply-msg!`
27src/frq/app.jolt the screens
28```
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
Take both native libraries from one place a66d1c1 nandi 19d ago37Both native libraries, then the app:
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago38
39```bash
40just lib
41just run
42```
43
Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago44`just lib` fetches both shared objects this client loads —
45[jolt-native](https://gitlab.com/nandithebull/jolt-native)'s `libvidya`, the
46retained-tree ABI glimmer paints through, and `libjoltmoq`, the AV media plane
47— out of that project's release, by the digests in `scripts/*.dotslash`, and
48links them into `build/lib`. Nothing is compiled: no Rust toolchain, and no
49jolt-native checkout beside this one. It needs `patchelf`, and only to name
50libasound in `libjoltmoq.so`, which v0.1.3 does not — see the comment in
51`scripts/lib.bb`; that goes away with the release that links it. `just bump` moves every pin to the
52latest release at once, and `just bump v0.1.2` to a named one.
53
54`just run` is `jolt -M:frq` with `LD_LIBRARY_PATH` pointed at `build/lib`. It
55connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick TLS on the
56connect 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 ago57plain listener:
58
59```bash
60cargo run --release --bin freeq-server # in the freeq checkout
61```
62
Paint the same screens into a terminal ab83b42 nandi 17d ago63## In a terminal
64
65The screens are hiccup over glimmer's reconciler, and the reconciler does not
66know what is under it — so the same tree paints into a terminal through
67jolt-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 ago68grid of cells instead of a GPU window.
69
70It is the client, not a preview of it. `frq.app/start!` is what a launch does —
71the saved settings, the rooms this client has been in, the sign-in that
72connects itself — and `src/frq/tui.jolt` hands it the terminal's timers instead
73of the window's. Nothing in `frq.app` changed.
Paint the same screens into a terminal ab83b42 nandi 17d ago74
75```bash
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago76nix run .#tui # or: just tui
Paint the same screens into a terminal ab83b42 nandi 17d ago77just 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 ago78just tui --headless --demo # a buffer of its own, no server
79just tui --headless --wait=9000 # long enough to have connected
Fit a conversation on the screen, not two messages of one 8a86eb3 nandi 17d ago80just tui --headless --dump # and the tree the library holds
Paint the same screens into a terminal ab83b42 nandi 17d ago81```
82
83The headless one is `tui_headless` — the same layout and the same painting with
84the 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 ago85CI check should be. It paints once and prints, so `--wait=` is how long the
86client is given first: the default is a picture of the connect screen, because
87that is where a client is a moment after launch, and `--demo` fills a `#tui`
88buffer for a screenshot that is not waiting on a server at all.
89
90Logs go to stderr, which in a terminal session is the screen frq is painting.
91Send them somewhere: `nix run .#tui 2>/tmp/frq.log`.
92
93What a terminal has not got, frq does without: pictures, avatars and the
94lightbox 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 ago95a texture, and there is no texture here.
96
97The spacing is written in points, for a window, and a cell is about eight of
98them across and sixteen down — so the backend is handed both numbers and each
99prop is divided by the axis it measures. A gap of half a cell rounds to
100nothing, which is what `:spacing 8` against a 16-point row is: thirteen
101messages fit where rounding it up left room for two.
102
103The two reserves in `src/frq/app.jolt` are the one thing a scale cannot
104answer, because they are counted in rows of chrome rather than in lengths: a
105window's row is 34 points and a terminal's is one cell. `chrome-row` is where
106that is said, and `frq.tui` sets it.
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago107
108Two things are unpinned, because the terminal backend is not in a jolt-native
109release yet: `libjolttui.so` comes out of a jolt-native checkout's target
110directory (`JOLT_NATIVE=…`, or beside this tree) for `just tui`, and the flake
111carries a second `jolt-native-tui` input at the rev that has it — its own input
112rather than a bump, so the window half stays on the release the rest of the
113tree names. Both become one pin when it ships.
Paint the same screens into a terminal ab83b42 nandi 17d ago114
Sign in with a Bluesky identity 5192124 nandi 20d ago115## Signing in
116
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago117Three modes on the connect screen.
Sign in with a Bluesky identity 5192124 nandi 20d ago118
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago119**Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a
120loopback port, puts it in `return_to`, and opens
121`auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the
122PDS and redirects back to that port with the handoff in the URL *fragment*, so
123it never reaches a server as a query string. The page frq serves there has one
124job: POST the fragment back to itself. What comes back is a single-use SASL
125`web-token` and a durable `broker_token`; later connections mint a fresh token
126from the durable one at `/session` and skip the browser.
Sign in with a Bluesky identity 5192124 nandi 20d ago127
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago128The 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 ago129a restart resumes without one, along with the handle and nick it belongs to —
130and it connects on its own at launch when one is there.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago131The web-token beside it is single-use and deliberately not saved. A token the
132broker no longer honours is dropped — from disk and memory — and the browser
133flow runs once more, rather than failing the same way on every Connect.
134
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago135**App password** signs in without a browser, straight to the user's own PDS:
136`resolveHandle` → DID → PDS from the DID document → `createSession`. The
137password goes to that PDS and nowhere else, is never written to disk, and is
138dropped once the session exists.
Sign in with a Bluesky identity 5192124 nandi 20d ago139
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago140Either way freeq sees only a token. The SASL mechanism is
141`ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server
142resolves through its own token store, or `method: "pds-session"` with the
143server's nonce echoed back so the token cannot be replayed elsewhere.
144
145A refused sign-in is reported and the connection carries on as a guest.
Sign in with a Bluesky identity 5192124 nandi 20d ago146
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago147## Android
148
Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago149An APK whose native halves come from jolt-native's release and whose Jolt half
150is frq's: `libvidya.so` (the Rust/egui C ABI, which owns the event loop as the
151NativeActivity's own library), `libjoltmoq.so` (the media plane) and
152`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 ago153
154```bash
Write the build in babashka, and pin the babashka 34832a8 nandi 18d ago155./android/build-apk.bb run # build, install, launch on a connected device
156./android/build-apk.bb log # logcat, filtered
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago157```
158
Take every jolt-native half from the release, and only from there a008d3b nandi 18d ago159Needs an SDK and a cross-built Chez in `~/.cache/vidya-chez-android`; the NDK
160comes down through `scripts/android-ndk.dotslash`. `just apk` builds the same
161APK as a buck2 graph, which is the incremental way in.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago162
163TLS does not work there: jolt reaches OpenSSL through the dynamic loader, and
164Android has no public `libssl` to load. The connect screen falls back to the
165plain `:6667` listener on its own, which is why the plain transport is the raw
166`socket`/`connect`/`send`/`recv` calls rather than jolt's `java.net.Socket`
167surface — that surface does not work on Android either, while the syscalls do.
168
169## What the PoC covers
170
171* TLS (`:6697`, via jolt.mvn-http's OpenSSL bindings) or plain TCP (`:6667`)
172* Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive
173* Auto-joins `#test` on `irc.freeq.at`
174* Join channels, channel buffers with unread counts, send and receive `PRIVMSG`
Ask for the backlog freeq restores channels without 94e59a2 nandi 20d ago175* Backlog on join, and `CHATHISTORY` for the channels freeq restores instead
Say when each thing was said dbf3b86 nandi 20d ago176* Twelve-hour timestamps from the server's own clock, with a heading wherever
177 the day changes
Answer a message, not just read that one was answered 4876db8 nandi 20d ago178* A chip above a reply quoting what it answers, and a click that goes there;
179 ↩ beside a sender to answer them, with `+draft/reply` on the way out
Pick any emoji, in colour 18c5ccd nandi 20d ago180* Emoji reactions: colour pills under a message, ☺ beside the sender to open a
181 picker over every emoji Vidya can draw (popular first, then Unicode's own
182 groups, searchable by name), and a second click on a pill to take yours off
183 — sent as `TAGMSG`, and restored from the server's own tally when the
184 backlog comes back
Show the pictures people paste 56cdac3 nandi 20d ago185* Inline previews for PNG links, fetched once and cached under
Click a picture to see it full size 563ada3 nandi 20d ago186 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
Send a picture by pasting it 8a491ba nandi 20d ago187* Ctrl+V in the draft attaches the picture on the clipboard: it is previewed
188 under the box and uploaded to freeq's media endpoint while you write the line
189 it goes with, and only on the way out does it become the link — which is the
190 whole of what sending an image over IRC means. The draft itself is never
191 written into. Text pastes as text, as it always did: the picture path is the
192 keystroke the field had no text to answer with
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago193* Join/part notices, DMs bucketed under the sender's nick
194* Discover list, search over buffers, disconnect
Remember which rooms this client has been in 1d062fd nandi 20d ago195* The rooms you have opened, remembered across runs and listed in the order
196 you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`)
Order the chat list by what you were last in 75e557e nandi 20d ago197* Conversations listed most recently opened first
Show who is talking, with their Bluesky picture f5548bd nandi 20d ago198* Bluesky avatars beside the sender, resolved from the DID freeq tags each
199 message with
Calls f31ad3d nandi 19d ago200* Calls: a Call button opens one in a channel, a banner offers Join where
201 somebody already has, and in one there is mute, deafen, video and leave. Mute
202 and deafen are separate — a deafened microphone still carries your voice.
203 Whoever turns a camera on appears as a tile; the self-view is labelled You
204 and sits last, where it cannot push a face you are talking to off the row
205
206## Calls
207
208Signaling is IRC and lives here: `+freeq.at/av-start`, `av-join` and `av-leave`
209go out as TAGMSGs and the server broadcasts `+freeq.at/av-state` back, which is
210what actually moves this client's state — a press is optimistic, and the server
211settles it. Losing a race to open a call (`start-collision`) is answered by
212joining the call that won rather than by reporting an error, since the person
213asked to be in a call in that room and there is one.
214
215Media is not IRC and is not here. Audio and video ride MoQ — Media over QUIC —
216through freeq's SFU, and that is `libjoltmoq`: Opus, H.264, capture and
217transport, lifted out of sleek rather than written a second time in jolt.
218`src/frq/av.jolt` is the whole of what frq says to it, and two of its rules
219shape this side:
220
221* **Nothing calls back.** Status and video are polled, drained by a timer that
222 glimmer runs on the loop thread — the only thread allowed to touch a node.
223* **A frame is borrowed.** The decoder's own buffer is handed to Vidya as a
224 pointer and painted by an `:image` with a `:feed`. The pixels never become a
225 jolt value and are never copied on this side, which is the only way thirty
226 frames a second is affordable here.
227
228The SFU is dialled once the server has minted a token, not when we ask to join:
229a remote SFU refuses a connection without one, and the MoQ client then retries
230in a loop that looks exactly like a hang.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago231
232## Limits
233
234* **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 ago235* **No `did:key` signing, no credential gates, no E2EE.** Sign-in of either
236 kind needs TLS, so it is desktop-only — the Android build connects as a
237 guest.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago238* **Only the broker token is persisted**, and only for OAuth. An app-password
239 sign-in is not remembered.
Show the pictures people paste 56cdac3 nandi 20d ago240* **Previews are PNG only** — the tree backend's decoder reads no other
241 format, and a fetch needs TLS, so the phone shows links. The link is left in
242 place either way.
243* **Nothing evicts the media cache.**
Calls f31ad3d nandi 19d ago244* **Calls are desktop-only.** `libjoltmoq` is not built for Android here, and
245 the camera and microphone paths that are would still need the runtime
246 permissions the APK does not ask for.
247* **One call at a time**, which is the media plane's rule and the microphone's.
248* **No call is offered in a DM** — freeq's AV signaling is a channel's.
Send a picture by pasting it 8a491ba nandi 20d ago249* **Pasting a picture needs a sign-in and a desktop.** The upload is filed
250 under the DID of a live session, so a guest cannot make one; and it is read
251 off the clipboard through the ABI's `vidya_clipboard_image_png`, which
252 arboard backs on desktop and nothing backs on Android. It also shares
253 nothing to your PDS and posts nothing to Bluesky — those fields are opt-in
254 and this client does not send them.
Calls f31ad3d nandi 19d ago255* **No scrollback trimming or threads.**
Stop mistaking a quiet connection for a closed one a225fb1 nandi 20d ago256* 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 ago257* Message lists are keyed vboxes; glimmer-vidya has no `:listbox` yet.