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