nandi/frqpublic Fork 0
5fcc5662ed2166b48fd36e4c41e011b50ec06c72
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 · 286 lines · 14.7 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
57just run
58```
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
61`nix develop` and comes back to the same recipe, so `just run` and
62`nix develop --command just run` are one code path rather than two. Nothing has
63to be installed for that but Nix.
Take bb from the flake, so a checkout needs nix and nothing else 270bee0 nandi 16d ago64
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago65`just run` is `jolt -m frq.cosmic` inside `nix develop`, with
66`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 10d 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 10d 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
Merge origin/main, and let one jolt-native answer for both backends 1e8b590 nandi 16d ago136`just tui` is `just run`'s two halves with the other backend under them: this
137tree's source on the flake's everything-else, in the dev shell. jolt-native
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago138carries 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 ago139window, glimmer-tui for the terminal — so one input answers for either, and
140nothing here needs a checkout beside the tree.
Paint the same screens into a terminal ab83b42 nandi 17d ago141
Sign in with a Bluesky identity 5192124 nandi 20d ago142## Signing in
143
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago144Three modes on the connect screen.
Sign in with a Bluesky identity 5192124 nandi 20d ago145
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago146**Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a
147loopback port, puts it in `return_to`, and opens
148`auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the
149PDS and redirects back to that port with the handoff in the URL *fragment*, so
150it never reaches a server as a query string. The page frq serves there has one
151job: POST the fragment back to itself. What comes back is a single-use SASL
152`web-token` and a durable `broker_token`; later connections mint a fresh token
153from the durable one at `/session` and skip the browser.
Sign in with a Bluesky identity 5192124 nandi 20d ago154
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago155The 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 ago156a restart resumes without one, along with the handle and nick it belongs to —
157and it connects on its own at launch when one is there.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago158The web-token beside it is single-use and deliberately not saved. A token the
159broker no longer honours is dropped — from disk and memory — and the browser
160flow runs once more, rather than failing the same way on every Connect.
161
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago162**App password** signs in without a browser, straight to the user's own PDS:
163`resolveHandle` → DID → PDS from the DID document → `createSession`. The
164password goes to that PDS and nowhere else, is never written to disk, and is
165dropped once the session exists.
Sign in with a Bluesky identity 5192124 nandi 20d ago166
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago167Either way freeq sees only a token. The SASL mechanism is
168`ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server
169resolves through its own token store, or `method: "pds-session"` with the
170server's nonce echoed back so the token cannot be replayed elsewhere.
171
172A refused sign-in is reported and the connection carries on as a guest.
Sign in with a Bluesky identity 5192124 nandi 20d ago173
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago174## Android
175
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago176The APK is **ClojureDart and Flutter**, not jolt — see
177[flutter/README.md](flutter/README.md). Nothing here builds it yet.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago178
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago179There was a jolt APK: `libvidya.so` painting through a NativeActivity, with frq
180compiled to an arm64 Chez boot image beside it. It is gone, and so are
181`nix/android.nix`, the `.#apk` outputs and the `just apk` recipe. The reason is
182not the build, which worked — it is that every backend it could paint with is
183retired. Vidya and jvui were experiments, and libcosmic is Wayland, X11 and
184wgpu, so it does not cross to a phone at all.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago185
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago186What the phone gains by the move is most of what it never had. TLS was the
187worst of it: jolt reaches OpenSSL through the dynamic loader and Android has no
188public `libssl`, so sign-in was desktop-only and the connect screen fell back to
189the plain `:6667` listener on its own. `dart:io` carries TLS in the runtime.
190The same goes for the media plane — V4L2 and ALSA are not there either, and
191Flutter has camera and audio plugins that are.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago192
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago193What carries over untouched is `common/` — see the three trees at the top.
194`frq.clock`, `frq.store` and the rest are compiled by both jolt and
195ClojureDart, 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 ago196
197## What the PoC covers
198
199* TLS (`:6697`, via jolt.mvn-http's OpenSSL bindings) or plain TCP (`:6667`)
200* Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive
201* Auto-joins `#test` on `irc.freeq.at`
202* Join channels, channel buffers with unread counts, send and receive `PRIVMSG`
Ask for the backlog freeq restores channels without 94e59a2 nandi 19d ago203* Backlog on join, and `CHATHISTORY` for the channels freeq restores instead
Say when each thing was said dbf3b86 nandi 19d ago204* Twelve-hour timestamps from the server's own clock, with a heading wherever
205 the day changes
Answer a message, not just read that one was answered 4876db8 nandi 19d ago206* A chip above a reply quoting what it answers, and a click that goes there;
207 ↩ beside a sender to answer them, with `+draft/reply` on the way out
Pick any emoji, in colour 18c5ccd nandi 19d ago208* 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 ago209 picker over every emoji the backend can draw (popular first, then Unicode's own
Pick any emoji, in colour 18c5ccd nandi 19d ago210 groups, searchable by name), and a second click on a pill to take yours off
211 — sent as `TAGMSG`, and restored from the server's own tally when the
212 backlog comes back
Show the pictures people paste 56cdac3 nandi 19d ago213* Inline previews for PNG links, fetched once and cached under
Click a picture to see it full size 563ada3 nandi 19d ago214 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
Send a picture by pasting it 8a491ba nandi 19d ago215* Ctrl+V in the draft attaches the picture on the clipboard: it is previewed
216 under the box and uploaded to freeq's media endpoint while you write the line
217 it goes with, and only on the way out does it become the link — which is the
218 whole of what sending an image over IRC means. The draft itself is never
219 written into. Text pastes as text, as it always did: the picture path is the
220 keystroke the field had no text to answer with
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago221* Join/part notices, DMs bucketed under the sender's nick
222* Discover list, search over buffers, disconnect
Remember which rooms this client has been in 1d062fd nandi 19d ago223* The rooms you have opened, remembered across runs and listed in the order
224 you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`)
Order the chat list by what you were last in 75e557e nandi 19d ago225* Conversations listed most recently opened first
Show who is talking, with their Bluesky picture f5548bd nandi 19d ago226* Bluesky avatars beside the sender, resolved from the DID freeq tags each
227 message with
Calls f31ad3d nandi 19d ago228* Calls: a Call button opens one in a channel, a banner offers Join where
229 somebody already has, and in one there is mute, deafen, video and leave. Mute
230 and deafen are separate — a deafened microphone still carries your voice.
231 Whoever turns a camera on appears as a tile; the self-view is labelled You
232 and sits last, where it cannot push a face you are talking to off the row
233
234## Calls
235
236Signaling is IRC and lives here: `+freeq.at/av-start`, `av-join` and `av-leave`
237go out as TAGMSGs and the server broadcasts `+freeq.at/av-state` back, which is
238what actually moves this client's state — a press is optimistic, and the server
239settles it. Losing a race to open a call (`start-collision`) is answered by
240joining the call that won rather than by reporting an error, since the person
241asked to be in a call in that room and there is one.
242
243Media is not IRC and is not here. Audio and video ride MoQ — Media over QUIC —
244through freeq's SFU, and that is `libjoltmoq`: Opus, H.264, capture and
245transport, lifted out of sleek rather than written a second time in jolt.
Rename the source files from .jolt to .clj 3c3a947 nandi 10d ago246`src/frq/av.clj` is the whole of what frq says to it, and two of its rules
Calls f31ad3d nandi 19d ago247shape this side:
248
249* **Nothing calls back.** Status and video are polled, drained by a timer that
250 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 ago251* **A frame is borrowed.** The decoder's own buffer is handed to the backend as
252 a pointer and painted by an `:image` with a `:feed`. The pixels never become a
Calls f31ad3d nandi 19d ago253 jolt value and are never copied on this side, which is the only way thirty
254 frames a second is affordable here.
255
256The SFU is dialled once the server has minted a token, not when we ask to join:
257a remote SFU refuses a connection without one, and the MoQ client then retries
258in a loop that looks exactly like a hang.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago259
260## Limits
261
262* **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 ago263* **No `did:key` signing, no credential gates, no E2EE.** Sign-in of either
264 kind needs TLS, so it is desktop-only — the Android build connects as a
265 guest.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago266* **Only the broker token is persisted**, and only for OAuth. An app-password
267 sign-in is not remembered.
Show the pictures people paste 56cdac3 nandi 19d ago268* **Previews are PNG only** — the tree backend's decoder reads no other
269 format, and a fetch needs TLS, so the phone shows links. The link is left in
270 place either way.
271* **Nothing evicts the media cache.**
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago272* **Calls are desktop-only.** The media plane is V4L2 and ALSA, which Android
273 does not have — and libcosmic, which paints the frames, does not run there
274 either. Flutter's camera and audio plugins are the way in on the phone, and
275 that is its own project.
Calls f31ad3d nandi 19d ago276* **One call at a time**, which is the media plane's rule and the microphone's.
277* **No call is offered in a DM** — freeq's AV signaling is a channel's.
Send a picture by pasting it 8a491ba nandi 19d ago278* **Pasting a picture needs a sign-in and a desktop.** The upload is filed
279 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 ago280 off the clipboard through the backend's `clipboard-image-png!`, which
281 libcosmic backs on the desktop and nothing backs in a terminal. It also shares
Send a picture by pasting it 8a491ba nandi 19d ago282 nothing to your PDS and posts nothing to Bluesky — those fields are opt-in
283 and this client does not send them.
Calls f31ad3d nandi 19d ago284* **No scrollback trimming or threads.**
Stop mistaking a quiet connection for a closed one a225fb1 nandi 19d ago285* 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 ago286* Message lists are keyed vboxes; glimmer-cosmic has no `:listbox` yet.