| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 1 | # frq |
| 2 | |
| 3 | A **[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 | |
| 8 | It is a proof of concept port of [sleek](../sleek), which is the same client in |
| 9 | Rust against egui directly. The screens are sleek's — connect, chats, chat, |
| 10 | discover, settings, under a tab bar — but each is hiccup over glimmer's widget |
| 11 | tags rather than immediate-mode drawing code, and state lives in ratoms instead |
| 12 | of an `AppState` struct. |
| 13 | |
| 14 | ``` |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 15 | src/frq/atproto.jolt handle → DID → PDS → session, and the SASL payloads |
| 16 | src/frq/oauth.jolt the broker flow: login URL, loopback capture, /session |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago | 17 | src/frq/store.jolt the saved sign-in, mode 600 in the config directory |
| Show who is talking, with their Bluesky picture f5548bd nandi 20d ago | 18 | src/frq/avatars.jolt profile pictures, by DID or handle |
| Show the pictures people paste 56cdac3 nandi 20d ago | 19 | src/frq/media.jolt image links: spot them, fetch them once, cache on disk |
| Say when each thing was said dbf3b86 nandi 20d ago | 20 | src/frq/clock.jolt the reader's own zone, twelve-hour times, day headings |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 21 | src/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 ago | 22 | src/frq/state.jolt the ratoms every screen reads, and `apply-msg!` |
| 23 | src/frq/app.jolt the screens |
| 24 | ``` |
| 25 | |
| Stop mistaking a quiet connection for a closed one a225fb1 nandi 20d ago | 26 | ## Tracing |
| 27 | |
| 28 | `FRQ_TRACE=1` prints every IRC line sent and received to stderr, which on |
| 29 | Android is logcat. |
| 30 | |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 31 | ## Running |
| 32 | |
| 33 | `libvidya` from Vidya's Rust/egui backend, then the app: |
| 34 | |
| 35 | ```bash |
| 36 | just lib |
| 37 | just run |
| 38 | ``` |
| 39 | |
| 40 | `just run` is `jolt -M:frq` with `LD_LIBRARY_PATH` pointed at the built |
| 41 | library. It connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick |
| 42 | TLS on the connect screen (or point it at `127.0.0.1`) for a local server's |
| 43 | plain listener: |
| 44 | |
| 45 | ```bash |
| 46 | cargo run --release --bin freeq-server # in the freeq checkout |
| 47 | ``` |
| 48 | |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 49 | ## Signing in |
| 50 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 51 | Three modes on the connect screen. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 52 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 53 | **Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a |
| 54 | loopback port, puts it in `return_to`, and opens |
| 55 | `auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the |
| 56 | PDS and redirects back to that port with the handoff in the URL *fragment*, so |
| 57 | it never reaches a server as a query string. The page frq serves there has one |
| 58 | job: POST the fragment back to itself. What comes back is a single-use SASL |
| 59 | `web-token` and a durable `broker_token`; later connections mint a fresh token |
| 60 | from the durable one at `/session` and skip the browser. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 61 | |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago | 62 | The 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 ago | 63 | a restart resumes without one, along with the handle and nick it belongs to — |
| 64 | and it connects on its own at launch when one is there. |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago | 65 | The web-token beside it is single-use and deliberately not saved. A token the |
| 66 | broker no longer honours is dropped — from disk and memory — and the browser |
| 67 | flow runs once more, rather than failing the same way on every Connect. |
| 68 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 69 | **App password** signs in without a browser, straight to the user's own PDS: |
| 70 | `resolveHandle` → DID → PDS from the DID document → `createSession`. The |
| 71 | password goes to that PDS and nowhere else, is never written to disk, and is |
| 72 | dropped once the session exists. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 73 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 74 | Either way freeq sees only a token. The SASL mechanism is |
| 75 | `ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server |
| 76 | resolves through its own token store, or `method: "pds-session"` with the |
| 77 | server's nonce echoed back so the token cannot be replayed elsewhere. |
| 78 | |
| 79 | A refused sign-in is reported and the connection carries on as a guest. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 80 | |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 81 | ## Android |
| 82 | |
| 83 | An APK with two shared libraries and no Java: `libvidya.so` (vidya's Rust/egui |
| 84 | C ABI, which owns the event loop as the NativeActivity's own library) and |
| 85 | `libjoltapp.so` (frq compiled to a Chez boot image). Both native halves come |
| 86 | from the vidya checkout; only the boot image is frq's. |
| 87 | |
| 88 | ```bash |
| 89 | ./android/build-apk.sh run # build, install, launch on a connected device |
| 90 | ./android/build-apk.sh log # logcat, filtered |
| 91 | ``` |
| 92 | |
| 93 | Needs what vidya's Android build needs — SDK, NDK r29, and a cross-built Chez |
| 94 | in `~/.cache/vidya-chez-android`. |
| 95 | |
| 96 | TLS does not work there: jolt reaches OpenSSL through the dynamic loader, and |
| 97 | Android has no public `libssl` to load. The connect screen falls back to the |
| 98 | plain `:6667` listener on its own, which is why the plain transport is the raw |
| 99 | `socket`/`connect`/`send`/`recv` calls rather than jolt's `java.net.Socket` |
| 100 | surface — that surface does not work on Android either, while the syscalls do. |
| 101 | |
| 102 | ## What the PoC covers |
| 103 | |
| 104 | * TLS (`:6697`, via jolt.mvn-http's OpenSSL bindings) or plain TCP (`:6667`) |
| 105 | * Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive |
| 106 | * Auto-joins `#test` on `irc.freeq.at` |
| 107 | * Join channels, channel buffers with unread counts, send and receive `PRIVMSG` |
| Ask for the backlog freeq restores channels without 94e59a2 nandi 20d ago | 108 | * Backlog on join, and `CHATHISTORY` for the channels freeq restores instead |
| Say when each thing was said dbf3b86 nandi 20d ago | 109 | * Twelve-hour timestamps from the server's own clock, with a heading wherever |
| 110 | the day changes |
| Show what a reply answers, and go there on a click 4d7f34d nandi 20d ago | 111 | * A chip above a reply quoting what it answers, and a click that goes there |
| Show the pictures people paste 56cdac3 nandi 20d ago | 112 | * Inline previews for PNG links, fetched once and cached under |
| Click a picture to see it full size 563ada3 nandi 20d ago | 113 | `$XDG_CACHE_HOME/frq/media`; click one to see it full size |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 114 | * Join/part notices, DMs bucketed under the sender's nick |
| 115 | * Discover list, search over buffers, disconnect |
| Remember which rooms this client has been in 1d062fd nandi 20d ago | 116 | * The rooms you have opened, remembered across runs and listed in the order |
| 117 | you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`) |
| Order the chat list by what you were last in 75e557e nandi 20d ago | 118 | * Conversations listed most recently opened first |
| Show who is talking, with their Bluesky picture f5548bd nandi 20d ago | 119 | * Bluesky avatars beside the sender, resolved from the DID freeq tags each |
| 120 | message with |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 121 | |
| 122 | ## Limits |
| 123 | |
| 124 | * **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 ago | 125 | * **No `did:key` signing, no credential gates, no E2EE.** Sign-in of either |
| 126 | kind needs TLS, so it is desktop-only — the Android build connects as a |
| 127 | guest. |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago | 128 | * **Only the broker token is persisted**, and only for OAuth. An app-password |
| 129 | sign-in is not remembered. |
| Show the pictures people paste 56cdac3 nandi 20d ago | 130 | * **Previews are PNG only** — the tree backend's decoder reads no other |
| 131 | format, and a fetch needs TLS, so the phone shows links. The link is left in |
| 132 | place either way. |
| 133 | * **Nothing evicts the media cache.** |
| Show who is talking, with their Bluesky picture f5548bd nandi 20d ago | 134 | * **No scrollback trimming, reactions, threads, or calls.** |
| Stop mistaking a quiet connection for a closed one a225fb1 nandi 20d ago | 135 | * 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 ago | 136 | * Message lists are keyed vboxes; glimmer-vidya has no `:listbox` yet. |