| Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 19d ago | 1 | # Working in this repo |
| 2 | |
| Six verbs, and the last of the nix 2e24e64 nandi yesterday | 3 | ## The toolchain, and where builds happen |
| Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 19d ago | 4 | |
| The last of the Clojure 284b59c nandi yesterday | 5 | There is no nix in the build. `tools/toolchain.sh` fetches Flutter (which |
| 6 | carries Dart) and Nim as sha256-pinned tarballs into `.toolchain/`, and every |
| 7 | `just` recipe runs inside the environment that script prints. The host brings |
| 8 | a C compiler, OpenSSL, git, curl, unzip and python3 — and GTK with the usual |
| 9 | CMake/Ninja/pkg-config for the Linux target. |
| 10 | |
| 11 | It used to carry a JDK, the Clojure CLI, a maven repo and an Android SDK as |
| 12 | well. Those were ClojureDart's and the APK's, and both are gone. |
| Six verbs, and the last of the nix 2e24e64 nandi yesterday | 13 | |
| 14 | **Prefer Modal for a long build.** A cold Flutter toolchain plus a full |
| 15 | compile is a lot of laptop, and the containers in `.modal/` do it on a real |
| 16 | machine: |
| Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 19d ago | 17 | |
| 18 | ```bash |
| Six verbs, and the last of the nix 2e24e64 nandi yesterday | 19 | just modal dev # the incremental Flutter loop |
| Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 19d ago | 20 | ``` |
| 21 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday | 22 | The containers in `.modal/` are not a third source tree: they are CI config |
| Six verbs, and the last of the nix 2e24e64 nandi yesterday | 23 | that happens to live here, the way `.github/` would be. They run the very same |
| 24 | `tools/toolchain.sh`, which is why a plain Debian image is enough. |
| Build the window somewhere with room for it 1451151 nandi 6d ago | 25 | |
| 26 | `modal app logs` is no substitute for watching that command: it resolves |
| 27 | deployed apps by name, not the ephemeral one a `modal run` creates, and carries |
| 28 | nothing until the Sandbox starts — the image build streams to the client and |
| 29 | nowhere else. |
| 30 | |
| The web container is plain Modal bd10e81 nandi 10h ago | 31 | Two containers, and they are not the same kind of thing — which is why they |
| 32 | are not written the same way. `dev` is a build that ends, and is a |
| 33 | `container.toml` read by `_loader.py`: a sandbox with a volume, a toolchain |
| 34 | and a command that changes. `web` is a deploy, and is plain Modal in |
| 35 | `.modal/web/app.py`, because four constants and a `Popen` did not need a spec |
| 36 | file to be read before the file itself made sense. |
| 37 | |
| 38 | `web` is a deploy: rickub builds `.modal/web/Dockerfile` into |
| The registry it pushes to is the one it has 627b785 nandi 10h ago | 39 | `registry.rickub.com` (`.rickub/workflows/web.yml`), and |
| The web container is plain Modal bd10e81 nandi 10h ago | 40 | `modal deploy .modal/web/app.py` serves that exact tag at a URL, |
| The registry it pushes to is the one it has 627b785 nandi 10h ago | 41 | building nothing. So `just build web` on a laptop and the thing |
| CI builds the image, Modal serves it 5693bd5 nandi 10h ago | 42 | on the internet come from the same two commands, run in different places — |
| 43 | and a deploy is a pull rather than a compile. |
| 44 | |
| Six verbs, and the last of the nix 2e24e64 nandi yesterday | 45 | The containers run as Modal **Sandboxes on a real VM** rather than under |
| 46 | gVisor: a real kernel, a working pty, and memory that is exactly what |
| 47 | `[resources] memory` asks for. |
| Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 19d ago | 48 | |
| Follow the xorg renames, and stop wrapping nix in distrobox a400a00 nandi 17d ago | 49 | One thing this container is *not* representative of: `/etc/localtime` is a |
| 50 | regular file here rather than a symlink, so anything that reads the zone out |
| Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 19d ago | 51 | of its path sees nothing. That is a real deployment shape, not an artefact — |
| 52 | frq.clock handles it. |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago | 53 | |
| Build the window somewhere with room for it 1451151 nandi 6d ago | 54 | ## Never pipe a long task through `tail` |
| 55 | |
| 56 | `tail` and `head` do not emit anything until their input ends, so a build, a |
| 57 | test run or a deploy piped through one shows nothing at all until it is over — |
| 58 | and if it is killed or times out first, its output is lost with it. That is the |
| 59 | opposite of what you want from the commands that take longest. |
| 60 | |
| 61 | Let them write to the terminal, or `tee` them if you want a copy to grep |
| 62 | afterwards: |
| 63 | |
| 64 | ```bash |
| The web container is plain Modal bd10e81 nandi 10h ago | 65 | modal run .modal/dev/container.py 2>&1 | tee /tmp/frq-build.log |
| Build the window somewhere with room for it 1451151 nandi 6d ago | 66 | ``` |
| 67 | |
| 68 | Trim afterwards, on the file, where the whole run is still there to re-read. |
| 69 | The same goes for `grep` and `awk` in a live pipeline: they buffer when their |
| 70 | output is not a terminal, so pass `--line-buffered` / `fflush()` or watch the |
| 71 | file instead. |
| 72 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday | 73 | ## The Nim core |
| 74 | |
| The last of the Clojure 284b59c nandi yesterday | 75 | `nim/` is the program. It owns the state, the screens, the IRC connection and |
| 76 | the signing; Flutter is a renderer over the widget tree it emits. Read |
| 77 | `nim/README.md` before touching it. |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday | 78 | |
| 79 | Two rules the ABI has, both of which cost a segfault to rediscover: |
| 80 | |
| 81 | * Every string the core returns is the **caller's** to free, with `frq_free`. |
| 82 | Nim's allocator is not Dart's. |
| 83 | * `frq_init` runs once before anything else. |
| 84 | |
| The last of the Clojure 284b59c nandi yesterday | 85 | `dart/frq_core` is the other half of that seam. It is a plain Dart package and |
| 86 | not a Flutter one, deliberately: `flutter/pubspec.yaml` depends on the Flutter |
| 87 | SDK, so anything living there needs a Flutter toolchain to check one assertion |
| 88 | about a string, where this resolves and tests on its own. |
| 89 | |
| 90 | The rule that used to be here said new Dart-side code is written in Dart |
| 91 | rather than ClojureDart. There is no ClojureDart left for it to rule against, |
| 92 | but the reasoning it rested on still holds for the next thing: logic goes in |
| 93 | Nim, the platform goes in Dart, and neither is written in a third language |
| 94 | because it is already open. |
| The binding is Dart, and it works f7aea3b nandi yesterday | 95 | |
| Six verbs, and the last of the nix 2e24e64 nandi yesterday | 96 | `just test nim` and `just test dart` need no Flutter, which is most of the |
| The binding is Dart, and it works f7aea3b nandi yesterday | 97 | point: the whole boundary is checkable in about a second. |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday | 98 | |
| The last of the Clojure 284b59c nandi yesterday | 99 | There is no `common/` any more, and that rule went with it. It said a module |
| 100 | stays until there is a wasm build of the core, because a browser has no |
| A web version, from the same core 23846db nandi 12h ago | 101 | dart:ffi. The premise was right and the conclusion was wrong: the answer was |
| 102 | not wasm but `nim js`, which compiles the same core — state, reducer, every |
| 103 | screen — to JavaScript that a page loads with a `<script>` tag. |
| 104 | |
| 105 | So there is a web target again, `just build web`. What differs from the |
| 106 | desktop is only the host: `nim/web/frq/*.nim` shadows `nim/src/frq/*.nim` by |
| 107 | search path (`--path:src --path:web`, later wins), so `frq/conn` is a queue a |
| 108 | WebSocket fills rather than two socket threads, `frq/store` is localStorage, |
| 109 | and `frq/crypto` says plainly that it cannot sign. The shared code above them |
| 110 | imports the same names either way and never learns which host it is on. Dart |
| 111 | does the same thing one layer up, in `dart/frq_core/lib/src/host.dart`. |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday | 112 | |
| The last of the Clojure 284b59c nandi yesterday | 113 | ## The source trees |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago | 114 | |
| 115 | ``` |
| A web version, from the same core 23846db nandi 12h ago | 116 | nim/src the program: state, screens, IRC, signing |
| 117 | nim/web the same program's host half, for a browser |
| 118 | dart/frq_core the binding — plain Dart, not a Flutter package |
| The last of the Clojure 284b59c nandi yesterday | 119 | flutter/lib the renderer, and the app's entry point |
| A web version, from the same core 23846db nandi 12h ago | 120 | flutter/web the page, and the JavaScript that owns the socket |
| Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago | 121 | ``` |
| 122 | |
| The last of the Clojure 284b59c nandi yesterday | 123 | `nim/src/frq/ui.nim` builds a widget tree; `frq_core` carries it across the |
| 124 | FFI as JSON; `flutter/lib/nim_renderer.dart` walks it into Flutter widgets. |
| 125 | The renderer knows the tag vocabulary and nothing else — no screens, no state, |
| 126 | no idea what "connect" means. If a feature ever needs a change on both sides, |
| 127 | the boundary is in the wrong place. |
| 128 | |
| 129 | There used to be two more trees. `src/` was jolt and libcosmic; `common/` and |
| 130 | `flutter/src/` were ClojureDart, compiled for Android, Linux and the web. Both |
| A web version, from the same core 23846db nandi 12h ago | 131 | are gone. The APK went with them and has not come back — it wants |
| 132 | `libfrqcore.so` cross-compiled for Android's ABIs — but the web target has, |
| 133 | by a different road than the one that was expected: `just build web`. |
| 134 | |
| 135 | Three things the web build does not do, all of them written down where they |
| 136 | are done rather than only here. It cannot sign a message, because Ed25519 in |
| 137 | a browser is asynchronous and every signature here is wanted inline, so a |
| 138 | reader is in a guest's position for reactions and edits. It has no |
| 139 | app-password tab, because that wants a blocking call to the reader's own PDS. |
| 140 | And it does not keep a broker token, because `localStorage` is readable by |
| 141 | every script the origin runs. |
| The last of the Clojure 284b59c nandi yesterday | 142 | |
| 143 | Two modules were never ported and are gone rather than moved: `frq.profile` |
| 144 | (the Bluesky profile behind a nick) and `frq.replies` (asking freeq what a |
| 145 | collapsed msgid was). Neither had a screen in the Nim app to appear on. |
| 146 | |