| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d 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 |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 6 | **libcosmic** on the desktop — and, on the phone, by Flutter through |
| 7 | [ClojureDart](https://github.com/tensegritics/ClojureDart) over the same |
| 8 | shared namespaces. See [flutter/README.md](flutter/README.md). |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 9 | |
| 10 | It is a proof of concept port of [sleek](../sleek), which is the same client in |
| 11 | Rust against egui directly. The screens are sleek's — connect, chats, chat, |
| 12 | discover, settings, under a tab bar — but each is hiccup over glimmer's widget |
| 13 | tags rather than immediate-mode drawing code, and state lives in ratoms instead |
| 14 | of an `AppState` struct. |
| 15 | |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 16 | Source lives in three trees, and the file extension is the boundary: |
| 17 | |
| 18 | ``` |
| 19 | common/ .cljc compiled by both jolt and ClojureDart — no jolt, no glimmer |
| 20 | src/ .clj the jolt half: glimmer, jolt.ffi, cosmic + tui backends |
| 21 | flutter/ .cljd the ClojureDart half: Flutter, dart:io — see flutter/README.md |
| 22 | ``` |
| 23 | |
| 24 | ClojureDart reads `.cljd` and `.cljc` and never `.clj`, so a namespace that |
| 25 | reaches for `jolt.host` cannot end up in a Flutter build by accident. What the |
| 26 | two halves share, they ask of `common/frq/io.cljc` — the host's job named once, |
| 27 | answered 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 19d ago | 29 | ``` |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 30 | common/frq/io.cljc the seam: filesystem, environment, config dir, clock |
| 31 | common/frq/clock.cljc IRCv3 time tags → the reader's own zone |
| 32 | common/frq/store.cljc the saved sign-in, mode 600 in the config directory |
| 33 | common/frq/emoji.cljc the picker's catalog: every drawable emoji and its name |
| 34 | src/frq/io/jolt.clj the desktop's answers to the seam |
| 35 | src/frq/atproto.clj handle → DID → PDS → session, and the SASL payloads |
| 36 | src/frq/oauth.clj the broker flow: login URL, loopback capture, /session |
| 37 | src/frq/avatars.clj profile pictures, by DID or handle |
| 38 | src/frq/profile.clj who someone is: the Bluesky profile behind a nick |
| 39 | src/frq/media.clj image links: spot them, fetch them once, cache on disk |
| 40 | src/frq/upload.clj a pasted picture to freeq's media endpoint, as multipart |
| 41 | src/frq/av.clj calls: the signaling, and a handle on the media plane |
| 42 | src/frq/irc.clj IRC over TLS or TCP: parser, reader thread, SASL, PRIVMSG |
| 43 | src/frq/state.clj the ratoms every screen reads, and `apply-msg!` |
| 44 | src/frq/app.clj the screens |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 45 | ``` |
| 46 | |
| Stop mistaking a quiet connection for a closed one a225fb1 nandi 19d ago | 47 | ## Tracing |
| 48 | |
| 49 | `FRQ_TRACE=1` prints every IRC line sent and received to stderr, which on |
| 50 | Android is logcat. |
| 51 | |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 52 | ## Running |
| 53 | |
| Build the desktop libraries rather than fetching a release of them 0b81161 nandi 14d ago | 54 | The app: |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 55 | |
| 56 | ```bash |
| Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago | 57 | just cosmic run |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 58 | ``` |
| 59 | |
| Inline the babashka scripts into the justfile 76dcc6c nandi 10d ago | 60 | Every 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 ago | 61 | `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. |
| 63 | Nothing has to be installed for that but Nix. |
| Take bb from the flake, so a checkout needs nix and nothing else 270bee0 nandi 16d ago | 64 | |
| Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago | 65 | `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 ago | 66 | `LD_LIBRARY_PATH` pointed at the shell's `JOLT_NATIVE_LIB` — the flake's build |
| 67 | of [jolt-native](https://gitlab.com/nandithebull/jolt-native), which carries |
| 68 | the shared objects this client loads: `libjoltcosmic`, the retained-tree ABI |
| 69 | glimmer paints the window through, and `libjolttui`, the same ABI over a grid |
| 70 | of cells. The source frq runs is the working tree; everything under it is built |
| 71 | rather than fetched, at the revs `flake.lock` names. Nothing has to be |
| 72 | installed but Nix, and no jolt-native checkout beside this one. |
| 73 | |
| 74 | There is no jvui and no Vidya any more. Both were experiments: the window is |
| 75 | libcosmic and the terminal is libjolttui, and those are the two backends there |
| 76 | are. |
| Build the desktop libraries rather than fetching a release of them 0b81161 nandi 14d ago | 77 | |
| 78 | `jolt` on its own does not work in this tree: `deps.edn` names both libraries |
| 79 | under `:jolt/native`, so every invocation loads them before it reads a line and |
| 80 | dies if the loader cannot find them. `just repl` is that jolt with the shell |
| 81 | under it — a REPL, or `just repl nrepl-server` for an editor. |
| 82 | |
| 83 | frq connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick TLS on |
| 84 | the 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 19d ago | 85 | plain listener: |
| 86 | |
| 87 | ```bash |
| 88 | cargo run --release --bin freeq-server # in the freeq checkout |
| 89 | ``` |
| 90 | |
| Paint the same screens into a terminal ab83b42 nandi 16d ago | 91 | ## In a terminal |
| 92 | |
| 93 | The screens are hiccup over glimmer's reconciler, and the reconciler does not |
| 94 | know 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 ago | 95 | jolt-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 16d ago | 96 | grid of cells instead of a GPU window. |
| 97 | |
| 98 | It is the client, not a preview of it. `frq.app/start!` is what a launch does — |
| 99 | the saved settings, the rooms this client has been in, the sign-in that |
| Rename the source files from .jolt to .clj 3c3a947 nandi 10d ago | 100 | connects 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 16d ago | 101 | of the window's. Nothing in `frq.app` changed. |
| Paint the same screens into a terminal ab83b42 nandi 16d ago | 102 | |
| 103 | ```bash |
| Start the terminal client the way a launch starts the window one 4f04b91 nandi 16d ago | 104 | nix run .#tui # or: just tui |
| Paint the same screens into a terminal ab83b42 nandi 16d ago | 105 | just tui --headless --cols=90 --rows=60 # one screenshot on stdout |
| Start the terminal client the way a launch starts the window one 4f04b91 nandi 16d ago | 106 | just tui --headless --demo # a buffer of its own, no server |
| 107 | just tui --headless --wait=9000 # long enough to have connected |
| Fit a conversation on the screen, not two messages of one 8a86eb3 nandi 16d ago | 108 | just tui --headless --dump # and the tree the library holds |
| Paint the same screens into a terminal ab83b42 nandi 16d ago | 109 | ``` |
| 110 | |
| 111 | The headless one is `tui_headless` — the same layout and the same painting with |
| 112 | the 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 16d ago | 113 | CI check should be. It paints once and prints, so `--wait=` is how long the |
| 114 | client is given first: the default is a picture of the connect screen, because |
| 115 | that is where a client is a moment after launch, and `--demo` fills a `#tui` |
| 116 | buffer for a screenshot that is not waiting on a server at all. |
| 117 | |
| 118 | Logs go to stderr, which in a terminal session is the screen frq is painting. |
| 119 | Send them somewhere: `nix run .#tui 2>/tmp/frq.log`. |
| 120 | |
| 121 | What a terminal has not got, frq does without: pictures, avatars and the |
| 122 | lightbox 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 16d ago | 123 | a texture, and there is no texture here. |
| 124 | |
| 125 | The spacing is written in points, for a window, and a cell is about eight of |
| 126 | them across and sixteen down — so the backend is handed both numbers and each |
| 127 | prop is divided by the axis it measures. A gap of half a cell rounds to |
| 128 | nothing, which is what `:spacing 8` against a 16-point row is: thirteen |
| 129 | messages fit where rounding it up left room for two. |
| 130 | |
| Rename the source files from .jolt to .clj 3c3a947 nandi 10d ago | 131 | The 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 16d ago | 132 | answer, because they are counted in rows of chrome rather than in lengths: a |
| 133 | window's row is 34 points and a terminal's is one cell. `chrome-row` is where |
| 134 | that is said, and `frq.tui` sets it. |
| Start the terminal client the way a launch starts the window one 4f04b91 nandi 16d ago | 135 | |
| Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago | 136 | `just tui` is `just cosmic run`'s two halves with the other backend under |
| 137 | them: this tree's source on the flake's everything-else, in the dev shell. |
| 138 | jolt-native |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 139 | carries 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 15d ago | 140 | window, glimmer-tui for the terminal — so one input answers for either, and |
| 141 | nothing here needs a checkout beside the tree. |
| Paint the same screens into a terminal ab83b42 nandi 16d ago | 142 | |
| Sign in with a Bluesky identity 5192124 nandi 19d ago | 143 | ## Signing in |
| 144 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 19d ago | 145 | Three modes on the connect screen. |
| Sign in with a Bluesky identity 5192124 nandi 19d ago | 146 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 19d ago | 147 | **Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a |
| 148 | loopback port, puts it in `return_to`, and opens |
| 149 | `auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the |
| 150 | PDS and redirects back to that port with the handoff in the URL *fragment*, so |
| 151 | it never reaches a server as a query string. The page frq serves there has one |
| 152 | job: 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 |
| 154 | from the durable one at `/session` and skip the browser. |
| Sign in with a Bluesky identity 5192124 nandi 19d ago | 155 | |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 19d ago | 156 | 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 | 157 | a restart resumes without one, along with the handle and nick it belongs to — |
| 158 | and it connects on its own at launch when one is there. |
| Remember an OAuth sign-in across restarts 4aa71e7 nandi 19d ago | 159 | The web-token beside it is single-use and deliberately not saved. A token the |
| 160 | broker no longer honours is dropped — from disk and memory — and the browser |
| 161 | flow runs once more, rather than failing the same way on every Connect. |
| 162 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 19d ago | 163 | **App password** signs in without a browser, straight to the user's own PDS: |
| 164 | `resolveHandle` → DID → PDS from the DID document → `createSession`. The |
| 165 | password goes to that PDS and nowhere else, is never written to disk, and is |
| 166 | dropped once the session exists. |
| Sign in with a Bluesky identity 5192124 nandi 19d ago | 167 | |
| Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 19d ago | 168 | Either way freeq sees only a token. The SASL mechanism is |
| 169 | `ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server |
| 170 | resolves through its own token store, or `method: "pds-session"` with the |
| 171 | server's nonce echoed back so the token cannot be replayed elsewhere. |
| 172 | |
| 173 | A refused sign-in is reported and the connection carries on as a guest. |
| Sign in with a Bluesky identity 5192124 nandi 19d ago | 174 | |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 175 | ## Android |
| 176 | |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 177 | The 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 19d ago | 179 | |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 180 | There was a jolt APK: `libvidya.so` painting through a NativeActivity, with frq |
| 181 | compiled 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 |
| 183 | not the build, which worked — it is that every backend it could paint with is |
| 184 | retired. Vidya and jvui were experiments, and libcosmic is Wayland, X11 and |
| 185 | wgpu, so it does not cross to a phone at all. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 186 | |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 187 | What the phone gains by the move is most of what it never had. TLS was the |
| 188 | worst of it: jolt reaches OpenSSL through the dynamic loader and Android has no |
| 189 | public `libssl`, so sign-in was desktop-only and the connect screen fell back to |
| 190 | the plain `:6667` listener on its own. `dart:io` carries TLS in the runtime. |
| 191 | The same goes for the media plane — V4L2 and ALSA are not there either, and |
| 192 | Flutter has camera and audio plugins that are. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 193 | |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago | 194 | What 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 |
| 196 | ClojureDart, and what they need from the host they ask `frq.io` for. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 197 | |
| 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 ago | 204 | * Backlog on join, and `CHATHISTORY` for the channels freeq restores instead |
| Say when each thing was said dbf3b86 nandi 19d ago | 205 | * 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 ago | 207 | * 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 ago | 209 | * 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 | 210 | picker over every emoji the backend can draw (popular first, then Unicode's own |
| Pick any emoji, in colour 18c5ccd nandi 19d ago | 211 | 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 ago | 214 | * Inline previews for PNG links, fetched once and cached under |
| Click a picture to see it full size 563ada3 nandi 19d ago | 215 | `$XDG_CACHE_HOME/frq/media`; click one to see it full size |
| Send a picture by pasting it 8a491ba nandi 19d ago | 216 | * 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 19d ago | 222 | * 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 ago | 224 | * 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 ago | 226 | * Conversations listed most recently opened first |
| Show who is talking, with their Bluesky picture f5548bd nandi 19d ago | 227 | * Bluesky avatars beside the sender, resolved from the DID freeq tags each |
| 228 | message with |
| Calls f31ad3d nandi 19d ago | 229 | * 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 | |
| 237 | Signaling is IRC and lives here: `+freeq.at/av-start`, `av-join` and `av-leave` |
| 238 | go out as TAGMSGs and the server broadcasts `+freeq.at/av-state` back, which is |
| 239 | what actually moves this client's state — a press is optimistic, and the server |
| 240 | settles it. Losing a race to open a call (`start-collision`) is answered by |
| 241 | joining the call that won rather than by reporting an error, since the person |
| 242 | asked to be in a call in that room and there is one. |
| 243 | |
| 244 | Media is not IRC and is not here. Audio and video ride MoQ — Media over QUIC — |
| 245 | through freeq's SFU, and that is `libjoltmoq`: Opus, H.264, capture and |
| 246 | transport, lifted out of sleek rather than written a second time in jolt. |
| Rename the source files from .jolt to .clj 3c3a947 nandi 10d ago | 247 | `src/frq/av.clj` is the whole of what frq says to it, and two of its rules |
| Calls f31ad3d nandi 19d ago | 248 | shape 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 ago | 252 | * **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 ago | 254 | jolt value and are never copied on this side, which is the only way thirty |
| 255 | frames a second is affordable here. |
| 256 | |
| 257 | The SFU is dialled once the server has minted a token, not when we ask to join: |
| 258 | a remote SFU refuses a connection without one, and the MoQ client then retries |
| 259 | in a loop that looks exactly like a hang. |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 19d ago | 260 | |
| 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 19d ago | 264 | * **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 19d ago | 267 | * **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 ago | 269 | * **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 ago | 273 | * **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 ago | 277 | * **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 ago | 279 | * **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 ago | 281 | 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 ago | 283 | 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 ago | 285 | * **No scrollback trimming or threads.** |
| Stop mistaking a quiet connection for a closed one a225fb1 nandi 19d ago | 286 | * 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 ago | 287 | * Message lists are keyed vboxes; glimmer-cosmic has no `:listbox` yet. |