The window connects
`just nim-spike` now does what `just nim-live` did: opens a TLS connection to
irc.freeq.at, registers, joins #test and shows the room filling. Same Nim, same
events, same reducer — the GUI was always dispatching them, and what stood
between it and a connection was one missing library path.
That was the whole bug. libfrqcore.so links OpenSSL for the TLS on :6697, and
the `flutter-desktop` shell had no libssl on its loader path where the `dart`
shell did — so the live script worked and the window would have died inside
`newContext` with a SIGSEGV that says nothing about SSL. It is `FRQ_OPENSSL_LIB`
in that shell now, prepended by the recipe for the app it launches and nothing
else: setting LD_LIBRARY_PATH in the shell itself would have reached Flutter's
own nixGL, which does careful things to the loader path that are not ours to
disturb.
Two switches beside FRQ_TRACE, and they exist because a Wayland window cannot
be clicked from a script: FRQ_AUTOCONNECT=1 presses Connect on the first
render, FRQ_NICK overrides the nickname — two runs with the same one collide
and the second is refused. Without them the only way to check that the window
connects is to sit in front of it, which is not a check.
Proof, from the window rather than a script:
auto FRQ_AUTOCONNECT set — connecting as frq-gui-21893
irc dialling irc.freeq.at:6697 over TLS
irc connected
irc.out NICK/USER as frq-gui-21893
irc.in :irc.freeq.at 001 … :Welcome to irc.freeq.at (guest)
irc.out JOIN #test
irc.in :irc.freeq.at 332 … #test 6789
112 lines received, the backlog rendered, no Dart exception.
The compose box also sends on Enter now and keeps focus afterwards. Both were
missing and both are the difference between a demo and something typeable: the
field is rebuilt from a fresh tree on every keystroke, so without a FocusNode
held per key the caret goes nowhere after the first line — the same bug the
controllers already had, one layer up.
Verified: 43 Nim, 29 Dart, 7 widget tests, and the window itself against the
real server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>1d62d1a parent: 35994d4 modified
flake.nix +8 -0 | @@ -595,6 +595,14 @@ | ||
| 595 | 595 | # pure Dart over the XDG directories. |
| 596 | 596 | buildInputs = [ pkgs.gtk3 pkgs.glib ]; |
| 597 | 597 | |
| 598 | + # Where libfrqcore.so's OpenSSL lives. A named variable and NOT | |
| 599 | + # LD_LIBRARY_PATH, deliberately: this shell also runs Flutter | |
| 600 | + # through nixGL, which does its own careful things to the loader | |
| 601 | + # path, and a blanket LD_LIBRARY_PATH here is the sort of thing | |
| 602 | + # that breaks GL on one machine and not another. The `nim-spike` | |
| 603 | + # recipe prepends this for the app it launches and nothing else. | |
| 604 | + FRQ_OPENSSL_LIB = lib.makeLibraryPath [ pkgs.openssl ]; | |
| 605 | + | |
| 598 | 606 | # Flutter paints through GL, and off NixOS the driver that can do |
| 599 | 607 | # that is the host's, not the store's. |
| 600 | 608 | NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel"; |
| @@ -595,6 +595,14 @@ | |||
| 595 | # pure Dart over the XDG directories. | 595 | # pure Dart over the XDG directories. |
| 596 | buildInputs = [ pkgs.gtk3 pkgs.glib ]; | 596 | buildInputs = [ pkgs.gtk3 pkgs.glib ]; |
| 597 | 597 | ||
| 598 | + # Where libfrqcore.so's OpenSSL lives. A named variable and NOT | ||
| 599 | + # LD_LIBRARY_PATH, deliberately: this shell also runs Flutter | ||
| 600 | + # through nixGL, which does its own careful things to the loader | ||
| 601 | + # path, and a blanket LD_LIBRARY_PATH here is the sort of thing | ||
| 602 | + # that breaks GL on one machine and not another. The `nim-spike` | ||
| 603 | + # recipe prepends this for the app it launches and nothing else. | ||
| 604 | + FRQ_OPENSSL_LIB = lib.makeLibraryPath [ pkgs.openssl ]; | ||
| 605 | + | ||
| 598 | # Flutter paints through GL, and off NixOS the driver that can do | 606 | # Flutter paints through GL, and off NixOS the driver that can do |
| 599 | # that is the host's, not the store's. | 607 | # that is the host's, not the store's. |
| 600 | NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel"; | 608 | NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel"; |
modified
flutter/lib/nim_renderer.dart +19 -2 | @@ -51,8 +51,17 @@ class _NimAppState extends State<NimApp> { | ||
| 51 | 51 | // comment survives three languages now. |
| 52 | 52 | final _controllers = <String, TextEditingController>{}; |
| 53 | 53 | |
| 54 | - void _send(String id, [String value = '']) => | |
| 55 | - setState(() => _tree = core.dispatch(id, value)); | |
| 54 | + // One focus node per keyed entry, for the same reason as the controllers. | |
| 55 | + // Without it, sending with Enter drops focus and the next line is typed | |
| 56 | + // into nothing — the field is rebuilt from a fresh tree every time. | |
| 57 | + final _focus = <String, FocusNode>{}; | |
| 58 | + | |
| 59 | + void _send(String id, [String value = '']) { | |
| 60 | + setState(() => _tree = core.dispatch(id, value)); | |
| 61 | + // Enter in the compose box clears the draft in Nim and rebuilds the | |
| 62 | + // field; putting focus back is what makes a second line typeable. | |
| 63 | + if (id == 'send') _focus['draft']?.requestFocus(); | |
| 64 | + } | |
| 56 | 65 | |
| 57 | 66 | @override |
| 58 | 67 | void dispose() { |
| @@ -60,6 +69,9 @@ class _NimAppState extends State<NimApp> { | ||
| 60 | 69 | for (final c in _controllers.values) { |
| 61 | 70 | c.dispose(); |
| 62 | 71 | } |
| 72 | + for (final f in _focus.values) { | |
| 73 | + f.dispose(); | |
| 74 | + } | |
| 63 | 75 | super.dispose(); |
| 64 | 76 | } |
| 65 | 77 | |
| @@ -193,12 +205,17 @@ class _NimAppState extends State<NimApp> { | ||
| 193 | 205 | } |
| 194 | 206 | final field = TextField( |
| 195 | 207 | controller: c, |
| 208 | + focusNode: _focus.putIfAbsent(key, FocusNode.new), | |
| 196 | 209 | decoration: InputDecoration( |
| 197 | 210 | hintText: n.prop('placeholder', ''), |
| 198 | 211 | isDense: true, |
| 199 | 212 | border: const OutlineInputBorder(), |
| 200 | 213 | ), |
| 201 | 214 | onChanged: (v) => _send(n.prop('onChange', ''), v), |
| 215 | + onSubmitted: (_) { | |
| 216 | + final submit = n.prop('onSubmit', ''); | |
| 217 | + if (submit.isNotEmpty) _send(submit); | |
| 218 | + }, | |
| 202 | 219 | ); |
| 203 | 220 | final w = n.prop('widthRequest', 0); |
| 204 | 221 | // A width request is a minimum in the screens' vocabulary, but here it |
| @@ -51,8 +51,17 @@ class _NimAppState extends State<NimApp> { | |||
| 51 | // comment survives three languages now. | 51 | // comment survives three languages now. |
| 52 | final _controllers = <String, TextEditingController>{}; | 52 | final _controllers = <String, TextEditingController>{}; |
| 53 | 53 | ||
| 54 | - void _send(String id, [String value = '']) => | 54 | + // One focus node per keyed entry, for the same reason as the controllers. |
| 55 | - setState(() => _tree = core.dispatch(id, value)); | 55 | + // Without it, sending with Enter drops focus and the next line is typed |
| 56 | + // into nothing — the field is rebuilt from a fresh tree every time. | ||
| 57 | + final _focus = <String, FocusNode>{}; | ||
| 58 | + | ||
| 59 | + void _send(String id, [String value = '']) { | ||
| 60 | + setState(() => _tree = core.dispatch(id, value)); | ||
| 61 | + // Enter in the compose box clears the draft in Nim and rebuilds the | ||
| 62 | + // field; putting focus back is what makes a second line typeable. | ||
| 63 | + if (id == 'send') _focus['draft']?.requestFocus(); | ||
| 64 | + } | ||
| 56 | 65 | ||
| 57 | @override | 66 | @override |
| 58 | void dispose() { | 67 | void dispose() { |
| @@ -60,6 +69,9 @@ class _NimAppState extends State<NimApp> { | |||
| 60 | for (final c in _controllers.values) { | 69 | for (final c in _controllers.values) { |
| 61 | c.dispose(); | 70 | c.dispose(); |
| 62 | } | 71 | } |
| 72 | + for (final f in _focus.values) { | ||
| 73 | + f.dispose(); | ||
| 74 | + } | ||
| 63 | super.dispose(); | 75 | super.dispose(); |
| 64 | } | 76 | } |
| 65 | 77 | ||
| @@ -193,12 +205,17 @@ class _NimAppState extends State<NimApp> { | |||
| 193 | } | 205 | } |
| 194 | final field = TextField( | 206 | final field = TextField( |
| 195 | controller: c, | 207 | controller: c, |
| 208 | + focusNode: _focus.putIfAbsent(key, FocusNode.new), | ||
| 196 | decoration: InputDecoration( | 209 | decoration: InputDecoration( |
| 197 | hintText: n.prop('placeholder', ''), | 210 | hintText: n.prop('placeholder', ''), |
| 198 | isDense: true, | 211 | isDense: true, |
| 199 | border: const OutlineInputBorder(), | 212 | border: const OutlineInputBorder(), |
| 200 | ), | 213 | ), |
| 201 | onChanged: (v) => _send(n.prop('onChange', ''), v), | 214 | onChanged: (v) => _send(n.prop('onChange', ''), v), |
| 215 | + onSubmitted: (_) { | ||
| 216 | + final submit = n.prop('onSubmit', ''); | ||
| 217 | + if (submit.isNotEmpty) _send(submit); | ||
| 218 | + }, | ||
| 202 | ); | 219 | ); |
| 203 | final w = n.prop('widthRequest', 0); | 220 | final w = n.prop('widthRequest', 0); |
| 204 | // A width request is a minimum in the screens' vocabulary, but here it | 221 | // A width request is a minimum in the screens' vocabulary, but here it |
modified
justfile +4 -0 | @@ -454,6 +454,10 @@ nim-spike action="run": | ||
| 454 | 454 | fi |
| 455 | 455 | cd flutter |
| 456 | 456 | flutter pub get |
| 457 | + # The Nim core links OpenSSL for the TLS on :6697, and the process that | |
| 458 | + # dlopens it has to be able to find one. Prepended here rather than set in | |
| 459 | + # the shell, so nixGL's own loader path is left alone. | |
| 460 | + export LD_LIBRARY_PATH="${FRQ_OPENSSL_LIB:-}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" | |
| 457 | 461 | runner=() |
| 458 | 462 | [ -e /run/current-system ] || runner=("$NIXGL") |
| 459 | 463 | case "{{action}}" in |
| @@ -454,6 +454,10 @@ nim-spike action="run": | |||
| 454 | fi | 454 | fi |
| 455 | cd flutter | 455 | cd flutter |
| 456 | flutter pub get | 456 | flutter pub get |
| 457 | + # The Nim core links OpenSSL for the TLS on :6697, and the process that | ||
| 458 | + # dlopens it has to be able to find one. Prepended here rather than set in | ||
| 459 | + # the shell, so nixGL's own loader path is left alone. | ||
| 460 | + export LD_LIBRARY_PATH="${FRQ_OPENSSL_LIB:-}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" | ||
| 457 | runner=() | 461 | runner=() |
| 458 | [ -e /run/current-system ] || runner=("$NIXGL") | 462 | [ -e /run/current-system ] || runner=("$NIXGL") |
| 459 | case "{{action}}" in | 463 | case "{{action}}" in |
modified
nim/README.md +14 -1 | @@ -88,13 +88,26 @@ the chat screen caps the backlog at fifty rows for exactly this reason, and | ||
| 88 | 88 | nothing has measured what a real one costs. |
| 89 | 89 | |
| 90 | 90 | ```bash |
| 91 | -just nim-spike # the window | |
| 91 | +just nim-spike # the window; click Connect | |
| 92 | +FRQ_TRACE=1 FRQ_AUTOCONNECT=1 just nim-spike # ...connecting on its own | |
| 92 | 93 | just nim-spike-test # 7 widget tests: real taps, real widgets |
| 93 | 94 | just nim-live # connect to a real freeq and say a line |
| 94 | 95 | just nim-bench # what the boundary costs |
| 95 | 96 | FRQ_TRACE=1 just nim-live # ...and every line on the wire |
| 96 | 97 | ``` |
| 97 | 98 | |
| 99 | +Two switches, both for the same reason — a GUI on Wayland cannot be clicked | |
| 100 | +from a script, so without them the only way to check the window connects is to | |
| 101 | +sit in front of it. `FRQ_AUTOCONNECT=1` presses Connect on the first render and | |
| 102 | +`FRQ_NICK` overrides the nickname, because two runs with the same one collide | |
| 103 | +on the server and the second is refused. | |
| 104 | + | |
| 105 | +The GUI needs OpenSSL on its loader path, which the `flutter-desktop` shell | |
| 106 | +provides as `FRQ_OPENSSL_LIB` and the `nim-spike` recipe prepends for the app | |
| 107 | +alone. Not set as `LD_LIBRARY_PATH` in the shell itself: that shell also runs | |
| 108 | +Flutter through nixGL, which does its own careful things to the loader path, | |
| 109 | +and a blanket setting there breaks GL on some machines and not others. | |
| 110 | + | |
| 98 | 111 | ## Status |
| 99 | 112 | |
| 100 | 113 | `frq/ircparse.nim` is ported and tested — 29 cases, `just nim-test` — and the |
| @@ -88,13 +88,26 @@ the chat screen caps the backlog at fifty rows for exactly this reason, and | |||
| 88 | nothing has measured what a real one costs. | 88 | nothing has measured what a real one costs. |
| 89 | 89 | ||
| 90 | ```bash | 90 | ```bash |
| 91 | -just nim-spike # the window | 91 | +just nim-spike # the window; click Connect |
| 92 | +FRQ_TRACE=1 FRQ_AUTOCONNECT=1 just nim-spike # ...connecting on its own | ||
| 92 | just nim-spike-test # 7 widget tests: real taps, real widgets | 93 | just nim-spike-test # 7 widget tests: real taps, real widgets |
| 93 | just nim-live # connect to a real freeq and say a line | 94 | just nim-live # connect to a real freeq and say a line |
| 94 | just nim-bench # what the boundary costs | 95 | just nim-bench # what the boundary costs |
| 95 | FRQ_TRACE=1 just nim-live # ...and every line on the wire | 96 | FRQ_TRACE=1 just nim-live # ...and every line on the wire |
| 96 | ``` | 97 | ``` |
| 97 | 98 | ||
| 99 | +Two switches, both for the same reason — a GUI on Wayland cannot be clicked | ||
| 100 | +from a script, so without them the only way to check the window connects is to | ||
| 101 | +sit in front of it. `FRQ_AUTOCONNECT=1` presses Connect on the first render and | ||
| 102 | +`FRQ_NICK` overrides the nickname, because two runs with the same one collide | ||
| 103 | +on the server and the second is refused. | ||
| 104 | + | ||
| 105 | +The GUI needs OpenSSL on its loader path, which the `flutter-desktop` shell | ||
| 106 | +provides as `FRQ_OPENSSL_LIB` and the `nim-spike` recipe prepends for the app | ||
| 107 | +alone. Not set as `LD_LIBRARY_PATH` in the shell itself: that shell also runs | ||
| 108 | +Flutter through nixGL, which does its own careful things to the loader path, | ||
| 109 | +and a blanket setting there breaks GL on some machines and not others. | ||
| 110 | + | ||
| 98 | ## Status | 111 | ## Status |
| 99 | 112 | ||
| 100 | `frq/ircparse.nim` is ported and tested — 29 cases, `just nim-test` — and the | 113 | `frq/ircparse.nim` is ported and tested — 29 cases, `just nim-test` — and the |
modified
nim/src/frq/screens/chat.nim +1 -1 | @@ -39,5 +39,5 @@ func chatScreen*(s: State): Node = | ||
| 39 | 39 | n("vbox", %*{"spacing": 4}, rows))), |
| 40 | 40 | hbox(%*{"spacing": 8}, |
| 41 | 41 | entry("draft", s.draft, "Message " & s.channel, "draft.change", |
| 42 | - width = 460), | |
| 42 | + width = 460, onSubmit = "send"), | |
| 43 | 43 | button("Send", "send", "primary"))) |
| @@ -39,5 +39,5 @@ func chatScreen*(s: State): Node = | |||
| 39 | n("vbox", %*{"spacing": 4}, rows))), | 39 | n("vbox", %*{"spacing": 4}, rows))), |
| 40 | hbox(%*{"spacing": 8}, | 40 | hbox(%*{"spacing": 8}, |
| 41 | entry("draft", s.draft, "Message " & s.channel, "draft.change", | 41 | entry("draft", s.draft, "Message " & s.channel, "draft.change", |
| 42 | - width = 460), | 42 | + width = 460, onSubmit = "send"), |
| 43 | button("Send", "send", "primary"))) | 43 | button("Send", "send", "primary"))) |
modified
nim/src/frq/state.nim +25 -1 | @@ -11,7 +11,7 @@ | ||
| 11 | 11 | ## function of this record; an event is the only way it changes; nothing else |
| 12 | 12 | ## crosses the boundary. That is what lets the renderer stay dumb. |
| 13 | 13 | |
| 14 | -import std/[json, strutils] | |
| 14 | +import std/[json, os, strutils] | |
| 15 | 15 | import trace, ircparse, irc |
| 16 | 16 | |
| 17 | 17 | type |
| @@ -220,3 +220,27 @@ proc drain*() = | ||
| 220 | 220 | else: |
| 221 | 221 | # Everything else is the MOTD and friends — traced, not shown. |
| 222 | 222 | trace("irc.skip", p.command & " " & $p.params) |
| 223 | + | |
| 224 | + | |
| 225 | +# -------------------------------------------------------------- autoconnect | |
| 226 | +# | |
| 227 | +# `FRQ_AUTOCONNECT=1` presses Connect as soon as the first screen is asked | |
| 228 | +# for. In the same spirit as FRQ_TRACE and for the same reason: a GUI on | |
| 229 | +# Wayland cannot be clicked from a script, so without this the only way to | |
| 230 | +# check that the window connects is to sit in front of it. It also makes | |
| 231 | +# `just nim-spike` a one-command demo. | |
| 232 | +# | |
| 233 | +# `FRQ_NICK` overrides the nickname, because two runs with the same one | |
| 234 | +# collide on the server and the second is refused. | |
| 235 | + | |
| 236 | +var autoconnectDone = false | |
| 237 | + | |
| 238 | +proc maybeAutoconnect*() = | |
| 239 | + if autoconnectDone: return | |
| 240 | + autoconnectDone = true | |
| 241 | + let want = getEnv("FRQ_AUTOCONNECT") | |
| 242 | + if want.len == 0 or want == "0": return | |
| 243 | + let nick = getEnv("FRQ_NICK") | |
| 244 | + if nick.len > 0: app.formNick = nick | |
| 245 | + trace("auto", "FRQ_AUTOCONNECT set — connecting as " & app.formNick) | |
| 246 | + dispatch(%*{"id": "connect"}) | |
| @@ -11,7 +11,7 @@ | |||
| 11 | ## function of this record; an event is the only way it changes; nothing else | 11 | ## function of this record; an event is the only way it changes; nothing else |
| 12 | ## crosses the boundary. That is what lets the renderer stay dumb. | 12 | ## crosses the boundary. That is what lets the renderer stay dumb. |
| 13 | 13 | ||
| 14 | -import std/[json, strutils] | 14 | +import std/[json, os, strutils] |
| 15 | import trace, ircparse, irc | 15 | import trace, ircparse, irc |
| 16 | 16 | ||
| 17 | type | 17 | type |
| @@ -220,3 +220,27 @@ proc drain*() = | |||
| 220 | else: | 220 | else: |
| 221 | # Everything else is the MOTD and friends — traced, not shown. | 221 | # Everything else is the MOTD and friends — traced, not shown. |
| 222 | trace("irc.skip", p.command & " " & $p.params) | 222 | trace("irc.skip", p.command & " " & $p.params) |
| 223 | + | ||
| 224 | + | ||
| 225 | +# -------------------------------------------------------------- autoconnect | ||
| 226 | +# | ||
| 227 | +# `FRQ_AUTOCONNECT=1` presses Connect as soon as the first screen is asked | ||
| 228 | +# for. In the same spirit as FRQ_TRACE and for the same reason: a GUI on | ||
| 229 | +# Wayland cannot be clicked from a script, so without this the only way to | ||
| 230 | +# check that the window connects is to sit in front of it. It also makes | ||
| 231 | +# `just nim-spike` a one-command demo. | ||
| 232 | +# | ||
| 233 | +# `FRQ_NICK` overrides the nickname, because two runs with the same one | ||
| 234 | +# collide on the server and the second is refused. | ||
| 235 | + | ||
| 236 | +var autoconnectDone = false | ||
| 237 | + | ||
| 238 | +proc maybeAutoconnect*() = | ||
| 239 | + if autoconnectDone: return | ||
| 240 | + autoconnectDone = true | ||
| 241 | + let want = getEnv("FRQ_AUTOCONNECT") | ||
| 242 | + if want.len == 0 or want == "0": return | ||
| 243 | + let nick = getEnv("FRQ_NICK") | ||
| 244 | + if nick.len > 0: app.formNick = nick | ||
| 245 | + trace("auto", "FRQ_AUTOCONNECT set — connecting as " & app.formNick) | ||
| 246 | + dispatch(%*{"id": "connect"}) | ||
modified
nim/src/frq/ui.nim +5 -1 | @@ -69,7 +69,8 @@ func button*(text: string, onClick: string, kind = "default"): Node = | ||
| 69 | 69 | ## `onClick` is an event id, not a closure. See the module comment. |
| 70 | 70 | n("button", %*{"label": text, "kind": kind, "onClick": onClick}) |
| 71 | 71 | |
| 72 | -func entry*(key, text, placeholder, onChange: string, width = 0): Node = | |
| 72 | +func entry*(key, text, placeholder, onChange: string, width = 0, | |
| 73 | + onSubmit = ""): Node = | |
| 73 | 74 | ## Every entry carries a key, and for the reason the Clojure's comment |
| 74 | 75 | ## gives: a renderer that keeps a text controller per field needs a stable |
| 75 | 76 | ## name for it, and without one the host and the port shared a controller |
| @@ -77,6 +78,9 @@ func entry*(key, text, placeholder, onChange: string, width = 0): Node = | ||
| 77 | 78 | var p = %*{"key": key, "text": text, "placeholder": placeholder, |
| 78 | 79 | "onChange": onChange} |
| 79 | 80 | if width > 0: p["widthRequest"] = %width |
| 81 | + # Enter, where the field has something to do with it. A compose box that | |
| 82 | + # only sends on a button click is one nobody can type into at speed. | |
| 83 | + if onSubmit.len > 0: p["onSubmit"] = %onSubmit | |
| 80 | 84 | n("entry", p) |
| 81 | 85 | |
| 82 | 86 | func checkbutton*(text: string, active: bool, onToggled: string): Node = |
| @@ -69,7 +69,8 @@ func button*(text: string, onClick: string, kind = "default"): Node = | |||
| 69 | ## `onClick` is an event id, not a closure. See the module comment. | 69 | ## `onClick` is an event id, not a closure. See the module comment. |
| 70 | n("button", %*{"label": text, "kind": kind, "onClick": onClick}) | 70 | n("button", %*{"label": text, "kind": kind, "onClick": onClick}) |
| 71 | 71 | ||
| 72 | -func entry*(key, text, placeholder, onChange: string, width = 0): Node = | 72 | +func entry*(key, text, placeholder, onChange: string, width = 0, |
| 73 | + onSubmit = ""): Node = | ||
| 73 | ## Every entry carries a key, and for the reason the Clojure's comment | 74 | ## Every entry carries a key, and for the reason the Clojure's comment |
| 74 | ## gives: a renderer that keeps a text controller per field needs a stable | 75 | ## gives: a renderer that keeps a text controller per field needs a stable |
| 75 | ## name for it, and without one the host and the port shared a controller | 76 | ## name for it, and without one the host and the port shared a controller |
| @@ -77,6 +78,9 @@ func entry*(key, text, placeholder, onChange: string, width = 0): Node = | |||
| 77 | var p = %*{"key": key, "text": text, "placeholder": placeholder, | 78 | var p = %*{"key": key, "text": text, "placeholder": placeholder, |
| 78 | "onChange": onChange} | 79 | "onChange": onChange} |
| 79 | if width > 0: p["widthRequest"] = %width | 80 | if width > 0: p["widthRequest"] = %width |
| 81 | + # Enter, where the field has something to do with it. A compose box that | ||
| 82 | + # only sends on a button click is one nobody can type into at speed. | ||
| 83 | + if onSubmit.len > 0: p["onSubmit"] = %onSubmit | ||
| 80 | n("entry", p) | 84 | n("entry", p) |
| 81 | 85 | ||
| 82 | func checkbutton*(text: string, active: bool, onToggled: string): Node = | 86 | func checkbutton*(text: string, active: bool, onToggled: string): Node = |
modified
nim/src/frq_core.nim +1 -0 | @@ -104,6 +104,7 @@ proc frq_irc_nick_of*(prefix: cstring): cstring {.exportc, dynlib.} = | ||
| 104 | 104 | # coming back. See `frq/ui.nim`. |
| 105 | 105 | |
| 106 | 106 | proc currentTree(): string = |
| 107 | + maybeAutoconnect() | |
| 107 | 108 | ## Whichever screen the state says. `drain` first, so the tree Dart gets is |
| 108 | 109 | ## built after every line that had arrived when it asked — that is the whole |
| 109 | 110 | ## of the polling model, and it is why there is no callback into Dart. |
| @@ -104,6 +104,7 @@ proc frq_irc_nick_of*(prefix: cstring): cstring {.exportc, dynlib.} = | |||
| 104 | # coming back. See `frq/ui.nim`. | 104 | # coming back. See `frq/ui.nim`. |
| 105 | 105 | ||
| 106 | proc currentTree(): string = | 106 | proc currentTree(): string = |
| 107 | + maybeAutoconnect() | ||
| 107 | ## Whichever screen the state says. `drain` first, so the tree Dart gets is | 108 | ## Whichever screen the state says. `drain` first, so the tree Dart gets is |
| 108 | ## built after every line that had arrived when it asked — that is the whole | 109 | ## built after every line that had arrived when it asked — that is the whole |
| 109 | ## of the polling model, and it is why there is no callback into Dart. | 110 | ## of the polling model, and it is why there is no callback into Dart. |