| 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 |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 4 | **[ClojureDart](https://github.com/tensegritics/ClojureDart)**, painted by |
| 5 | Flutter — one set of screens on Android, on the Linux desktop and in a browser. |
| 6 | See [flutter/README.md](flutter/README.md). |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 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, |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 10 | discover, settings, under a tab bar — but each is hiccup over the widget tags |
| 11 | `frq.hiccup` translates into Flutter, and state lives in atoms instead of an |
| 12 | `AppState` struct. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 13 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 14 | Source lives in three trees: |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 15 | |
| 16 | ``` |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 17 | common/ .cljc portable: the screens, the state, the protocol — no dart: |
| 18 | flutter/ .cljd the Flutter half, and the host's answers — flutter/README.md |
| 19 | nim/ .nim the portable logic as a native library — nim/README.md |
| The binding is Dart, and it works f7aea3b nandi 11h ago | 20 | dart/ .dart the binding to it, and not a Flutter package — dart/README.md |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 21 | ``` |
| 22 | |
| The binding is Dart, and it works f7aea3b nandi 11h ago | 23 | The first two are the client as it runs today. `nim/` is where the logic under |
| 24 | the screens is moving, a module at a time, behind a C ABI; `dart/` is what |
| 25 | calls it. One module has made the trip so far and nothing imports it yet — see |
| 26 | `nim/README.md` for what is wired up and what is not. |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 27 | |
| 28 | What `common/` needs of the host it asks `common/frq/io.cljc` for — the seam, |
| 29 | named once and answered per target: `frq.io.dart` on Android and the desktop, |
| 30 | `frq.io.web` in a browser. |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 31 | |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 32 | ``` |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 33 | common/frq/io.cljc the seam: filesystem, environment, config dir, clock |
| 34 | common/frq/clock.cljc IRCv3 time tags → the reader's own zone |
| 35 | common/frq/store.cljc the saved sign-in, mode 600 in the config directory |
| 36 | common/frq/emoji.cljc the picker's catalog: every emoji and its name |
| 37 | common/frq/rooms.cljc the rooms this client has been in, and their order |
| 38 | common/frq/cells.cljc the atoms every screen reads |
| 39 | common/frq/screens/ connect, chats, chat, discover, settings |
| 40 | common/frq/irc/parse.cljc the IRC line parser, tags and all |
| 41 | common/frq/irc/handshake.cljc SASL, driven from shared code |
| 42 | common/frq/atproto/core.cljc handle → DID → PDS → session, and the SASL payloads |
| 43 | common/frq/oauth/core.cljc the broker flow, as far as it is portable |
| 44 | common/frq/msgsig.cljc message signatures |
| 45 | flutter/src/frq/main.cljd the entry point: installs the host, then starts |
| 46 | flutter/src/frq/hiccup.cljd the widget tags, as Flutter |
| 47 | flutter/src/frq/net/ sockets: dart:io on native, WebSocket on the web |
| 48 | flutter/src/frq/io/ the host's answers to the seam |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 49 | ``` |
| 50 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 51 | There used to be a third tree, `src/`, and another runtime under it: jolt, with |
| 52 | [glimmer](https://github.com/jolt-lang/glimmer) components painted by |
| 53 | **libcosmic** in a desktop window and by `libjolttui` in a terminal, plus an |
| 54 | AV media plane over MoQ. It is gone. Flutter is the only frontend now, which is |
| 55 | why `common/` no longer carries `#?(:jolt ...)` reader conditionals and why the |
| 56 | calls, terminal and `nix run .#frq` sections that used to be here are not. |
| 57 | |
| Stop mistaking a quiet connection for a closed one a225fb1 nandi 19d ago | 58 | ## Tracing |
| 59 | |
| 60 | `FRQ_TRACE=1` prints every IRC line sent and received to stderr, which on |
| 61 | Android is logcat. |
| 62 | |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 63 | ## Running |
| 64 | |
| 65 | ```bash |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 66 | just flutter-desktop run # the Linux window |
| 67 | just apk run # onto a connected Android device |
| 68 | just flutter-web serve # a browser, on :8080 |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 69 | ``` |
| 70 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 71 | Every recipe lives in the `justfile` itself. The two that need a toolchain from |
| 72 | Nix re-enter `nix develop` and come back to the same recipe, so `just |
| 73 | flutter-desktop` and `nix develop .#flutter-desktop --command just |
| 74 | flutter-desktop` are one code path rather than two. `just flutter-web` needs no |
| 75 | Nix at all: `tools/toolchain.sh` fetches Flutter, a JDK and the Clojure CLI by |
| 76 | sha256, which is what lets `.modal/flutter-web/` run the same script on a plain |
| 77 | Debian image. |
| 78 | |
| 79 | All three are one `clojure -M:cljd compile` over `flutter/src` and `common/`, |
| 80 | and differ only in which Flutter target runs afterwards. |
| Build the desktop libraries rather than fetching a release of them 0b81161 nandi 15d ago | 81 | |
| 82 | frq connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick TLS on |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 83 | the connect screen (or point it at `127.0.0.1`) for a local server's plain |
| 84 | listener: |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 85 | |
| 86 | ```bash |
| 87 | cargo run --release --bin freeq-server # in the freeq checkout |
| 88 | ``` |
| 89 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 90 | A browser has no TCP, so the web build wants a WebSocket URL in the Server |
| 91 | field — `wss://irc.freeq.at/irc`. And Bluesky sign-in only completes on |
| 92 | `localhost`, because that is the one origin freeq's auth broker will redirect |
| 93 | back to: `just web-local` is what serves the Modal-built bundle there. |
| Paint the same screens into a terminal ab83b42 nandi 17d ago | 94 | |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 95 | ## Signing in |
| 96 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 97 | Three modes on the connect screen. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 98 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 99 | **Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a |
| 100 | loopback port, puts it in `return_to`, and opens |
| 101 | `auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the |
| 102 | PDS and redirects back to that port with the handoff in the URL *fragment*, so |
| 103 | it never reaches a server as a query string. The page frq serves there has one |
| 104 | job: POST the fragment back to itself. What comes back is a single-use SASL |
| 105 | `web-token` and a durable `broker_token`; later connections mint a fresh token |
| 106 | from the durable one at `/session` and skip the browser. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 107 | |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago | 108 | 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 19d ago | 109 | a restart resumes without one, along with the handle and nick it belongs to — |
| 110 | and it connects on its own at launch when one is there. |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago | 111 | The web-token beside it is single-use and deliberately not saved. A token the |
| 112 | broker no longer honours is dropped — from disk and memory — and the browser |
| 113 | flow runs once more, rather than failing the same way on every Connect. |
| 114 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 115 | **App password** signs in without a browser, straight to the user's own PDS: |
| 116 | `resolveHandle` → DID → PDS from the DID document → `createSession`. The |
| 117 | password goes to that PDS and nowhere else, is never written to disk, and is |
| 118 | dropped once the session exists. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 119 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago | 120 | Either way freeq sees only a token. The SASL mechanism is |
| 121 | `ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server |
| 122 | resolves through its own token store, or `method: "pds-session"` with the |
| 123 | server's nonce echoed back so the token cannot be replayed elsewhere. |
| 124 | |
| 125 | A refused sign-in is reported and the connection carries on as a guest. |
| Sign in with a Bluesky identity 5192124 nandi 20d ago | 126 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 127 | ## Targets |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 128 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 129 | Three, from one compile, and what separates them is the host half rather than |
| 130 | the screens. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 131 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 132 | **Android** and **the Linux desktop** are both `dart:io` underneath: |
| 133 | `frq.io.dart` answers the seam, `frq.net.dart` opens a real TCP or TLS socket. |
| 134 | `frq.io.dart/write-private-file!` is the one place that asks which of the two it |
| 135 | is on (`Platform.isAndroid`), because assuming cost a token its file mode. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 136 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 137 | **The web** is not: a browser has no TCP and no filesystem, so `frq.net.web` |
| 138 | carries an IRC WebSocket and `frq.io.web` keeps the seam's files in local |
| 139 | storage. Bluesky sign-in works there only on `localhost` — see Running. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 140 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 141 | What carries over untouched is `common/` — the screens, the state, the parser, |
| 142 | the protocol. See the two trees at the top. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 143 | |
| 144 | ## What the PoC covers |
| 145 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 146 | * TLS (`:6697`, out of `dart:io`) or plain TCP (`:6667`); a WebSocket on the web |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 147 | * Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive |
| 148 | * Auto-joins `#test` on `irc.freeq.at` |
| 149 | * Join channels, channel buffers with unread counts, send and receive `PRIVMSG` |
| Ask for the backlog freeq restores channels without 94e59a2 nandi 19d ago | 150 | * Backlog on join, and `CHATHISTORY` for the channels freeq restores instead |
| Say when each thing was said dbf3b86 nandi 19d ago | 151 | * Twelve-hour timestamps from the server's own clock, with a heading wherever |
| 152 | the day changes |
| Answer a message, not just read that one was answered 4876db8 nandi 19d ago | 153 | * A chip above a reply quoting what it answers, and a click that goes there; |
| 154 | ↩ beside a sender to answer them, with `+draft/reply` on the way out |
| Pick any emoji, in colour 18c5ccd nandi 19d ago | 155 | * 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 ago | 156 | picker over every emoji the backend can draw (popular first, then Unicode's own |
| Pick any emoji, in colour 18c5ccd nandi 19d ago | 157 | groups, searchable by name), and a second click on a pill to take yours off |
| 158 | — sent as `TAGMSG`, and restored from the server's own tally when the |
| 159 | backlog comes back |
| Show the pictures people paste 56cdac3 nandi 19d ago | 160 | * Inline previews for PNG links, fetched once and cached under |
| Click a picture to see it full size 563ada3 nandi 19d ago | 161 | `$XDG_CACHE_HOME/frq/media`; click one to see it full size |
| Send a picture by pasting it 8a491ba nandi 19d ago | 162 | * Ctrl+V in the draft attaches the picture on the clipboard: it is previewed |
| 163 | under the box and uploaded to freeq's media endpoint while you write the line |
| 164 | it goes with, and only on the way out does it become the link — which is the |
| 165 | whole of what sending an image over IRC means. The draft itself is never |
| 166 | written into. Text pastes as text, as it always did: the picture path is the |
| 167 | keystroke the field had no text to answer with |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 168 | * Join/part notices, DMs bucketed under the sender's nick |
| 169 | * Discover list, search over buffers, disconnect |
| Remember which rooms this client has been in 1d062fd nandi 19d ago | 170 | * The rooms you have opened, remembered across runs and listed in the order |
| 171 | you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`) |
| Order the chat list by what you were last in 75e557e nandi 19d ago | 172 | * Conversations listed most recently opened first |
| Show who is talking, with their Bluesky picture f5548bd nandi 19d ago | 173 | * Bluesky avatars beside the sender, resolved from the DID freeq tags each |
| 174 | message with |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 175 | |
| 176 | ## Limits |
| 177 | |
| 178 | * **TLS and plain TCP only** — no WebSocket, no iroh. On Android, plain only. |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 179 | * **No `did:key` signing, no credential gates, no E2EE.** |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago | 180 | * **Only the broker token is persisted**, and only for OAuth. An app-password |
| 181 | sign-in is not remembered. |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 182 | * **Previews are PNG only.** The link is left in place either way. |
| Show the pictures people paste 56cdac3 nandi 19d ago | 183 | * **Nothing evicts the media cache.** |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 184 | * **No calls.** The AV signaling is still in the screens, but the media plane |
| 185 | it drove was `libjoltmoq` under the retired jolt half — Opus, H.264, V4L2 and |
| 186 | ALSA, none of which crosses to Flutter. The Call controls are wired to |
| 187 | actions no target installs. Flutter's camera and audio plugins are the way |
| 188 | back in, and that is its own project. |
| 189 | * **Attaching a picture needs a sign-in.** The upload is filed under the DID |
| 190 | of a live session, so a guest cannot make one. It also shares nothing to your |
| 191 | PDS and posts nothing to Bluesky — those fields are opt-in and this client |
| 192 | does not send them. |
| Calls f31ad3d nandi 19d ago | 193 | * **No scrollback trimming or threads.** |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 11h ago | 194 | * A sent line waits up to 200ms for the reader to flush it. |