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

Draw frq.app's own connect screen on the phone

One file, rendered by jolt on the desktop and ClojureDart on the phone, in
the COSMIC theme read out of cosmic-config. The TUI renders byte-identical
either side of this, which is the only proof that it is really one file.

What was blanking it was frq.hiccup ignoring :width-request. The connect
screen puts the host and the port side by side in an :hbox and gives each a
width; a TextField takes its width from its parent and a Row offers unbounded
width, so that is a hard layout error. jolt-cosmic already says this — an
entry is Length::Fixed(w) from width_request unless :hexpand asks for the
rest of the row — and the renderer simply had not grown it.

The shape of the failure is worth naming. A layout error happens after the
build, so no try/catch sees it, Flutter paints nothing rather than its red
box, and every sibling in the same children vector disappears with it. The
screen was blank and the log was empty.

Four theories went past before the bisect: Center in an unbounded height,
fn* as a binding name, qualified symbols in :watch, a Builder error boundary
— each one a three-minute deploy, none of them it. What worked was a harness
rendering one candidate at a time with a labelled marker between them, so the
last label standing says where it died. It walked down to server-fields in
three passes.

That harness also proved the rest: frq.cells compiles under both compilers
and holds its values on the device, frq.screens.connect loads there, and
every tag and component renders on its own.

And it found a second bug behind the first. Two :entry nodes with no :key
shared one TextEditingController, so the host field showed the port — glimmer
matches children by position when there is no key, and a backend that holds a
controller per field needs a name for it. Every entry in the shared screen
carries one now, which changes nothing on the desktop.

mode-tabs, server-fields and connect-action are public rather than private:
a bisect cannot reach a private fn, and they are worth reaching.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-11T20:37:23-07:00 Browse files
0bc64b5 parent: 39459ed
modified common/frq/screens/connect.cljc +20 -9
@@ -30,7 +30,7 @@
3030
3131 ;; ---------------------------------------------------------------- connect
3232
33-(defn- mode-tabs []
33+(defn mode-tabs []
3434 [:hbox {:spacing 8}
3535 (for [[k label] [[:guest "Guest"] [:bluesky "Bluesky"] [:app-password "App password"]]]
3636 [:button {:key k
@@ -38,15 +38,22 @@
3838 :kind (if (= k @cells/auth-mode) :primary :default)
3939 :on-click #(reset! cells/auth-mode k)}])])
4040
41-(defn- server-fields []
41+(defn server-fields []
4242 [:vbox {:spacing 6}
43+ ;; Every :entry carries a :key. glimmer matches children by position when
44+ ;; one is absent, so this changes nothing on the desktop — but a backend
45+ ;; that keeps a text controller per field needs a stable name for it, and
46+ ;; without one the host and the port shared a controller and both showed
47+ ;; the port. See frq.hiccup.
4348 [:label {:label "Server"}]
4449 [:hbox {:spacing 8}
45- [:entry {:text @cells/form-host
50+ [:entry {:key :host
51+ :text @cells/form-host
4652 :width-request 220
4753 :placeholder "host"
4854 :on-change #(reset! cells/form-host %)}]
49- [:entry {:text @cells/form-port
55+ [:entry {:key :port
56+ :text @cells/form-port
5057 :width-request 90
5158 :placeholder "6697"
5259 :on-change #(reset! cells/form-port %)}]]
@@ -56,7 +63,7 @@
5663 (reset! cells/form-port
5764 (if @cells/form-tls? "6697" "6667")))}]])
5865
59-(defn- connect-action []
66+(defn connect-action []
6067 (if @cells/connecting?
6168 [:hbox {:spacing 8}
6269 [:spinner {}]
@@ -78,7 +85,8 @@
7885 [:title-2 {:label "Sign in with Bluesky"}]
7986 [:dim-label {:label "Opens your browser for AT Protocol OAuth. freeq's broker hands back a token; no password passes through frq."}]
8087 [:label {:label "Handle"}]
81- [:entry {:text @cells/form-handle
88+ [:entry {:key :handle
89+ :text @cells/form-handle
8290 :width-request 320
8391 :placeholder "alice.bsky.social"
8492 :on-change #(reset! cells/form-handle %)}]
@@ -98,12 +106,14 @@
98106 [:title-2 {:label "Sign in with an app password"}]
99107 [:dim-label {:label "No browser. Your app password goes to your own PDS; freeq is handed the session it mints."}]
100108 [:label {:label "Handle"}]
101- [:entry {:text @cells/form-handle
109+ [:entry {:key :handle
110+ :text @cells/form-handle
102111 :width-request 320
103112 :placeholder "alice.bsky.social"
104113 :on-change #(reset! cells/form-handle %)}]
105114 [:label {:label "App password"}]
106- [:entry {:text @cells/form-app-password
115+ [:entry {:key :app-password
116+ :text @cells/form-app-password
107117 :width-request 320
108118 :placeholder "xxxx-xxxx-xxxx-xxxx"
109119 :on-change #(reset! cells/form-app-password %)}]
@@ -112,7 +122,8 @@
112122 [:vbox {:spacing 6}
113123 [:title-2 {:label "Connect as guest"}]
114124 [:label {:label "Nick"}]
115- [:entry {:text @cells/form-nick
125+ [:entry {:key :nick
126+ :text @cells/form-nick
116127 :width-request 320
117128 :placeholder "your nick"
118129 :on-change #(reset! cells/form-nick %)}]])
@@ -30,7 +30,7 @@
30 30
31 ;; ---------------------------------------------------------------- connect31 ;; ---------------------------------------------------------------- connect
32 32
33-(defn- mode-tabs []33+(defn mode-tabs []
34 [:hbox {:spacing 8}34 [:hbox {:spacing 8}
35 (for [[k label] [[:guest "Guest"] [:bluesky "Bluesky"] [:app-password "App password"]]]35 (for [[k label] [[:guest "Guest"] [:bluesky "Bluesky"] [:app-password "App password"]]]
36 [:button {:key k36 [:button {:key k
@@ -38,15 +38,22 @@
38 :kind (if (= k @cells/auth-mode) :primary :default)38 :kind (if (= k @cells/auth-mode) :primary :default)
39 :on-click #(reset! cells/auth-mode k)}])])39 :on-click #(reset! cells/auth-mode k)}])])
40 40
41-(defn- server-fields []41+(defn server-fields []
42 [:vbox {:spacing 6}42 [:vbox {:spacing 6}
43+ ;; Every :entry carries a :key. glimmer matches children by position when
44+ ;; one is absent, so this changes nothing on the desktop — but a backend
45+ ;; that keeps a text controller per field needs a stable name for it, and
46+ ;; without one the host and the port shared a controller and both showed
47+ ;; the port. See frq.hiccup.
43 [:label {:label "Server"}]48 [:label {:label "Server"}]
44 [:hbox {:spacing 8}49 [:hbox {:spacing 8}
45- [:entry {:text @cells/form-host50+ [:entry {:key :host
51+ :text @cells/form-host
46 :width-request 22052 :width-request 220
47 :placeholder "host"53 :placeholder "host"
48 :on-change #(reset! cells/form-host %)}]54 :on-change #(reset! cells/form-host %)}]
49- [:entry {:text @cells/form-port55+ [:entry {:key :port
56+ :text @cells/form-port
50 :width-request 9057 :width-request 90
51 :placeholder "6697"58 :placeholder "6697"
52 :on-change #(reset! cells/form-port %)}]]59 :on-change #(reset! cells/form-port %)}]]
@@ -56,7 +63,7 @@
56 (reset! cells/form-port63 (reset! cells/form-port
57 (if @cells/form-tls? "6697" "6667")))}]])64 (if @cells/form-tls? "6697" "6667")))}]])
58 65
59-(defn- connect-action []66+(defn connect-action []
60 (if @cells/connecting?67 (if @cells/connecting?
61 [:hbox {:spacing 8}68 [:hbox {:spacing 8}
62 [:spinner {}]69 [:spinner {}]
@@ -78,7 +85,8 @@
78 [:title-2 {:label "Sign in with Bluesky"}]85 [:title-2 {:label "Sign in with Bluesky"}]
79 [:dim-label {:label "Opens your browser for AT Protocol OAuth. freeq's broker hands back a token; no password passes through frq."}]86 [:dim-label {:label "Opens your browser for AT Protocol OAuth. freeq's broker hands back a token; no password passes through frq."}]
80 [:label {:label "Handle"}]87 [:label {:label "Handle"}]
81- [:entry {:text @cells/form-handle88+ [:entry {:key :handle
89+ :text @cells/form-handle
82 :width-request 32090 :width-request 320
83 :placeholder "alice.bsky.social"91 :placeholder "alice.bsky.social"
84 :on-change #(reset! cells/form-handle %)}]92 :on-change #(reset! cells/form-handle %)}]
@@ -98,12 +106,14 @@
98 [:title-2 {:label "Sign in with an app password"}]106 [:title-2 {:label "Sign in with an app password"}]
99 [:dim-label {:label "No browser. Your app password goes to your own PDS; freeq is handed the session it mints."}]107 [:dim-label {:label "No browser. Your app password goes to your own PDS; freeq is handed the session it mints."}]
100 [:label {:label "Handle"}]108 [:label {:label "Handle"}]
101- [:entry {:text @cells/form-handle109+ [:entry {:key :handle
110+ :text @cells/form-handle
102 :width-request 320111 :width-request 320
103 :placeholder "alice.bsky.social"112 :placeholder "alice.bsky.social"
104 :on-change #(reset! cells/form-handle %)}]113 :on-change #(reset! cells/form-handle %)}]
105 [:label {:label "App password"}]114 [:label {:label "App password"}]
106- [:entry {:text @cells/form-app-password115+ [:entry {:key :app-password
116+ :text @cells/form-app-password
107 :width-request 320117 :width-request 320
108 :placeholder "xxxx-xxxx-xxxx-xxxx"118 :placeholder "xxxx-xxxx-xxxx-xxxx"
109 :on-change #(reset! cells/form-app-password %)}]119 :on-change #(reset! cells/form-app-password %)}]
@@ -112,7 +122,8 @@
112 [:vbox {:spacing 6}122 [:vbox {:spacing 6}
113 [:title-2 {:label "Connect as guest"}]123 [:title-2 {:label "Connect as guest"}]
114 [:label {:label "Nick"}]124 [:label {:label "Nick"}]
115- [:entry {:text @cells/form-nick125+ [:entry {:key :nick
126+ :text @cells/form-nick
116 :width-request 320127 :width-request 320
117 :placeholder "your nick"128 :placeholder "your nick"
118 :on-change #(reset! cells/form-nick %)}]])129 :on-change #(reset! cells/form-nick %)}]])
modified flutter/README.md +25 -4
@@ -130,10 +130,10 @@ What `frq.hiccup` does not do is glimmer's reconciliation: Flutter rebuilds
130130 from the top and diffs its own element tree, so a cell firing rebuilds the
131131 screen rather than the subtree that read it. Fine at this size.
132132
133-`frq.main` still paints a hand-written tree rather than `frq.app`'s own. Not
134-because the screens need changing — because requiring them pulls `frq.state`,
135-which pulls `frq.irc`, which reaches for jolt.host. The tree it paints uses
136-only tags `frq.app` uses, so it is a test of the backend and nothing more.
133+`frq.main` paints `frq.app`'s own connect screen, out of
134+`common/frq/screens/connect.cljc` — the same file the desktop renders. What it
135+reads is `frq.cells` and what it calls is `frq.actions`, and each platform
136+fills those in: `frq.state`'s reducers on the desktop, dart:io here.
137137
138138 ## The order to do the rest in
139139
@@ -165,3 +165,24 @@ only tags `frq.app` uses, so it is a test of the backend and nothing more.
165165 platform by reader conditional.
166166 6. **`frq.app`** follows it, and the tags it uses that `frq.hiccup` does not
167167 cover yet paint as an orange `?tag` until they do.
168+
169+## What a missing tag property looks like
170+
171+Worth writing down, because it cost an evening. `frq.hiccup` ignored
172+`:width-request`, and the connect screen puts two entries side by side in an
173+`:hbox` with one. A TextField takes its width from its parent and a Row offers
174+unbounded width, so that is a hard layout error — and a layout error happens
175+after the build, so it is not an exception anything can catch, paints nothing
176+at all rather than Flutter's red box, and takes every sibling in the same
177+`children` vector down with it. The screen was blank and the log was empty.
178+
179+The way through was a harness that renders each candidate in turn with a
180+labelled marker between them, so the last label standing says where it died.
181+Not guesswork: four wrong theories went past before that — `Center` in an
182+unbounded height, `fn*` as a binding name, qualified symbols in `:watch`, a
183+`Builder` boundary — each one a three-minute deploy.
184+
185+It also found that two `:entry` nodes with no `:key` shared one
186+TextEditingController, so the host field showed the port. glimmer matches
187+children by position when there is no key; a backend holding a controller per
188+field needs a name for it.
@@ -130,10 +130,10 @@ What `frq.hiccup` does not do is glimmer's reconciliation: Flutter rebuilds
130 from the top and diffs its own element tree, so a cell firing rebuilds the130 from the top and diffs its own element tree, so a cell firing rebuilds the
131 screen rather than the subtree that read it. Fine at this size.131 screen rather than the subtree that read it. Fine at this size.
132 132
133-`frq.main` still paints a hand-written tree rather than `frq.app`'s own. Not133+`frq.main` paints `frq.app`'s own connect screen, out of
134-because the screens need changing — because requiring them pulls `frq.state`,134+`common/frq/screens/connect.cljc` — the same file the desktop renders. What it
135-which pulls `frq.irc`, which reaches for jolt.host. The tree it paints uses135+reads is `frq.cells` and what it calls is `frq.actions`, and each platform
136-only tags `frq.app` uses, so it is a test of the backend and nothing more.136+fills those in: `frq.state`'s reducers on the desktop, dart:io here.
137 137
138 ## The order to do the rest in138 ## The order to do the rest in
139 139
@@ -165,3 +165,24 @@ only tags `frq.app` uses, so it is a test of the backend and nothing more.
165 platform by reader conditional.165 platform by reader conditional.
166 6. **`frq.app`** follows it, and the tags it uses that `frq.hiccup` does not166 6. **`frq.app`** follows it, and the tags it uses that `frq.hiccup` does not
167 cover yet paint as an orange `?tag` until they do.167 cover yet paint as an orange `?tag` until they do.
168+
169+## What a missing tag property looks like
170+
171+Worth writing down, because it cost an evening. `frq.hiccup` ignored
172+`:width-request`, and the connect screen puts two entries side by side in an
173+`:hbox` with one. A TextField takes its width from its parent and a Row offers
174+unbounded width, so that is a hard layout error — and a layout error happens
175+after the build, so it is not an exception anything can catch, paints nothing
176+at all rather than Flutter's red box, and takes every sibling in the same
177+`children` vector down with it. The screen was blank and the log was empty.
178+
179+The way through was a harness that renders each candidate in turn with a
180+labelled marker between them, so the last label standing says where it died.
181+Not guesswork: four wrong theories went past before that — `Center` in an
182+unbounded height, `fn*` as a binding name, qualified symbols in `:watch`, a
183+`Builder` boundary — each one a three-minute deploy.
184+
185+It also found that two `:entry` nodes with no `:key` shared one
186+TextEditingController, so the host field showed the port. glimmer matches
187+children by position when there is no key; a backend holding a controller per
188+field needs a name for it.
modified flutter/src/frq/hiccup.cljd +20 -3
@@ -245,11 +245,20 @@
245245
246246 ;; text_input: a filled rounded field, no outline. COSMIC entries sit in
247247 ;; the component colour rather than behind a border.
248+ ;; An entry sizes itself, as it does in jolt-cosmic: Fixed(w) from
249+ ;; :width-request unless :hexpand asks for the rest of the row.
250+ ;;
251+ ;; Not optional. A TextField takes its width from its parent, and a Row
252+ ;; offers its children unbounded width — so two entries side by side in
253+ ;; an :hbox, which is exactly what the connect screen's host and port
254+ ;; are, is a hard layout error. That is what painted the whole screen
255+ ;; blank with nothing in the log.
248256 :entry
249257 (let [on-change (:on-change p)
250258 on-activate (:on-activate p)
251- rows (:rows p)]
252- (m/TextField
259+ rows (:rows p)
260+ w (:width-request p)
261+ field (m/TextField
253262 .controller (controller-for (:key p) (:text p))
254263 .onChanged (when on-change #(on-change %))
255264 .onSubmitted (when on-activate (fn [_] (on-activate)))
@@ -275,7 +284,15 @@
275284 .focusedBorder (m/OutlineInputBorder
276285 .borderRadius (m/BorderRadius.circular t/radius-s)
277286 .borderSide (m/BorderSide .color t/accent
278- .width 1.0)))))
287+ .width 1.0))))]
288+ ;; No Expanded when there is no width: Expanded in a Column expands
289+ ;; along the main axis, which is vertical, and an entry in a card
290+ ;; would grow to fill the card. A bare field is right there — a
291+ ;; Column hands its children bounded width — and a row wants the
292+ ;; width-request the caller already writes.
293+ (if w
294+ (m/SizedBox .width (dbl w 200.0) .child field)
295+ field))
279296
280297 :emoji
281298 (m/Text (str (:emoji p "")) .style (m/TextStyle .fontSize (dbl (:size p) 16.0)))
@@ -245,11 +245,20 @@
245 245
246 ;; text_input: a filled rounded field, no outline. COSMIC entries sit in246 ;; text_input: a filled rounded field, no outline. COSMIC entries sit in
247 ;; the component colour rather than behind a border.247 ;; the component colour rather than behind a border.
248+ ;; An entry sizes itself, as it does in jolt-cosmic: Fixed(w) from
249+ ;; :width-request unless :hexpand asks for the rest of the row.
250+ ;;
251+ ;; Not optional. A TextField takes its width from its parent, and a Row
252+ ;; offers its children unbounded width — so two entries side by side in
253+ ;; an :hbox, which is exactly what the connect screen's host and port
254+ ;; are, is a hard layout error. That is what painted the whole screen
255+ ;; blank with nothing in the log.
248 :entry256 :entry
249 (let [on-change (:on-change p)257 (let [on-change (:on-change p)
250 on-activate (:on-activate p)258 on-activate (:on-activate p)
251- rows (:rows p)]259+ rows (:rows p)
252- (m/TextField260+ w (:width-request p)
261+ field (m/TextField
253 .controller (controller-for (:key p) (:text p))262 .controller (controller-for (:key p) (:text p))
254 .onChanged (when on-change #(on-change %))263 .onChanged (when on-change #(on-change %))
255 .onSubmitted (when on-activate (fn [_] (on-activate)))264 .onSubmitted (when on-activate (fn [_] (on-activate)))
@@ -275,7 +284,15 @@
275 .focusedBorder (m/OutlineInputBorder284 .focusedBorder (m/OutlineInputBorder
276 .borderRadius (m/BorderRadius.circular t/radius-s)285 .borderRadius (m/BorderRadius.circular t/radius-s)
277 .borderSide (m/BorderSide .color t/accent286 .borderSide (m/BorderSide .color t/accent
278- .width 1.0)))))287+ .width 1.0))))]
288+ ;; No Expanded when there is no width: Expanded in a Column expands
289+ ;; along the main axis, which is vertical, and an entry in a card
290+ ;; would grow to fill the card. A bare field is right there — a
291+ ;; Column hands its children bounded width — and a row wants the
292+ ;; width-request the caller already writes.
293+ (if w
294+ (m/SizedBox .width (dbl w 200.0) .child field)
295+ field))
279 296
280 :emoji297 :emoji
281 (m/Text (str (:emoji p "")) .style (m/TextStyle .fontSize (dbl (:size p) 16.0)))298 (m/Text (str (:emoji p "")) .style (m/TextStyle .fontSize (dbl (:size p) 16.0)))
modified flutter/src/frq/main.cljd +18 -2
@@ -161,6 +161,22 @@
161161 (m/Scaffold .appBar (m/AppBar .title (m/Text "frq")))
162162 .body
163163 (f/widget
164- :watch [st status ls lines io identity-out hh handle]
164+ :let [c-status cells/status
165+ c-error cells/error
166+ c-connecting cells/connecting?
167+ c-mode cells/auth-mode
168+ c-handle cells/form-handle
169+ c-nick cells/form-nick
170+ c-host cells/form-host
171+ c-port cells/form-port
172+ c-tls cells/form-tls?
173+ c-apppw cells/form-app-password
174+ c-broker cells/broker-token
175+ c-login cells/login-url]
176+ :watch [st c-status er c-error cn c-connecting am c-mode
177+ fh c-handle nk c-nick hs c-host pt c-port tl c-tls
178+ ap c-apppw bt c-broker lu c-login ls lines]
165179 (m/SingleChildScrollView
166- .child (h/render (screen)))))))
180+ ;; frq.app's own connect screen, out of common/ — the same file the
181+ ;; desktop renders, painted by frq.hiccup in COSMIC's theme.
182+ .child (h/render [connect/connect-screen]))))))
@@ -161,6 +161,22 @@
161 (m/Scaffold .appBar (m/AppBar .title (m/Text "frq")))161 (m/Scaffold .appBar (m/AppBar .title (m/Text "frq")))
162 .body162 .body
163 (f/widget163 (f/widget
164- :watch [st status ls lines io identity-out hh handle]164+ :let [c-status cells/status
165+ c-error cells/error
166+ c-connecting cells/connecting?
167+ c-mode cells/auth-mode
168+ c-handle cells/form-handle
169+ c-nick cells/form-nick
170+ c-host cells/form-host
171+ c-port cells/form-port
172+ c-tls cells/form-tls?
173+ c-apppw cells/form-app-password
174+ c-broker cells/broker-token
175+ c-login cells/login-url]
176+ :watch [st c-status er c-error cn c-connecting am c-mode
177+ fh c-handle nk c-nick hs c-host pt c-port tl c-tls
178+ ap c-apppw bt c-broker lu c-login ls lines]
165 (m/SingleChildScrollView179 (m/SingleChildScrollView
166- .child (h/render (screen)))))))180+ ;; frq.app's own connect screen, out of common/ — the same file the
181+ ;; desktop renders, painted by frq.hiccup in COSMIC's theme.
182+ .child (h/render [connect/connect-screen]))))))