nandi/frqpublic Fork 0
ab83b42
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Paint the same screens into a terminal

The screens are hiccup over glimmer's reconciler, and the reconciler does not
know what is under it — so the tree that egui paints as a window goes through
libjolttui as cells instead, and frq.app is untouched. frq.tui is the whole
of what that costs here: it requires glimmer-tui.core after frq.app, so the
backend installed last is the terminal, and seeds a #tui buffer through the
real push-message! so a session started for a look at the layout has a
conversation in it rather than an empty column.

Two things are unpinned, because the backend is not in a jolt-native release:
scripts/tui.bb takes libjolttui.so out of a checkout's target directory and
resolves glimmer-tui from the same place, and the flake carries a second
jolt-native input at the rev that has it — its own input rather than a bump,
so the window half stays on the release the rest of the tree names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-01T22:58:25-07:00 Browse files
ab83b42 parent: 4a372ae
modified README.md +33 -0
@@ -60,6 +60,39 @@ plain listener:
6060 cargo run --release --bin freeq-server # in the freeq checkout
6161 ```
6262
63+## In a terminal
64+
65+The screens are hiccup over glimmer's reconciler, and the reconciler does not
66+know what is under it — so the same tree paints into a terminal through
67+jolt-native's `libjolttui`, which exports libvidya's retained-tree ABI over a
68+grid of cells instead of a GPU window. `src/frq/tui.jolt` is the whole of what
69+that costs on this side: it requires `glimmer-tui.core` after `frq.app`, so the
70+backend installed last is the terminal, and seeds a `#tui` buffer so a session
71+started for a look at the layout has a conversation in it.
72+
73+```bash
74+just tui # the terminal, until Ctrl-Q
75+just tui --headless --cols=90 --rows=60 # one screenshot on stdout
76+```
77+
78+The headless one is `tui_headless` — the same layout and the same painting with
79+the writer taken off the end — which is what a screenshot in a bug report or a
80+CI check should be.
81+
82+Two things are unpinned here, because the terminal backend is not in a
83+jolt-native release yet: `libjolttui.so` comes out of a jolt-native checkout's
84+target directory (`JOLT_NATIVE=…`, or beside this tree), and `glimmer-tui` is
85+resolved from the same checkout. Both become pins like every other when it
86+ships.
87+
88+What a terminal has not got, frq does without: pictures, avatars, the lightbox
89+and calls draw nothing. And frq's spacing is written in points, for a window —
90+`scripts/tui.bb` hands the backend `:points-per-cell 8` so those numbers land
91+in cells, but `below-messages` in `src/frq/app.jolt` is point *arithmetic*
92+rather than a point *length*, and a scale cannot fix it: it reserves about
93+thirteen rows more than the compose bar needs, so the bottom of the backlog is
94+pushed out of the list.
95+
6396 ## Signing in
6497
6598 Three modes on the connect screen.
@@ -60,6 +60,39 @@ plain listener:
60 cargo run --release --bin freeq-server # in the freeq checkout60 cargo run --release --bin freeq-server # in the freeq checkout
61 ```61 ```
62 62
63+## In a terminal
64+
65+The screens are hiccup over glimmer's reconciler, and the reconciler does not
66+know what is under it — so the same tree paints into a terminal through
67+jolt-native's `libjolttui`, which exports libvidya's retained-tree ABI over a
68+grid of cells instead of a GPU window. `src/frq/tui.jolt` is the whole of what
69+that costs on this side: it requires `glimmer-tui.core` after `frq.app`, so the
70+backend installed last is the terminal, and seeds a `#tui` buffer so a session
71+started for a look at the layout has a conversation in it.
72+
73+```bash
74+just tui # the terminal, until Ctrl-Q
75+just tui --headless --cols=90 --rows=60 # one screenshot on stdout
76+```
77+
78+The headless one is `tui_headless` — the same layout and the same painting with
79+the writer taken off the end — which is what a screenshot in a bug report or a
80+CI check should be.
81+
82+Two things are unpinned here, because the terminal backend is not in a
83+jolt-native release yet: `libjolttui.so` comes out of a jolt-native checkout's
84+target directory (`JOLT_NATIVE=…`, or beside this tree), and `glimmer-tui` is
85+resolved from the same checkout. Both become pins like every other when it
86+ships.
87+
88+What a terminal has not got, frq does without: pictures, avatars, the lightbox
89+and calls draw nothing. And frq's spacing is written in points, for a window —
90+`scripts/tui.bb` hands the backend `:points-per-cell 8` so those numbers land
91+in cells, but `below-messages` in `src/frq/app.jolt` is point *arithmetic*
92+rather than a point *length*, and a scale cannot fix it: it reserves about
93+thirteen rows more than the compose bar needs, so the bottom of the backlog is
94+pushed out of the list.
95+
63 ## Signing in96 ## Signing in
64 97
65 Three modes on the connect screen.98 Three modes on the connect screen.
modified deps.edn +17 -2
@@ -34,6 +34,21 @@
3434 :darwin ["libjoltmoq.dylib"]
3535 :linux ["libjoltmoq.so"]}]
3636
37- :aliases {:frq {:main-opts ["-m" "frq.app"]}}
37+ :aliases {:frq {:main-opts ["-m" "frq.app"]}
3838
39- :tasks {frq "jolt -M:frq"}}
39+ ;; The same screens in a terminal. glimmer-tui is the other backend
40+ ;; for the same reconciler — libjolttui's tree ABI is libvidya's
41+ ;; with cells under it — so `frq.tui` requires it after `frq.app`
42+ ;; and renders the hiccup that is already written.
43+ ;;
44+ ;; A path rather than a sha because the backend is not in a
45+ ;; jolt-native release yet; it wants the checkout beside this one,
46+ ;; and `libjolttui.so` on the library path:
47+ ;;
48+ ;; just tui
49+ :tui {:extra-deps {nandi/glimmer-tui
50+ {:local/root "../jolt-native/jolt/glimmer-tui"}}
51+ :main-opts ["-m" "frq.tui"]}}
52+
53+ :tasks {frq "jolt -M:frq"
54+ tui "jolt -M:tui"}}
@@ -34,6 +34,21 @@
34 :darwin ["libjoltmoq.dylib"]34 :darwin ["libjoltmoq.dylib"]
35 :linux ["libjoltmoq.so"]}]35 :linux ["libjoltmoq.so"]}]
36 36
37- :aliases {:frq {:main-opts ["-m" "frq.app"]}}37+ :aliases {:frq {:main-opts ["-m" "frq.app"]}
38 38
39- :tasks {frq "jolt -M:frq"}}39+ ;; The same screens in a terminal. glimmer-tui is the other backend
40+ ;; for the same reconciler — libjolttui's tree ABI is libvidya's
41+ ;; with cells under it — so `frq.tui` requires it after `frq.app`
42+ ;; and renders the hiccup that is already written.
43+ ;;
44+ ;; A path rather than a sha because the backend is not in a
45+ ;; jolt-native release yet; it wants the checkout beside this one,
46+ ;; and `libjolttui.so` on the library path:
47+ ;;
48+ ;; just tui
49+ :tui {:extra-deps {nandi/glimmer-tui
50+ {:local/root "../jolt-native/jolt/glimmer-tui"}}
51+ :main-opts ["-m" "frq.tui"]}}
52+
53+ :tasks {frq "jolt -M:frq"
54+ tui "jolt -M:tui"}}
modified flake.lock +18 -0
@@ -124,6 +124,23 @@
124124 "url": "https://gitlab.com/nandithebull/jolt-native"
125125 }
126126 },
127+ "jolt-native-tui": {
128+ "flake": false,
129+ "locked": {
130+ "lastModified": 1788328565,
131+ "narHash": "sha256-wqfStc5zgi+kslskv/JnPAGZCaxGvZ8e/V2+6qpLF8I=",
132+ "ref": "refs/heads/main",
133+ "rev": "3cfee15a9d938584f526f606f853771eaad7f56c",
134+ "revCount": 32,
135+ "type": "git",
136+ "url": "https://gitlab.com/nandithebull/jolt-native"
137+ },
138+ "original": {
139+ "rev": "3cfee15a9d938584f526f606f853771eaad7f56c",
140+ "type": "git",
141+ "url": "https://gitlab.com/nandithebull/jolt-native"
142+ }
143+ },
127144 "jolt-src": {
128145 "flake": false,
129146 "locked": {
@@ -207,6 +224,7 @@
207224 "glimmer": "glimmer",
208225 "jolt-android-src": "jolt-android-src",
209226 "jolt-native": "jolt-native",
227+ "jolt-native-tui": "jolt-native-tui",
210228 "jolt-src": "jolt-src",
211229 "nix-appimage": "nix-appimage",
212230 "nixgl": "nixgl",
@@ -124,6 +124,23 @@
124 "url": "https://gitlab.com/nandithebull/jolt-native"124 "url": "https://gitlab.com/nandithebull/jolt-native"
125 }125 }
126 },126 },
127+ "jolt-native-tui": {
128+ "flake": false,
129+ "locked": {
130+ "lastModified": 1788328565,
131+ "narHash": "sha256-wqfStc5zgi+kslskv/JnPAGZCaxGvZ8e/V2+6qpLF8I=",
132+ "ref": "refs/heads/main",
133+ "rev": "3cfee15a9d938584f526f606f853771eaad7f56c",
134+ "revCount": 32,
135+ "type": "git",
136+ "url": "https://gitlab.com/nandithebull/jolt-native"
137+ },
138+ "original": {
139+ "rev": "3cfee15a9d938584f526f606f853771eaad7f56c",
140+ "type": "git",
141+ "url": "https://gitlab.com/nandithebull/jolt-native"
142+ }
143+ },
127 "jolt-src": {144 "jolt-src": {
128 "flake": false,145 "flake": false,
129 "locked": {146 "locked": {
@@ -207,6 +224,7 @@
207 "glimmer": "glimmer",224 "glimmer": "glimmer",
208 "jolt-android-src": "jolt-android-src",225 "jolt-android-src": "jolt-android-src",
209 "jolt-native": "jolt-native",226 "jolt-native": "jolt-native",
227+ "jolt-native-tui": "jolt-native-tui",
210 "jolt-src": "jolt-src",228 "jolt-src": "jolt-src",
211 "nix-appimage": "nix-appimage",229 "nix-appimage": "nix-appimage",
212 "nixgl": "nixgl",230 "nixgl": "nixgl",
modified flake.nix +81 -1
@@ -41,6 +41,17 @@
4141 flake = false;
4242 };
4343
44+ # The terminal backend, which v0.1.3 has not got: crates/jolt-tui (the tree
45+ # ABI over a grid of cells) and jolt/glimmer-tui (the jolt side that binds
46+ # it). Its own input rather than a bump of the one above, deliberately —
47+ # the window half stays pinned to the release the rest of the tree names,
48+ # and only `tui` evaluates this. When the backend ships in a release the
49+ # two become one pin again.
50+ jolt-native-tui = {
51+ url = "git+https://gitlab.com/nandithebull/jolt-native?rev=3cfee15a9d938584f526f606f853771eaad7f56c";
52+ flake = false;
53+ };
54+
4455 # Chez itself, because the APK needs a cross target nixpkgs does not
4556 # build: frq's Scheme is compiled to an arm64 boot image, and that wants
4657 # Chez's own `tarm64le` workarea — boot files, xpatch and libkernel.a.
@@ -81,7 +92,7 @@
8192 };
8293 };
8394
84- outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, chez-src, jolt-android-src, nixgl, nix-appimage }:
95+ outputs = { self, nixpkgs, jolt-src, jolt-native, jolt-native-tui, glimmer, chez-src, jolt-android-src, nixgl, nix-appimage }:
8596 let
8697 systems = [ "x86_64-linux" "aarch64-linux" ];
8798 forEachSystem = f:
@@ -148,6 +159,44 @@
148159 '';
149160 };
150161
162+ # libjolttui alone, out of the tui input's workspace. One crate
163+ # rather than the whole of it: the terminal backend's dependencies
164+ # are crossterm and a width table, where libvidya's and libjoltmoq's
165+ # are egui, openh264 and v4l none of which a terminal needs, and
166+ # all of which this would otherwise build a second time at a second
167+ # rev.
168+ nativeTui = pkgs.rustPlatform.buildRustPackage {
169+ pname = "jolt-tui";
170+ version = "0.1.0";
171+ src = jolt-native-tui;
172+
173+ cargoLock = {
174+ lockFile = "${jolt-native-tui}/Cargo.lock";
175+ allowBuiltinFetchGit = true;
176+ };
177+
178+ nativeBuildInputs = with pkgs; [ pkg-config cmake rustPlatform.bindgenHook ];
179+ buildInputs = with pkgs; [ alsa-lib pipewire openssl libxkbcommon wayland libGL ];
180+
181+ cargoBuildFlags = [ "-p" "jolt-tui" ];
182+
183+ # As above: upstream's .cargo/config.toml drives the build through
184+ # DotSlash, which a sandbox has no network for.
185+ postPatch = ''
186+ rm -f .cargo/config.toml
187+ '';
188+
189+ V4L2R_VIDEODEV2_H_PATH = "${pkgs.linuxHeaders}/include";
190+ doCheck = false;
191+
192+ installPhase = ''
193+ runHook preInstall
194+ mkdir -p "$out/lib"
195+ install -m644 target/*/release/libjolttui.so "$out/lib/"
196+ runHook postInstall
197+ '';
198+ };
199+
151200 # Jolt itself: Clojure on Chez, built the way its own flake builds it.
152201 # A function, because there are two of them upstream for the
153202 # desktop, and the Bionic-addrinfo fork for the boot image the APK
@@ -201,6 +250,7 @@
201250 # deps.edn asks for glimmer by git the top-level override below
202251 # answers for both.
203252 glimmerVidya = "${jolt-native}/jolt/glimmer-vidya";
253+ glimmerTui = "${jolt-native-tui}/jolt/glimmer-tui";
204254
205255 # egui reaches for these with dlopen rather than linking them, so
206256 # being in the cdylib's buildInputs is not enough the launcher has
@@ -243,6 +293,31 @@
243293 -M:frq "$@"
244294 '';
245295
296+ # The same source, the other backend. No GL, no nixGL and no X11
297+ # a terminal is the one surface that needs nothing from the host but
298+ # a terminal, which is the reason this output exists.
299+ tuiScript = pkgs.writeShellScript "frq-tui" ''
300+ export LD_LIBRARY_PATH="${nativeTui}/lib:${native}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
301+ cd ${frqSource}
302+
303+ exec ${joltRuntime}/bin/jolt \
304+ -Sdeps '{:deps {jolt-lang/glimmer {:local/root "${glimmer}"} nandi/glimmer-vidya {:local/root "${glimmerVidya}"} nandi/glimmer-tui {:local/root "${glimmerTui}"}}}' \
305+ -m frq.tui "$@"
306+ '';
307+
308+ tui = pkgs.runCommand "frq-tui-0.1.0"
309+ {
310+ meta = {
311+ description = "frq's screens in a terminal";
312+ mainProgram = "frq-tui";
313+ platforms = systems;
314+ };
315+ }
316+ ''
317+ mkdir -p "$out/bin"
318+ ln -s ${tuiScript} "$out/bin/frq-tui"
319+ '';
320+
246321 frq = pkgs.runCommand "frq-0.1.0"
247322 {
248323 meta = {
@@ -286,6 +361,7 @@
286361 in
287362 {
288363 inherit native frq;
364+ inherit nativeTui tui;
289365 jolt = joltRuntime;
290366 default = frq;
291367
@@ -309,6 +385,10 @@
309385 type = "app";
310386 program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.frq}/bin/frq";
311387 };
388+ tui = {
389+ type = "app";
390+ program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.tui}/bin/frq-tui";
391+ };
312392 });
313393 };
314394 }
@@ -41,6 +41,17 @@
41 flake = false;41 flake = false;
42 };42 };
43 43
44+ # The terminal backend, which v0.1.3 has not got: crates/jolt-tui (the tree
45+ # ABI over a grid of cells) and jolt/glimmer-tui (the jolt side that binds
46+ # it). Its own input rather than a bump of the one above, deliberately —
47+ # the window half stays pinned to the release the rest of the tree names,
48+ # and only `tui` evaluates this. When the backend ships in a release the
49+ # two become one pin again.
50+ jolt-native-tui = {
51+ url = "git+https://gitlab.com/nandithebull/jolt-native?rev=3cfee15a9d938584f526f606f853771eaad7f56c";
52+ flake = false;
53+ };
54+
44 # Chez itself, because the APK needs a cross target nixpkgs does not55 # Chez itself, because the APK needs a cross target nixpkgs does not
45 # build: frq's Scheme is compiled to an arm64 boot image, and that wants56 # build: frq's Scheme is compiled to an arm64 boot image, and that wants
46 # Chez's own `tarm64le` workarea — boot files, xpatch and libkernel.a.57 # Chez's own `tarm64le` workarea — boot files, xpatch and libkernel.a.
@@ -81,7 +92,7 @@
81 };92 };
82 };93 };
83 94
84- outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, chez-src, jolt-android-src, nixgl, nix-appimage }:95+ outputs = { self, nixpkgs, jolt-src, jolt-native, jolt-native-tui, glimmer, chez-src, jolt-android-src, nixgl, nix-appimage }:
85 let96 let
86 systems = [ "x86_64-linux" "aarch64-linux" ];97 systems = [ "x86_64-linux" "aarch64-linux" ];
87 forEachSystem = f:98 forEachSystem = f:
@@ -148,6 +159,44 @@
148 '';159 '';
149 };160 };
150 161
162+ # libjolttui alone, out of the tui input's workspace. One crate
163+ # rather than the whole of it: the terminal backend's dependencies
164+ # are crossterm and a width table, where libvidya's and libjoltmoq's
165+ # are egui, openh264 and v4l none of which a terminal needs, and
166+ # all of which this would otherwise build a second time at a second
167+ # rev.
168+ nativeTui = pkgs.rustPlatform.buildRustPackage {
169+ pname = "jolt-tui";
170+ version = "0.1.0";
171+ src = jolt-native-tui;
172+
173+ cargoLock = {
174+ lockFile = "${jolt-native-tui}/Cargo.lock";
175+ allowBuiltinFetchGit = true;
176+ };
177+
178+ nativeBuildInputs = with pkgs; [ pkg-config cmake rustPlatform.bindgenHook ];
179+ buildInputs = with pkgs; [ alsa-lib pipewire openssl libxkbcommon wayland libGL ];
180+
181+ cargoBuildFlags = [ "-p" "jolt-tui" ];
182+
183+ # As above: upstream's .cargo/config.toml drives the build through
184+ # DotSlash, which a sandbox has no network for.
185+ postPatch = ''
186+ rm -f .cargo/config.toml
187+ '';
188+
189+ V4L2R_VIDEODEV2_H_PATH = "${pkgs.linuxHeaders}/include";
190+ doCheck = false;
191+
192+ installPhase = ''
193+ runHook preInstall
194+ mkdir -p "$out/lib"
195+ install -m644 target/*/release/libjolttui.so "$out/lib/"
196+ runHook postInstall
197+ '';
198+ };
199+
151 # Jolt itself: Clojure on Chez, built the way its own flake builds it.200 # Jolt itself: Clojure on Chez, built the way its own flake builds it.
152 # A function, because there are two of them upstream for the201 # A function, because there are two of them upstream for the
153 # desktop, and the Bionic-addrinfo fork for the boot image the APK202 # desktop, and the Bionic-addrinfo fork for the boot image the APK
@@ -201,6 +250,7 @@
201 # deps.edn asks for glimmer by git the top-level override below250 # deps.edn asks for glimmer by git the top-level override below
202 # answers for both.251 # answers for both.
203 glimmerVidya = "${jolt-native}/jolt/glimmer-vidya";252 glimmerVidya = "${jolt-native}/jolt/glimmer-vidya";
253+ glimmerTui = "${jolt-native-tui}/jolt/glimmer-tui";
204 254
205 # egui reaches for these with dlopen rather than linking them, so255 # egui reaches for these with dlopen rather than linking them, so
206 # being in the cdylib's buildInputs is not enough the launcher has256 # being in the cdylib's buildInputs is not enough the launcher has
@@ -243,6 +293,31 @@
243 -M:frq "$@"293 -M:frq "$@"
244 '';294 '';
245 295
296+ # The same source, the other backend. No GL, no nixGL and no X11
297+ # a terminal is the one surface that needs nothing from the host but
298+ # a terminal, which is the reason this output exists.
299+ tuiScript = pkgs.writeShellScript "frq-tui" ''
300+ export LD_LIBRARY_PATH="${nativeTui}/lib:${native}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
301+ cd ${frqSource}
302+
303+ exec ${joltRuntime}/bin/jolt \
304+ -Sdeps '{:deps {jolt-lang/glimmer {:local/root "${glimmer}"} nandi/glimmer-vidya {:local/root "${glimmerVidya}"} nandi/glimmer-tui {:local/root "${glimmerTui}"}}}' \
305+ -m frq.tui "$@"
306+ '';
307+
308+ tui = pkgs.runCommand "frq-tui-0.1.0"
309+ {
310+ meta = {
311+ description = "frq's screens in a terminal";
312+ mainProgram = "frq-tui";
313+ platforms = systems;
314+ };
315+ }
316+ ''
317+ mkdir -p "$out/bin"
318+ ln -s ${tuiScript} "$out/bin/frq-tui"
319+ '';
320+
246 frq = pkgs.runCommand "frq-0.1.0"321 frq = pkgs.runCommand "frq-0.1.0"
247 {322 {
248 meta = {323 meta = {
@@ -286,6 +361,7 @@
286 in361 in
287 {362 {
288 inherit native frq;363 inherit native frq;
364+ inherit nativeTui tui;
289 jolt = joltRuntime;365 jolt = joltRuntime;
290 default = frq;366 default = frq;
291 367
@@ -309,6 +385,10 @@
309 type = "app";385 type = "app";
310 program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.frq}/bin/frq";386 program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.frq}/bin/frq";
311 };387 };
388+ tui = {
389+ type = "app";
390+ program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.tui}/bin/frq-tui";
391+ };
312 });392 });
313 };393 };
314 }394 }
modified justfile +4 -0
@@ -31,3 +31,7 @@ sync-dist:
3131 # The app.
3232 run *args:
3333 scripts/run.bb {{args}}
34+
35+# The same screens in a terminal. `just tui --headless` prints one screenshot.
36+tui *args:
37+ scripts/tui.bb {{args}}
@@ -31,3 +31,7 @@ sync-dist:
31 # The app.31 # The app.
32 run *args:32 run *args:
33 scripts/run.bb {{args}}33 scripts/run.bb {{args}}
34+
35+# The same screens in a terminal. `just tui --headless` prints one screenshot.
36+tui *args:
37+ scripts/tui.bb {{args}}
added scripts/tui.bb +95 -0
new file mode 100755
@@ -0,0 +1,95 @@
1+#!/bin/sh
2+#_(
3+exec "$(dirname "$0")/bb" "$0" "$@"
4+)
5+
6+;; frq's screens in a terminal, from this working tree.
7+;;
8+;; tui.bb [--headless] [--cols=N] [--rows=N]
9+;;
10+;; run.bb with the other backend under it. Two things differ, and both are
11+;; because the terminal backend is not in a jolt-native release yet:
12+;;
13+;; * `libjolttui.so` comes out of a jolt-native checkout's target directory
14+;; rather than out of a pinned archive, and is linked into build/lib beside
15+;; the two that are pinned. Point JOLT_NATIVE at that checkout, or leave it
16+;; beside this one.
17+;; * `glimmer-tui` — the jolt half of that backend, and the namespace
18+;; `frq.tui` requires — is resolved from the same checkout with -Sdeps,
19+;; which is what lets this work from a worktree, where the relative path in
20+;; deps.edn's `:tui` alias does not point where it does from a clone. The
21+;; alias is not used here for that reason: its coordinate for the same
22+;; library wins over the one -Sdeps merges in, so this passes `-m` itself
23+;; and leaves `jolt -M:tui` to the checkouts the alias is right for.
24+;;
25+;; When the backend ships in a release, both of those become pins like every
26+;; other, and this script becomes run.bb with a different alias.
27+(require '[babashka.fs :as fs]
28+ '[babashka.process :as p])
29+
30+(def root (str (fs/canonicalize (fs/path (fs/parent *file*) ".."))))
31+(def lib (fs/path root "build" "lib"))
32+
33+(def jolt
34+ (let [pin (fs/path root "scripts" "jolt")]
35+ (if (and (fs/exists? pin) (fs/which "dotslash")) (str pin) "jolt")))
36+
37+(defn die [& msg]
38+ (binding [*out* *err*] (println (apply str msg)))
39+ (System/exit 1))
40+
41+;; The checkout the terminal backend lives in. A worktree of it counts — what
42+;; is wanted is a directory holding `jolt/glimmer-tui` and a built libjolttui.
43+(def jolt-native
44+ (let [given (System/getenv "JOLT_NATIVE")
45+ guess (fs/path root ".." "jolt-native")
46+ dir (cond
47+ given (fs/path given)
48+ (fs/exists? guess) guess
49+ :else nil)]
50+ (when-not dir
51+ (die "no jolt-native checkout: set JOLT_NATIVE, or put one beside this tree"))
52+ (str (fs/canonicalize dir))))
53+
54+(def glimmer-tui (fs/path jolt-native "jolt" "glimmer-tui"))
55+
56+(when-not (fs/exists? glimmer-tui)
57+ (die glimmer-tui " is not there — this wants the branch that carries the "
58+ "terminal backend (crates/jolt-tui and jolt/glimmer-tui)"))
59+
60+;; cargo's release build first, then its debug one, then what buck2 left: any
61+;; of the three is the library, and which one a person has is their business.
62+(def jolttui
63+ (or (first (for [c ["target/release/libjolttui.so"
64+ "target/debug/libjolttui.so"]
65+ :let [p (fs/path jolt-native c)]
66+ :when (fs/exists? p)]
67+ p))
68+ (first (sort-by (comp - fs/file-time->millis fs/last-modified-time)
69+ (fs/glob (fs/path jolt-native "buck-out")
70+ "**/libjolttui.so")))
71+ (die "no libjolttui.so under " jolt-native
72+ " — build it: cargo build --release -p jolt-tui")))
73+
74+;; The two pinned libraries, into build/lib.
75+(p/shell (str (fs/path root "scripts" "lib.bb")))
76+
77+;; And the unpinned third, linked rather than copied for the same reason the
78+;; others are: a rebuild reaches a running tree without anything here going
79+;; stale.
80+(let [dest (fs/path lib "libjolttui.so")]
81+ (fs/create-dirs lib)
82+ (fs/delete-if-exists dest)
83+ (fs/create-sym-link dest (str (fs/canonicalize jolttui))))
84+
85+(System/exit
86+ (:exit @(apply p/process
87+ {:inherit true
88+ :dir root
89+ :extra-env {"LD_LIBRARY_PATH"
90+ (str lib (when-let [p (System/getenv "LD_LIBRARY_PATH")]
91+ (str ":" p)))}}
92+ jolt
93+ "-Sdeps" (str "{:deps {nandi/glimmer-tui {:local/root \""
94+ glimmer-tui "\"}}}")
95+ "-m" "frq.tui" *command-line-args*)))
new file mode 100755
@@ -0,0 +1,95 @@
1+#!/bin/sh
2+#_(
3+exec "$(dirname "$0")/bb" "$0" "$@"
4+)
5+
6+;; frq's screens in a terminal, from this working tree.
7+;;
8+;; tui.bb [--headless] [--cols=N] [--rows=N]
9+;;
10+;; run.bb with the other backend under it. Two things differ, and both are
11+;; because the terminal backend is not in a jolt-native release yet:
12+;;
13+;; * `libjolttui.so` comes out of a jolt-native checkout's target directory
14+;; rather than out of a pinned archive, and is linked into build/lib beside
15+;; the two that are pinned. Point JOLT_NATIVE at that checkout, or leave it
16+;; beside this one.
17+;; * `glimmer-tui` — the jolt half of that backend, and the namespace
18+;; `frq.tui` requires — is resolved from the same checkout with -Sdeps,
19+;; which is what lets this work from a worktree, where the relative path in
20+;; deps.edn's `:tui` alias does not point where it does from a clone. The
21+;; alias is not used here for that reason: its coordinate for the same
22+;; library wins over the one -Sdeps merges in, so this passes `-m` itself
23+;; and leaves `jolt -M:tui` to the checkouts the alias is right for.
24+;;
25+;; When the backend ships in a release, both of those become pins like every
26+;; other, and this script becomes run.bb with a different alias.
27+(require '[babashka.fs :as fs]
28+ '[babashka.process :as p])
29+
30+(def root (str (fs/canonicalize (fs/path (fs/parent *file*) ".."))))
31+(def lib (fs/path root "build" "lib"))
32+
33+(def jolt
34+ (let [pin (fs/path root "scripts" "jolt")]
35+ (if (and (fs/exists? pin) (fs/which "dotslash")) (str pin) "jolt")))
36+
37+(defn die [& msg]
38+ (binding [*out* *err*] (println (apply str msg)))
39+ (System/exit 1))
40+
41+;; The checkout the terminal backend lives in. A worktree of it counts — what
42+;; is wanted is a directory holding `jolt/glimmer-tui` and a built libjolttui.
43+(def jolt-native
44+ (let [given (System/getenv "JOLT_NATIVE")
45+ guess (fs/path root ".." "jolt-native")
46+ dir (cond
47+ given (fs/path given)
48+ (fs/exists? guess) guess
49+ :else nil)]
50+ (when-not dir
51+ (die "no jolt-native checkout: set JOLT_NATIVE, or put one beside this tree"))
52+ (str (fs/canonicalize dir))))
53+
54+(def glimmer-tui (fs/path jolt-native "jolt" "glimmer-tui"))
55+
56+(when-not (fs/exists? glimmer-tui)
57+ (die glimmer-tui " is not there — this wants the branch that carries the "
58+ "terminal backend (crates/jolt-tui and jolt/glimmer-tui)"))
59+
60+;; cargo's release build first, then its debug one, then what buck2 left: any
61+;; of the three is the library, and which one a person has is their business.
62+(def jolttui
63+ (or (first (for [c ["target/release/libjolttui.so"
64+ "target/debug/libjolttui.so"]
65+ :let [p (fs/path jolt-native c)]
66+ :when (fs/exists? p)]
67+ p))
68+ (first (sort-by (comp - fs/file-time->millis fs/last-modified-time)
69+ (fs/glob (fs/path jolt-native "buck-out")
70+ "**/libjolttui.so")))
71+ (die "no libjolttui.so under " jolt-native
72+ " — build it: cargo build --release -p jolt-tui")))
73+
74+;; The two pinned libraries, into build/lib.
75+(p/shell (str (fs/path root "scripts" "lib.bb")))
76+
77+;; And the unpinned third, linked rather than copied for the same reason the
78+;; others are: a rebuild reaches a running tree without anything here going
79+;; stale.
80+(let [dest (fs/path lib "libjolttui.so")]
81+ (fs/create-dirs lib)
82+ (fs/delete-if-exists dest)
83+ (fs/create-sym-link dest (str (fs/canonicalize jolttui))))
84+
85+(System/exit
86+ (:exit @(apply p/process
87+ {:inherit true
88+ :dir root
89+ :extra-env {"LD_LIBRARY_PATH"
90+ (str lib (when-let [p (System/getenv "LD_LIBRARY_PATH")]
91+ (str ":" p)))}}
92+ jolt
93+ "-Sdeps" (str "{:deps {nandi/glimmer-tui {:local/root \""
94+ glimmer-tui "\"}}}")
95+ "-m" "frq.tui" *command-line-args*)))
added src/frq/tui.jolt +123 -0
new file mode 100644
@@ -0,0 +1,123 @@
1+(ns frq.tui
2+ "frq's own screens, painted into a terminal.
3+
4+ The components in `frq.app` are hiccup over glimmer's reconciler, and the
5+ reconciler does not know what is under it so the same tree that egui paints
6+ as a window goes through `libjolttui` as cells instead. Nothing in `frq.app`
7+ changes; this namespace only picks the other backend and gives it a screen.
8+
9+ Requiring order is the whole trick. `frq.app` pulls in `glimmer-vidya.core`,
10+ which installs itself on load; `glimmer-tui.core` is required after it and
11+ installs itself in turn, so the backend glimmer renders with is the terminal.
12+ The Vidya library is still loaded and frq still calls into it for a window
13+ title, a window width, a picture chooser and with no window open every one
14+ of those is inert, which is the behaviour those calls already have to have.
15+
16+ Two ways to run it:
17+
18+ jolt -M:tui the real terminal, until Ctrl-Q
19+ jolt -M:tui --headless one screenshot on stdout, no terminal at all
20+
21+ The headless one is `tui_headless` the same layout and the same painting
22+ with the writer taken off the end and is what a screenshot in a bug report
23+ or a CI check should be."
24+ (:require [clojure.string :as str]
25+ [frq.app :as app]
26+ [frq.state :as s]
27+ [glimmer.core :as ui]
28+ ;; last, so its install! is the one that stands
29+ [glimmer-tui.core :as tui]))
30+
31+(def ^:private demo-channel "#tui")
32+
33+;; frq's spacing is written in points, because it was written for a window:
34+;; `:margin 12`, `:width-request 260`, a sidebar of 320. A cell is worth about
35+;; eight of those across, so that is the divisor the backend is handed and
36+;; the same one `narrow!` multiplies back up, so `wide?` is asked its question
37+;; in the units it was written against.
38+(def ^:private points-per-cell 8)
39+
40+(defn- seed!
41+ "Put a conversation on screen for a client that is not connected to anything.
42+
43+ A terminal session started for a look at the layout has no server behind it,
44+ and an empty buffer says nothing about how the buffer looks. These go in
45+ through `push-message!` rather than into the atom directly, so what is drawn
46+ is a real buffer timestamps, day headings, the actor lookup, reply chips
47+ and reactions all included."
48+ []
49+ (let [now (System/currentTimeMillis)
50+ minute 60000]
51+ (s/push-message! demo-channel "*" (str "Now talking in " demo-channel)
52+ {:at (- now (* 32 minute))})
53+ (s/push-message! demo-channel "nandi" "the tree ABI is the same one libvidya exports"
54+ {:at (- now (* 12 minute)) :id "m1"})
55+ (s/push-message! demo-channel "vidya" "so the jolt side picks a window or a terminal and changes nothing else"
56+ {:at (- now (* 11 minute)) :id "m2" :reply-to "m1"
57+ :reactions {"👍" ["nandi"]}})
58+ (s/push-message! demo-channel "nandi" "one reconciler, two shared objects"
59+ {:at (- now (* 2 minute)) :id "m3"}))
60+ (swap! s/channels assoc-in [demo-channel :joined?] true)
61+ (swap! s/channels assoc-in [demo-channel :users]
62+ {"nandi" "@" "vidya" "" "you" ""})
63+ (reset! s/form-nick "you")
64+ (reset! s/status (str "Not connected — " demo-channel " is a demo buffer"))
65+ (reset! s/current demo-channel)
66+ (reset! s/screen :chat)
67+ nil)
68+
69+(defn- narrow!
70+ "Tell the layout how much room it has, in the units it expects.
71+
72+ `frq.state`'s `wide?` is written against a window's width in points, and the
73+ terminal's is in cells — two orders of magnitude smaller, so left alone every
74+ session would take the narrow layout by accident rather than on purpose. A
75+ cell is about eight points wide and a row about sixteen tall, which is close
76+ enough for the one question anything here asks of these numbers."
77+ []
78+ (let [[cols rows] (tui/screen-size)]
79+ (reset! s/window-width (* cols points-per-cell))
80+ ;; A row is worth about twice a column: a cell is taller than it is wide,
81+ ;; and the heights in frq a picture's, the space kept for the compose bar
82+ ;; are measured against the window's.
83+ (reset! s/window-height (* rows points-per-cell 2)))
84+ nil)
85+
86+(defn- run-headless!
87+ "Mount the app in a session of `cols` by `rows` with no terminal, let it
88+ settle, and print what was painted."
89+ [cols rows]
90+ ;; The size has to be known before the first render, or the layout reads the
91+ ;; zero it was left at. A timer runs on the loop thread, which is where
92+ ;; screen-size may be asked and the first one fires before the first paint.
93+ (tui/after! 0 narrow!)
94+ (tui/after! 120 (fn []
95+ (println (tui/screen-str))
96+ (tui/quit!)))
97+ (ui/run app/app :headless [cols rows] :points-per-cell points-per-cell))
98+
99+(defn- run-terminal! []
100+ (tui/after! 0 narrow!)
101+ ;; The terminal is resized by dragging its edge, and only a poll notices:
102+ ;; the backend reports a size, and only what a component derefs re-renders.
103+ (tui/every! 200 narrow!)
104+ (ui/run app/app :mouse true :points-per-cell points-per-cell))
105+
106+(defn -main [& args]
107+ (let [args (set args)
108+ headless? (contains? args "--headless")
109+ cols (if-let [n (first (keep #(when (str/starts-with? % "--cols=")
110+ (subs % 7))
111+ args))]
112+ (Integer/parseInt n)
113+ 100)
114+ rows (if-let [n (first (keep #(when (str/starts-with? % "--rows=")
115+ (subs % 7))
116+ args))]
117+ (Integer/parseInt n)
118+ 36)]
119+ (s/restore-prefs!)
120+ (seed!)
121+ (if headless?
122+ (run-headless! cols rows)
123+ (run-terminal!))))
new file mode 100644
@@ -0,0 +1,123 @@
1+(ns frq.tui
2+ "frq's own screens, painted into a terminal.
3+
4+ The components in `frq.app` are hiccup over glimmer's reconciler, and the
5+ reconciler does not know what is under it so the same tree that egui paints
6+ as a window goes through `libjolttui` as cells instead. Nothing in `frq.app`
7+ changes; this namespace only picks the other backend and gives it a screen.
8+
9+ Requiring order is the whole trick. `frq.app` pulls in `glimmer-vidya.core`,
10+ which installs itself on load; `glimmer-tui.core` is required after it and
11+ installs itself in turn, so the backend glimmer renders with is the terminal.
12+ The Vidya library is still loaded and frq still calls into it for a window
13+ title, a window width, a picture chooser and with no window open every one
14+ of those is inert, which is the behaviour those calls already have to have.
15+
16+ Two ways to run it:
17+
18+ jolt -M:tui the real terminal, until Ctrl-Q
19+ jolt -M:tui --headless one screenshot on stdout, no terminal at all
20+
21+ The headless one is `tui_headless` the same layout and the same painting
22+ with the writer taken off the end and is what a screenshot in a bug report
23+ or a CI check should be."
24+ (:require [clojure.string :as str]
25+ [frq.app :as app]
26+ [frq.state :as s]
27+ [glimmer.core :as ui]
28+ ;; last, so its install! is the one that stands
29+ [glimmer-tui.core :as tui]))
30+
31+(def ^:private demo-channel "#tui")
32+
33+;; frq's spacing is written in points, because it was written for a window:
34+;; `:margin 12`, `:width-request 260`, a sidebar of 320. A cell is worth about
35+;; eight of those across, so that is the divisor the backend is handed and
36+;; the same one `narrow!` multiplies back up, so `wide?` is asked its question
37+;; in the units it was written against.
38+(def ^:private points-per-cell 8)
39+
40+(defn- seed!
41+ "Put a conversation on screen for a client that is not connected to anything.
42+
43+ A terminal session started for a look at the layout has no server behind it,
44+ and an empty buffer says nothing about how the buffer looks. These go in
45+ through `push-message!` rather than into the atom directly, so what is drawn
46+ is a real buffer timestamps, day headings, the actor lookup, reply chips
47+ and reactions all included."
48+ []
49+ (let [now (System/currentTimeMillis)
50+ minute 60000]
51+ (s/push-message! demo-channel "*" (str "Now talking in " demo-channel)
52+ {:at (- now (* 32 minute))})
53+ (s/push-message! demo-channel "nandi" "the tree ABI is the same one libvidya exports"
54+ {:at (- now (* 12 minute)) :id "m1"})
55+ (s/push-message! demo-channel "vidya" "so the jolt side picks a window or a terminal and changes nothing else"
56+ {:at (- now (* 11 minute)) :id "m2" :reply-to "m1"
57+ :reactions {"👍" ["nandi"]}})
58+ (s/push-message! demo-channel "nandi" "one reconciler, two shared objects"
59+ {:at (- now (* 2 minute)) :id "m3"}))
60+ (swap! s/channels assoc-in [demo-channel :joined?] true)
61+ (swap! s/channels assoc-in [demo-channel :users]
62+ {"nandi" "@" "vidya" "" "you" ""})
63+ (reset! s/form-nick "you")
64+ (reset! s/status (str "Not connected — " demo-channel " is a demo buffer"))
65+ (reset! s/current demo-channel)
66+ (reset! s/screen :chat)
67+ nil)
68+
69+(defn- narrow!
70+ "Tell the layout how much room it has, in the units it expects.
71+
72+ `frq.state`'s `wide?` is written against a window's width in points, and the
73+ terminal's is in cells — two orders of magnitude smaller, so left alone every
74+ session would take the narrow layout by accident rather than on purpose. A
75+ cell is about eight points wide and a row about sixteen tall, which is close
76+ enough for the one question anything here asks of these numbers."
77+ []
78+ (let [[cols rows] (tui/screen-size)]
79+ (reset! s/window-width (* cols points-per-cell))
80+ ;; A row is worth about twice a column: a cell is taller than it is wide,
81+ ;; and the heights in frq a picture's, the space kept for the compose bar
82+ ;; are measured against the window's.
83+ (reset! s/window-height (* rows points-per-cell 2)))
84+ nil)
85+
86+(defn- run-headless!
87+ "Mount the app in a session of `cols` by `rows` with no terminal, let it
88+ settle, and print what was painted."
89+ [cols rows]
90+ ;; The size has to be known before the first render, or the layout reads the
91+ ;; zero it was left at. A timer runs on the loop thread, which is where
92+ ;; screen-size may be asked and the first one fires before the first paint.
93+ (tui/after! 0 narrow!)
94+ (tui/after! 120 (fn []
95+ (println (tui/screen-str))
96+ (tui/quit!)))
97+ (ui/run app/app :headless [cols rows] :points-per-cell points-per-cell))
98+
99+(defn- run-terminal! []
100+ (tui/after! 0 narrow!)
101+ ;; The terminal is resized by dragging its edge, and only a poll notices:
102+ ;; the backend reports a size, and only what a component derefs re-renders.
103+ (tui/every! 200 narrow!)
104+ (ui/run app/app :mouse true :points-per-cell points-per-cell))
105+
106+(defn -main [& args]
107+ (let [args (set args)
108+ headless? (contains? args "--headless")
109+ cols (if-let [n (first (keep #(when (str/starts-with? % "--cols=")
110+ (subs % 7))
111+ args))]
112+ (Integer/parseInt n)
113+ 100)
114+ rows (if-let [n (first (keep #(when (str/starts-with? % "--rows=")
115+ (subs % 7))
116+ args))]
117+ (Integer/parseInt n)
118+ 36)]
119+ (s/restore-prefs!)
120+ (seed!)
121+ (if headless?
122+ (run-headless! cols rows)
123+ (run-terminal!))))