nandi/frqpublic Fork 0
8a86eb3
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.

Fit a conversation on the screen, not two messages of one

Two things were spending the screen on air. The backend measured every gap in
columns, and a cell is half as wide as it is tall — so frq's 8-point spacing,
half a row, was rounded up to a whole one at every level of a tree whose chat
column has ten children, most of them empty wrappers. That is fixed where it
belongs, in the backend, and the pin moves with it.

The other half is here: the reserves under the two scrolling lists are counted
in rows of chrome rather than in lengths, and a window's row is 34 points where
a terminal's is one cell. So they are written against `chrome-row`, which the
window leaves at 34 — the numbers below it are unchanged — and frq.tui sets to
a row of its own.

Thirteen messages now, where there were two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-01T23:55:36-07:00 Browse files
8a86eb3 parent: 5c7976e
modified README.md +13 -6
@@ -77,6 +77,7 @@ nix run .#tui # or: just tui
7777 just tui --headless --cols=90 --rows=60 # one screenshot on stdout
7878 just tui --headless --demo # a buffer of its own, no server
7979 just tui --headless --wait=9000 # long enough to have connected
80+just tui --headless --dump # and the tree the library holds
8081 ```
8182
8283 The headless one is `tui_headless` — the same layout and the same painting with
@@ -91,12 +92,18 @@ Send them somewhere: `nix run .#tui 2>/tmp/frq.log`.
9192
9293 What a terminal has not got, frq does without: pictures, avatars and the
9394 lightbox draw nothing, and calls are off — the media plane paints frames into
94-a texture, and there is no texture here. And frq's spacing is written in
95-points, for a window — the backend is handed `:points-per-cell 8` so those
96-numbers land in cells, but `below-messages` in `src/frq/app.jolt` is point
97-*arithmetic* rather than a point *length*, and a scale cannot fix it: it
98-reserves about thirteen rows more than the compose bar needs, so the bottom of
99-the backlog is pushed out of the list.
95+a texture, and there is no texture here.
96+
97+The spacing is written in points, for a window, and a cell is about eight of
98+them across and sixteen down — so the backend is handed both numbers and each
99+prop is divided by the axis it measures. A gap of half a cell rounds to
100+nothing, which is what `:spacing 8` against a 16-point row is: thirteen
101+messages fit where rounding it up left room for two.
102+
103+The two reserves in `src/frq/app.jolt` are the one thing a scale cannot
104+answer, because they are counted in rows of chrome rather than in lengths: a
105+window's row is 34 points and a terminal's is one cell. `chrome-row` is where
106+that is said, and `frq.tui` sets it.
100107
101108 Two things are unpinned, because the terminal backend is not in a jolt-native
102109 release yet: `libjolttui.so` comes out of a jolt-native checkout's target
@@ -77,6 +77,7 @@ nix run .#tui # or: just tui
77 just tui --headless --cols=90 --rows=60 # one screenshot on stdout77 just tui --headless --cols=90 --rows=60 # one screenshot on stdout
78 just tui --headless --demo # a buffer of its own, no server78 just tui --headless --demo # a buffer of its own, no server
79 just tui --headless --wait=9000 # long enough to have connected79 just tui --headless --wait=9000 # long enough to have connected
80+just tui --headless --dump # and the tree the library holds
80 ```81 ```
81 82
82 The headless one is `tui_headless` — the same layout and the same painting with83 The headless one is `tui_headless` — the same layout and the same painting with
@@ -91,12 +92,18 @@ Send them somewhere: `nix run .#tui 2>/tmp/frq.log`.
91 92
92 What a terminal has not got, frq does without: pictures, avatars and the93 What a terminal has not got, frq does without: pictures, avatars and the
93 lightbox draw nothing, and calls are off — the media plane paints frames into94 lightbox draw nothing, and calls are off — the media plane paints frames into
94-a texture, and there is no texture here. And frq's spacing is written in95+a texture, and there is no texture here.
95-points, for a window — the backend is handed `:points-per-cell 8` so those96+
96-numbers land in cells, but `below-messages` in `src/frq/app.jolt` is point97+The spacing is written in points, for a window, and a cell is about eight of
97-*arithmetic* rather than a point *length*, and a scale cannot fix it: it98+them across and sixteen down — so the backend is handed both numbers and each
98-reserves about thirteen rows more than the compose bar needs, so the bottom of99+prop is divided by the axis it measures. A gap of half a cell rounds to
99-the backlog is pushed out of the list.100+nothing, which is what `:spacing 8` against a 16-point row is: thirteen
101+messages fit where rounding it up left room for two.
102+
103+The two reserves in `src/frq/app.jolt` are the one thing a scale cannot
104+answer, because they are counted in rows of chrome rather than in lengths: a
105+window's row is 34 points and a terminal's is one cell. `chrome-row` is where
106+that is said, and `frq.tui` sets it.
100 107
101 Two things are unpinned, because the terminal backend is not in a jolt-native108 Two things are unpinned, because the terminal backend is not in a jolt-native
102 release yet: `libjolttui.so` comes out of a jolt-native checkout's target109 release yet: `libjolttui.so` comes out of a jolt-native checkout's target
modified flake.lock +5 -5
@@ -127,16 +127,16 @@
127127 "jolt-native-tui": {
128128 "flake": false,
129129 "locked": {
130- "lastModified": 1788331190,
131- "narHash": "sha256-iH35HO0ViHryfld1QKNb5xCUIDFbQsUWXvMpLA0JFb0=",
130+ "lastModified": 1788332109,
131+ "narHash": "sha256-sS9ruxMwMmpESDVNpDGjdHb37DZRayB0qoJbvYfOp1A=",
132132 "ref": "refs/heads/main",
133- "rev": "68910bd024f8012cf0a0248b2c01850714a3ef49",
134- "revCount": 34,
133+ "rev": "c4f56b0a96ecb29336e2cf16c522b794ae62ad3f",
134+ "revCount": 35,
135135 "type": "git",
136136 "url": "https://gitlab.com/nandithebull/jolt-native"
137137 },
138138 "original": {
139- "rev": "68910bd024f8012cf0a0248b2c01850714a3ef49",
139+ "rev": "c4f56b0a96ecb29336e2cf16c522b794ae62ad3f",
140140 "type": "git",
141141 "url": "https://gitlab.com/nandithebull/jolt-native"
142142 }
@@ -127,16 +127,16 @@
127 "jolt-native-tui": {127 "jolt-native-tui": {
128 "flake": false,128 "flake": false,
129 "locked": {129 "locked": {
130- "lastModified": 1788331190,130+ "lastModified": 1788332109,
131- "narHash": "sha256-iH35HO0ViHryfld1QKNb5xCUIDFbQsUWXvMpLA0JFb0=",131+ "narHash": "sha256-sS9ruxMwMmpESDVNpDGjdHb37DZRayB0qoJbvYfOp1A=",
132 "ref": "refs/heads/main",132 "ref": "refs/heads/main",
133- "rev": "68910bd024f8012cf0a0248b2c01850714a3ef49",133+ "rev": "c4f56b0a96ecb29336e2cf16c522b794ae62ad3f",
134- "revCount": 34,134+ "revCount": 35,
135 "type": "git",135 "type": "git",
136 "url": "https://gitlab.com/nandithebull/jolt-native"136 "url": "https://gitlab.com/nandithebull/jolt-native"
137 },137 },
138 "original": {138 "original": {
139- "rev": "68910bd024f8012cf0a0248b2c01850714a3ef49",139+ "rev": "c4f56b0a96ecb29336e2cf16c522b794ae62ad3f",
140 "type": "git",140 "type": "git",
141 "url": "https://gitlab.com/nandithebull/jolt-native"141 "url": "https://gitlab.com/nandithebull/jolt-native"
142 }142 }
modified flake.nix +1 -1
@@ -51,7 +51,7 @@
5151 # and only `tui` evaluates this. When the backend ships in a release the
5252 # two become one pin again.
5353 jolt-native-tui = {
54- url = "git+https://gitlab.com/nandithebull/jolt-native?rev=68910bd024f8012cf0a0248b2c01850714a3ef49";
54+ url = "git+https://gitlab.com/nandithebull/jolt-native?rev=c4f56b0a96ecb29336e2cf16c522b794ae62ad3f";
5555 flake = false;
5656 };
5757
@@ -51,7 +51,7 @@
51 # and only `tui` evaluates this. When the backend ships in a release the51 # and only `tui` evaluates this. When the backend ships in a release the
52 # two become one pin again.52 # two become one pin again.
53 jolt-native-tui = {53 jolt-native-tui = {
54- url = "git+https://gitlab.com/nandithebull/jolt-native?rev=68910bd024f8012cf0a0248b2c01850714a3ef49";54+ url = "git+https://gitlab.com/nandithebull/jolt-native?rev=c4f56b0a96ecb29336e2cf16c522b794ae62ad3f";
55 flake = false;55 flake = false;
56 };56 };
57 57
modified src/frq/app.jolt +22 -4
@@ -162,12 +162,29 @@
162162
163163 (def ^:private sidebar-width 320)
164164
165+;; How tall a row of chrome is — a button, the compose bar, a line of tabs.
166+;;
167+;; The two reserves below are counted in points against this: a window's row is
168+;; 34 of them, and every gap around it was chosen at that size. A terminal's
169+;; row is one cell, and the same count then reserves two or three times the
170+;; room the strip under the list actually needs — which costs a message a row,
171+;; and a conversation is measured in how many of those fit.
172+;;
173+;; So the counts stay as they are, written where the reasoning is, and a
174+;; backend whose rows are a different height says so here. Nothing else in the
175+;; tree needs it: every other number is a length, and a length scales on the
176+;; way across.
177+(def ^:private window-row 34)
178+(defonce chrome-row (atom window-row))
179+
180+(defn- chrome-scale [] (/ (double @chrome-row) window-row))
181+
165182 (defn- below-list []
166183 ;; What the strip under the list needs, counted: the gap after the list, the
167184 ;; separator, the gap under it, the tab bar's row of buttons and the margin
168185 ;; below them. Short by any of it and the tabs go off the bottom edge — which
169186 ;; is the thing this layout exists to stop.
170- (+ 8 8 8 34 12))
187+ (* (chrome-scale) (+ 8 8 8 34 12)))
171188
172189 (defn chats-screen []
173190 (let [buffers (s/channel-list)]
@@ -985,9 +1002,10 @@
9851002 ;; any of it and the column runs past the bottom edge, which does not show as
9861003 ;; a list that is too long: it shows as a compose bar sitting flat on the
9871004 ;; bottom of the window with its margin cut off.
988- (+ 140
989- (if @s/replying-to 34 0)
990- (if @s/attachment 76 0)))
1005+ (* (chrome-scale)
1006+ (+ 140
1007+ (if @s/replying-to 34 0)
1008+ (if @s/attachment 76 0))))
9911009
9921010 (defn- messages-width
9931011 "How wide the message list may be with the people panel beside it.
@@ -162,12 +162,29 @@
162 162
163 (def ^:private sidebar-width 320)163 (def ^:private sidebar-width 320)
164 164
165+;; How tall a row of chrome is — a button, the compose bar, a line of tabs.
166+;;
167+;; The two reserves below are counted in points against this: a window's row is
168+;; 34 of them, and every gap around it was chosen at that size. A terminal's
169+;; row is one cell, and the same count then reserves two or three times the
170+;; room the strip under the list actually needs — which costs a message a row,
171+;; and a conversation is measured in how many of those fit.
172+;;
173+;; So the counts stay as they are, written where the reasoning is, and a
174+;; backend whose rows are a different height says so here. Nothing else in the
175+;; tree needs it: every other number is a length, and a length scales on the
176+;; way across.
177+(def ^:private window-row 34)
178+(defonce chrome-row (atom window-row))
179+
180+(defn- chrome-scale [] (/ (double @chrome-row) window-row))
181+
165 (defn- below-list []182 (defn- below-list []
166 ;; What the strip under the list needs, counted: the gap after the list, the183 ;; What the strip under the list needs, counted: the gap after the list, the
167 ;; separator, the gap under it, the tab bar's row of buttons and the margin184 ;; separator, the gap under it, the tab bar's row of buttons and the margin
168 ;; below them. Short by any of it and the tabs go off the bottom edge — which185 ;; below them. Short by any of it and the tabs go off the bottom edge — which
169 ;; is the thing this layout exists to stop.186 ;; is the thing this layout exists to stop.
170- (+ 8 8 8 34 12))187+ (* (chrome-scale) (+ 8 8 8 34 12)))
171 188
172 (defn chats-screen []189 (defn chats-screen []
173 (let [buffers (s/channel-list)]190 (let [buffers (s/channel-list)]
@@ -985,9 +1002,10 @@
985 ;; any of it and the column runs past the bottom edge, which does not show as1002 ;; any of it and the column runs past the bottom edge, which does not show as
986 ;; a list that is too long: it shows as a compose bar sitting flat on the1003 ;; a list that is too long: it shows as a compose bar sitting flat on the
987 ;; bottom of the window with its margin cut off.1004 ;; bottom of the window with its margin cut off.
988- (+ 1401005+ (* (chrome-scale)
989- (if @s/replying-to 34 0)1006+ (+ 140
990- (if @s/attachment 76 0)))1007+ (if @s/replying-to 34 0)
1008+ (if @s/attachment 76 0))))
991 1009
992 (defn- messages-width1010 (defn- messages-width
993 "How wide the message list may be with the people panel beside it.1011 "How wide the message list may be with the people panel beside it.
modified src/frq/tui.jolt +24 -8
@@ -46,6 +46,10 @@
4646 ;; in the units it was written against.
4747 (def ^:private points-per-cell 8)
4848
49+;; And down the page a cell is worth about twice that: a character is roughly
50+;; twice as tall as it is wide, which is what decides how many messages fit.
51+(def ^:private points-per-row (* 2 points-per-cell))
52+
4953 (defn- seed!
5054 "Put a conversation on screen without a server behind it — `--demo`.
5155
@@ -86,10 +90,9 @@
8690 []
8791 (let [[cols rows] (tui/screen-size)]
8892 (reset! s/window-width (* cols points-per-cell))
89- ;; A row is worth about twice a column: a cell is taller than it is wide,
90- ;; and the heights in frq — a picture's, the space kept for the compose bar
91- ;; — are measured against the window's.
92- (reset! s/window-height (* rows points-per-cell 2)))
93+ ;; In the same points: the heights in frq — a picture's, the room kept for
94+ ;; the compose bar — are measured against the window's.
95+ (reset! s/window-height (* rows points-per-row)))
9396 nil)
9497
9598 (defn- start!
@@ -100,6 +103,10 @@
100103 Everything else — the saved settings, the rooms, the sign-in that connects
101104 itself — is the same startup the window runs."
102105 []
106+ ;; A row of chrome here is one cell, not a window's 34 points, so the strip
107+ ;; under a scrolling list reserves the three rows it needs rather than the
108+ ;; nine the window's count works out to.
109+ (reset! app/chrome-row points-per-row)
103110 (app/start! {:after! tui/after!
104111 :every! tui/every!
105112 :title! nil
@@ -109,7 +116,7 @@
109116 (defn- run-headless!
110117 "Mount the app in a session of `cols` by `rows` with no terminal, let it
111118 settle, and print what was painted."
112- [cols rows demo? wait]
119+ [cols rows demo? wait dump?]
113120 ;; The size has to be known before the first render, or the layout reads the
114121 ;; zero it was left at. A timer runs on the loop thread, which is where
115122 ;; screen-size may be asked and the first one fires before the first paint.
@@ -122,8 +129,14 @@
122129 ;; of the connect screen and `--wait=` is how you ask for one of the room.
123130 (tui/after! wait (fn []
124131 (println (tui/screen-str))
132+ ;; The tree as the library holds it, which is what a report
133+ ;; about a layout is actually about: props after scaling,
134+ ;; and the shape the reconciler left behind.
135+ (when dump? (println (tui/dump-str)))
125136 (tui/quit!)))
126- (ui/run app/app :headless [cols rows] :points-per-cell points-per-cell))
137+ (ui/run app/app :headless [cols rows]
138+ :points-per-cell points-per-cell
139+ :points-per-row points-per-row))
127140
128141 (defn- run-terminal! [demo?]
129142 ;; On the loop thread, and before the first paint: `start!` sets timers of
@@ -133,12 +146,15 @@
133146 (narrow!)
134147 (start!)
135148 (when demo? (seed!))))
136- (ui/run app/app :mouse true :points-per-cell points-per-cell))
149+ (ui/run app/app :mouse true
150+ :points-per-cell points-per-cell
151+ :points-per-row points-per-row))
137152
138153 (defn -main [& args]
139154 (let [args (set args)
140155 headless? (contains? args "--headless")
141156 demo? (contains? args "--demo")
157+ dump? (contains? args "--dump")
142158 num (fn [flag default]
143159 (if-let [n (first (keep #(when (str/starts-with? % flag)
144160 (subs % (count flag)))
@@ -149,5 +165,5 @@
149165 rows (num "--rows=" 36)
150166 wait (num "--wait=" 400)]
151167 (if headless?
152- (run-headless! cols rows demo? wait)
168+ (run-headless! cols rows demo? wait dump?)
153169 (run-terminal! demo?))))
@@ -46,6 +46,10 @@
46 ;; in the units it was written against.46 ;; in the units it was written against.
47 (def ^:private points-per-cell 8)47 (def ^:private points-per-cell 8)
48 48
49+;; And down the page a cell is worth about twice that: a character is roughly
50+;; twice as tall as it is wide, which is what decides how many messages fit.
51+(def ^:private points-per-row (* 2 points-per-cell))
52+
49 (defn- seed!53 (defn- seed!
50 "Put a conversation on screen without a server behind it — `--demo`.54 "Put a conversation on screen without a server behind it — `--demo`.
51 55
@@ -86,10 +90,9 @@
86 []90 []
87 (let [[cols rows] (tui/screen-size)]91 (let [[cols rows] (tui/screen-size)]
88 (reset! s/window-width (* cols points-per-cell))92 (reset! s/window-width (* cols points-per-cell))
89- ;; A row is worth about twice a column: a cell is taller than it is wide,93+ ;; In the same points: the heights in frq — a picture's, the room kept for
90- ;; and the heights in frq — a picture's, the space kept for the compose bar94+ ;; the compose bar — are measured against the window's.
91- ;; — are measured against the window's.95+ (reset! s/window-height (* rows points-per-row)))
92- (reset! s/window-height (* rows points-per-cell 2)))
93 nil)96 nil)
94 97
95 (defn- start!98 (defn- start!
@@ -100,6 +103,10 @@
100 Everything else — the saved settings, the rooms, the sign-in that connects103 Everything else — the saved settings, the rooms, the sign-in that connects
101 itself — is the same startup the window runs."104 itself — is the same startup the window runs."
102 []105 []
106+ ;; A row of chrome here is one cell, not a window's 34 points, so the strip
107+ ;; under a scrolling list reserves the three rows it needs rather than the
108+ ;; nine the window's count works out to.
109+ (reset! app/chrome-row points-per-row)
103 (app/start! {:after! tui/after!110 (app/start! {:after! tui/after!
104 :every! tui/every!111 :every! tui/every!
105 :title! nil112 :title! nil
@@ -109,7 +116,7 @@
109 (defn- run-headless!116 (defn- run-headless!
110 "Mount the app in a session of `cols` by `rows` with no terminal, let it117 "Mount the app in a session of `cols` by `rows` with no terminal, let it
111 settle, and print what was painted."118 settle, and print what was painted."
112- [cols rows demo? wait]119+ [cols rows demo? wait dump?]
113 ;; The size has to be known before the first render, or the layout reads the120 ;; The size has to be known before the first render, or the layout reads the
114 ;; zero it was left at. A timer runs on the loop thread, which is where121 ;; zero it was left at. A timer runs on the loop thread, which is where
115 ;; screen-size may be asked and the first one fires before the first paint.122 ;; screen-size may be asked and the first one fires before the first paint.
@@ -122,8 +129,14 @@
122 ;; of the connect screen and `--wait=` is how you ask for one of the room.129 ;; of the connect screen and `--wait=` is how you ask for one of the room.
123 (tui/after! wait (fn []130 (tui/after! wait (fn []
124 (println (tui/screen-str))131 (println (tui/screen-str))
132+ ;; The tree as the library holds it, which is what a report
133+ ;; about a layout is actually about: props after scaling,
134+ ;; and the shape the reconciler left behind.
135+ (when dump? (println (tui/dump-str)))
125 (tui/quit!)))136 (tui/quit!)))
126- (ui/run app/app :headless [cols rows] :points-per-cell points-per-cell))137+ (ui/run app/app :headless [cols rows]
138+ :points-per-cell points-per-cell
139+ :points-per-row points-per-row))
127 140
128 (defn- run-terminal! [demo?]141 (defn- run-terminal! [demo?]
129 ;; On the loop thread, and before the first paint: `start!` sets timers of142 ;; On the loop thread, and before the first paint: `start!` sets timers of
@@ -133,12 +146,15 @@
133 (narrow!)146 (narrow!)
134 (start!)147 (start!)
135 (when demo? (seed!))))148 (when demo? (seed!))))
136- (ui/run app/app :mouse true :points-per-cell points-per-cell))149+ (ui/run app/app :mouse true
150+ :points-per-cell points-per-cell
151+ :points-per-row points-per-row))
137 152
138 (defn -main [& args]153 (defn -main [& args]
139 (let [args (set args)154 (let [args (set args)
140 headless? (contains? args "--headless")155 headless? (contains? args "--headless")
141 demo? (contains? args "--demo")156 demo? (contains? args "--demo")
157+ dump? (contains? args "--dump")
142 num (fn [flag default]158 num (fn [flag default]
143 (if-let [n (first (keep #(when (str/starts-with? % flag)159 (if-let [n (first (keep #(when (str/starts-with? % flag)
144 (subs % (count flag)))160 (subs % (count flag)))
@@ -149,5 +165,5 @@
149 rows (num "--rows=" 36)165 rows (num "--rows=" 36)
150 wait (num "--wait=" 400)]166 wait (num "--wait=" 400)]
151 (if headless?167 (if headless?
152- (run-headless! cols rows demo? wait)168+ (run-headless! cols rows demo? wait dump?)
153 (run-terminal! demo?))))169 (run-terminal! demo?))))