nandi/frqpublic Fork 0
5ce66d5b65a33704b80cac21d9c089bb5032953c
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 · 287 lines · 14.8 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
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago6**libcosmic** on the desktop — and, on the phone, by Flutter through
7[ClojureDart](https://github.com/tensegritics/ClojureDart) over the same
8shared namespaces. See [flutter/README.md](flutter/README.md).
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago9
10It is a proof of concept port of [sleek](../sleek), which is the same client in
11Rust against egui directly. The screens are sleek's — connect, chats, chat,
12discover, settings, under a tab bar — but each is hiccup over glimmer's widget
13tags rather than immediate-mode drawing code, and state lives in ratoms instead
14of an `AppState` struct.
15
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago16Source lives in three trees, and the file extension is the boundary:
17
18```
19common/ .cljc compiled by both jolt and ClojureDart — no jolt, no glimmer
20src/ .clj the jolt half: glimmer, jolt.ffi, cosmic + tui backends
21flutter/ .cljd the ClojureDart half: Flutter, dart:io — see flutter/README.md
22```
23
24ClojureDart reads `.cljd` and `.cljc` and never `.clj`, so a namespace that
25reaches for `jolt.host` cannot end up in a Flutter build by accident. What the
26two halves share, they ask of `common/frq/io.cljc` — the host's job named once,
27answered by `frq.io.jolt` on one side and `frq.io.dart` on the other.
28
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago29```
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago30common/frq/io.cljc the seam: filesystem, environment, config dir, clock
31common/frq/clock.cljc IRCv3 time tags → the reader's own zone
32common/frq/store.cljc the saved sign-in, mode 600 in the config directory
33common/frq/emoji.cljc the picker's catalog: every drawable emoji and its name
34src/frq/io/jolt.clj the desktop's answers to the seam
35src/frq/atproto.clj handle → DID → PDS → session, and the SASL payloads
36src/frq/oauth.clj the broker flow: login URL, loopback capture, /session
37src/frq/avatars.clj profile pictures, by DID or handle
38src/frq/profile.clj who someone is: the Bluesky profile behind a nick
39src/frq/media.clj image links: spot them, fetch them once, cache on disk
40src/frq/upload.clj a pasted picture to freeq's media endpoint, as multipart
41src/frq/av.clj calls: the signaling, and a handle on the media plane
42src/frq/irc.clj IRC over TLS or TCP: parser, reader thread, SASL, PRIVMSG
43src/frq/state.clj the ratoms every screen reads, and `apply-msg!`
44src/frq/app.clj the screens
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago45```
46
Stop mistaking a quiet connection for a closed one a225fb1 nandi 19d ago47## Tracing
48
49`FRQ_TRACE=1` prints every IRC line sent and received to stderr, which on
50Android is logcat.
51
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago52## Running
53
Build the desktop libraries rather than fetching a release of them 0b81161 nandi 15d ago54The app:
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago55
56```bash
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago57just cosmic run
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago58```
59
Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago60Every recipe lives in the `justfile` itself. Each one that runs frq re-enters
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago61`nix develop` and comes back to the same recipe, so `just cosmic run` and
62`nix develop --command just cosmic run` are one code path rather than two.
63Nothing has to be installed for that but Nix.
Take bb from the flake, so a checkout needs nix and nothing else 270bee0 nandi 16d ago64
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago65`just cosmic run` is `jolt -m frq.cosmic` inside `nix develop`, with
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago66`LD_LIBRARY_PATH` pointed at the shell's `JOLT_NATIVE_LIB` — the flake's build
67of [jolt-native](https://gitlab.com/nandithebull/jolt-native), which carries
68the shared objects this client loads: `libjoltcosmic`, the retained-tree ABI
69glimmer paints the window through, and `libjolttui`, the same ABI over a grid
70of cells. The source frq runs is the working tree; everything under it is built
71rather than fetched, at the revs `flake.lock` names. Nothing has to be
72installed but Nix, and no jolt-native checkout beside this one.
73
74There is no jvui and no Vidya any more. Both were experiments: the window is
75libcosmic and the terminal is libjolttui, and those are the two backends there
76are.
Build the desktop libraries rather than fetching a release of them 0b81161 nandi 15d ago77
78`jolt` on its own does not work in this tree: `deps.edn` names both libraries
79under `:jolt/native`, so every invocation loads them before it reads a line and
80dies if the loader cannot find them. `just repl` is that jolt with the shell
81under it — a REPL, or `just repl nrepl-server` for an editor.
82
83frq connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick TLS on
84the 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 ago85plain listener:
86
87```bash
88cargo run --release --bin freeq-server # in the freeq checkout
89```
90
Paint the same screens into a terminal ab83b42 nandi 17d ago91## In a terminal
92
93The screens are hiccup over glimmer's reconciler, and the reconciler does not
94know what is under it — so the same tree paints into a terminal through
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago95jolt-native's `libjolttui`, which exports the same retained-tree ABI over a
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago96grid of cells instead of a GPU window.
97
98It is the client, not a preview of it. `frq.app/start!` is what a launch does —
99the saved settings, the rooms this client has been in, the sign-in that
Rename the source files from .jolt to .clj 3c3a947 nandi 11d ago100connects 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 ago101of the window's. Nothing in `frq.app` changed.
Paint the same screens into a terminal ab83b42 nandi 17d ago102
103```bash
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago104nix run .#tui # or: just tui
Paint the same screens into a terminal ab83b42 nandi 17d ago105just 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 ago106just tui --headless --demo # a buffer of its own, no server
107just tui --headless --wait=9000 # long enough to have connected
Fit a conversation on the screen, not two messages of one 8a86eb3 nandi 17d ago108just tui --headless --dump # and the tree the library holds
Paint the same screens into a terminal ab83b42 nandi 17d ago109```
110
111The headless one is `tui_headless` — the same layout and the same painting with
112the 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 ago113CI check should be. It paints once and prints, so `--wait=` is how long the
114client is given first: the default is a picture of the connect screen, because
115that is where a client is a moment after launch, and `--demo` fills a `#tui`
116buffer for a screenshot that is not waiting on a server at all.
117
118Logs go to stderr, which in a terminal session is the screen frq is painting.
119Send them somewhere: `nix run .#tui 2>/tmp/frq.log`.
120
121What a terminal has not got, frq does without: pictures, avatars and the
122lightbox 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 ago123a texture, and there is no texture here.
124
125The spacing is written in points, for a window, and a cell is about eight of
126them across and sixteen down — so the backend is handed both numbers and each
127prop is divided by the axis it measures. A gap of half a cell rounds to
128nothing, which is what `:spacing 8` against a 16-point row is: thirteen
129messages fit where rounding it up left room for two.
130
Rename the source files from .jolt to .clj 3c3a947 nandi 11d ago131The 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 ago132answer, because they are counted in rows of chrome rather than in lengths: a
133window's row is 34 points and a terminal's is one cell. `chrome-row` is where
134that is said, and `frq.tui` sets it.
Start the terminal client the way a launch starts the window one 4f04b91 nandi 17d ago135
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago136`just tui` is `just cosmic run`'s two halves with the other backend under
137them: this tree's source on the flake's everything-else, in the dev shell.
138jolt-native
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago139carries both native libraries and both Jolt sides — glimmer-cosmic for the
Merge origin/main, and let one jolt-native answer for both backends 1e8b590 nandi 16d ago140window, glimmer-tui for the terminal — so one input answers for either, and
141nothing here needs a checkout beside the tree.
Paint the same screens into a terminal ab83b42 nandi 17d ago142
Sign in with a Bluesky identity 5192124 nandi 20d ago143## Signing in
144
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago145Three modes on the connect screen.
Sign in with a Bluesky identity 5192124 nandi 20d ago146
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago147**Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a
148loopback port, puts it in `return_to`, and opens
149`auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the
150PDS and redirects back to that port with the handoff in the URL *fragment*, so
151it never reaches a server as a query string. The page frq serves there has one
152job: POST the fragment back to itself. What comes back is a single-use SASL
153`web-token` and a durable `broker_token`; later connections mint a fresh token
154from the durable one at `/session` and skip the browser.
Sign in with a Bluesky identity 5192124 nandi 20d ago155
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago156The durable token is saved to `$XDG_CONFIG_HOME/frq/session.edn` (mode 600) so
Connect on launch when an account is remembered 9237cf4 nandi 19d ago157a restart resumes without one, along with the handle and nick it belongs to —
158and it connects on its own at launch when one is there.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago159The web-token beside it is single-use and deliberately not saved. A token the
160broker no longer honours is dropped — from disk and memory — and the browser
161flow runs once more, rather than failing the same way on every Connect.
162
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago163**App password** signs in without a browser, straight to the user's own PDS:
164`resolveHandle` → DID → PDS from the DID document → `createSession`. The
165password goes to that PDS and nowhere else, is never written to disk, and is
166dropped once the session exists.
Sign in with a Bluesky identity 5192124 nandi 20d ago167
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago168Either way freeq sees only a token. The SASL mechanism is
169`ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server
170resolves through its own token store, or `method: "pds-session"` with the
171server's nonce echoed back so the token cannot be replayed elsewhere.
172
173A refused sign-in is reported and the connection carries on as a guest.
Sign in with a Bluesky identity 5192124 nandi 20d ago174
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago175## Android
176
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago177The APK is **ClojureDart and Flutter**, not jolt — see
178[flutter/README.md](flutter/README.md). Nothing here builds it yet.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago179
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago180There was a jolt APK: `libvidya.so` painting through a NativeActivity, with frq
181compiled to an arm64 Chez boot image beside it. It is gone, and so are
182`nix/android.nix`, the `.#apk` outputs and the `just apk` recipe. The reason is
183not the build, which worked — it is that every backend it could paint with is
184retired. Vidya and jvui were experiments, and libcosmic is Wayland, X11 and
185wgpu, so it does not cross to a phone at all.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago186
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago187What the phone gains by the move is most of what it never had. TLS was the
188worst of it: jolt reaches OpenSSL through the dynamic loader and Android has no
189public `libssl`, so sign-in was desktop-only and the connect screen fell back to
190the plain `:6667` listener on its own. `dart:io` carries TLS in the runtime.
191The same goes for the media plane — V4L2 and ALSA are not there either, and
192Flutter has camera and audio plugins that are.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago193
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago194What carries over untouched is `common/` — see the three trees at the top.
195`frq.clock`, `frq.store` and the rest are compiled by both jolt and
196ClojureDart, and what they need from the host they ask `frq.io` for.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago197
198## What the PoC covers
199
200* TLS (`:6697`, via jolt.mvn-http's OpenSSL bindings) or plain TCP (`:6667`)
201* Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive
202* Auto-joins `#test` on `irc.freeq.at`
203* Join channels, channel buffers with unread counts, send and receive `PRIVMSG`
Ask for the backlog freeq restores channels without 94e59a2 nandi 19d ago204* Backlog on join, and `CHATHISTORY` for the channels freeq restores instead
Say when each thing was said dbf3b86 nandi 19d ago205* Twelve-hour timestamps from the server's own clock, with a heading wherever
206 the day changes
Answer a message, not just read that one was answered 4876db8 nandi 19d ago207* A chip above a reply quoting what it answers, and a click that goes there;
208 ↩ beside a sender to answer them, with `+draft/reply` on the way out
Pick any emoji, in colour 18c5ccd nandi 19d ago209* Emoji reactions: colour pills under a message, ☺ beside the sender to open a
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago210 picker over every emoji the backend can draw (popular first, then Unicode's own
Pick any emoji, in colour 18c5ccd nandi 19d ago211 groups, searchable by name), and a second click on a pill to take yours off
212 — sent as `TAGMSG`, and restored from the server's own tally when the
213 backlog comes back
Show the pictures people paste 56cdac3 nandi 19d ago214* Inline previews for PNG links, fetched once and cached under
Click a picture to see it full size 563ada3 nandi 19d ago215 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
Send a picture by pasting it 8a491ba nandi 19d ago216* Ctrl+V in the draft attaches the picture on the clipboard: it is previewed
217 under the box and uploaded to freeq's media endpoint while you write the line
218 it goes with, and only on the way out does it become the link — which is the
219 whole of what sending an image over IRC means. The draft itself is never
220 written into. Text pastes as text, as it always did: the picture path is the
221 keystroke the field had no text to answer with
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago222* Join/part notices, DMs bucketed under the sender's nick
223* Discover list, search over buffers, disconnect
Remember which rooms this client has been in 1d062fd nandi 19d ago224* The rooms you have opened, remembered across runs and listed in the order
225 you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`)
Order the chat list by what you were last in 75e557e nandi 19d ago226* Conversations listed most recently opened first
Show who is talking, with their Bluesky picture f5548bd nandi 19d ago227* Bluesky avatars beside the sender, resolved from the DID freeq tags each
228 message with
Calls f31ad3d nandi 19d ago229* Calls: a Call button opens one in a channel, a banner offers Join where
230 somebody already has, and in one there is mute, deafen, video and leave. Mute
231 and deafen are separate — a deafened microphone still carries your voice.
232 Whoever turns a camera on appears as a tile; the self-view is labelled You
233 and sits last, where it cannot push a face you are talking to off the row
234
235## Calls
236
237Signaling is IRC and lives here: `+freeq.at/av-start`, `av-join` and `av-leave`
238go out as TAGMSGs and the server broadcasts `+freeq.at/av-state` back, which is
239what actually moves this client's state — a press is optimistic, and the server
240settles it. Losing a race to open a call (`start-collision`) is answered by
241joining the call that won rather than by reporting an error, since the person
242asked to be in a call in that room and there is one.
243
244Media is not IRC and is not here. Audio and video ride MoQ — Media over QUIC —
245through freeq's SFU, and that is `libjoltmoq`: Opus, H.264, capture and
246transport, lifted out of sleek rather than written a second time in jolt.
Rename the source files from .jolt to .clj 3c3a947 nandi 11d ago247`src/frq/av.clj` is the whole of what frq says to it, and two of its rules
Calls f31ad3d nandi 19d ago248shape this side:
249
250* **Nothing calls back.** Status and video are polled, drained by a timer that
251 glimmer runs on the loop thread — the only thread allowed to touch a node.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago252* **A frame is borrowed.** The decoder's own buffer is handed to the backend as
253 a pointer and painted by an `:image` with a `:feed`. The pixels never become a
Calls f31ad3d nandi 19d ago254 jolt value and are never copied on this side, which is the only way thirty
255 frames a second is affordable here.
256
257The SFU is dialled once the server has minted a token, not when we ask to join:
258a remote SFU refuses a connection without one, and the MoQ client then retries
259in a loop that looks exactly like a hang.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago260
261## Limits
262
263* **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 ago264* **No `did:key` signing, no credential gates, no E2EE.** Sign-in of either
265 kind needs TLS, so it is desktop-only — the Android build connects as a
266 guest.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago267* **Only the broker token is persisted**, and only for OAuth. An app-password
268 sign-in is not remembered.
Show the pictures people paste 56cdac3 nandi 19d ago269* **Previews are PNG only** — the tree backend's decoder reads no other
270 format, and a fetch needs TLS, so the phone shows links. The link is left in
271 place either way.
272* **Nothing evicts the media cache.**
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago273* **Calls are desktop-only.** The media plane is V4L2 and ALSA, which Android
274 does not have — and libcosmic, which paints the frames, does not run there
275 either. Flutter's camera and audio plugins are the way in on the phone, and
276 that is its own project.
Calls f31ad3d nandi 19d ago277* **One call at a time**, which is the media plane's rule and the microphone's.
278* **No call is offered in a DM** — freeq's AV signaling is a channel's.
Send a picture by pasting it 8a491ba nandi 19d ago279* **Pasting a picture needs a sign-in and a desktop.** The upload is filed
280 under the DID of a live session, so a guest cannot make one; and it is read
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago281 off the clipboard through the backend's `clipboard-image-png!`, which
282 libcosmic backs on the desktop and nothing backs in a terminal. It also shares
Send a picture by pasting it 8a491ba nandi 19d ago283 nothing to your PDS and posts nothing to Bluesky — those fields are opt-in
284 and this client does not send them.
Calls f31ad3d nandi 19d ago285* **No scrollback trimming or threads.**
Stop mistaking a quiet connection for a closed one a225fb1 nandi 19d ago286* A sent line waits up to 200ms for the reader thread to flush it.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago287* Message lists are keyed vboxes; glimmer-cosmic has no `:listbox` yet.