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

frq_core.dart · 202 lines · 7.6 KBDart Blame HistoryRaw
The binding is Dart, and it works f7aea3b nandi yesterday1/// The Nim core, as Dart functions.
2///
A web version, from the same core 23846db nandi 12h ago3/// The logic lives in Nim; this is the shape it takes on this side. Two hosts
4/// answer it — `src/host_ffi.dart` through `dart:ffi` on a desktop, and
5/// `src/host_js.dart` against the `nim js` build in a browser — and
6/// `src/host.dart` is the one line that chooses. Nothing below this comment
7/// knows which, which is the point: the widget tree, its decoding, and the
8/// change detection that makes polling cheap are the same work whatever
9/// produced the JSON.
The binding is Dart, and it works f7aea3b nandi yesterday10///
11/// **Dart and not ClojureDart, on purpose.** The point of the Nim core is to
12/// have less Clojure, so new code on this side of the boundary is written in
A web version, from the same core 23846db nandi 12h ago13/// the language the platform speaks.
The binding is Dart, and it works f7aea3b nandi yesterday14///
15/// No `package:ffi` either. That package exists mostly for `Utf8`
A web version, from the same core 23846db nandi 12h ago16/// conversions, and doing them against `dart:convert` costs about ten lines
17/// and keeps `pubspec.yaml` unchanged.
The binding is Dart, and it works f7aea3b nandi yesterday18library;
19
20import 'dart:convert';
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi yesterday21
A web version, from the same core 23846db nandi 12h ago22import 'src/host.dart' as host;
The binding is Dart, and it works f7aea3b nandi yesterday23
24// ------------------------------------------------------------------ public
25
26/// The core's version, for a caller that wants to check the library it found
A web version, from the same core 23846db nandi 12h ago27/// is the one it was built against.
28String get version => host.hostVersion();
The binding is Dart, and it works f7aea3b nandi yesterday29
30/// An IRC line, taken apart: `{raw, tags, account, prefix, command, params}`.
31///
32/// `tags`, `account` and `prefix` are null where the line carried none, which
33/// is the distinction `frq.irc.parse` draws with nil and every caller depends
34/// on — a PRIVMSG from a server with no prefix is not the same line as one
35/// from a nick.
36Map<String, dynamic> parseLine(String line) {
A web version, from the same core 23846db nandi 12h ago37 final json = host.str1('frq_irc_parse_line', line);
The binding is Dart, and it works f7aea3b nandi yesterday38 return jsonDecode(json ?? 'null') as Map<String, dynamic>;
39}
40
41/// One IRCv3 tag's value, unescaped — null where the tag is absent OR empty,
42/// which IRCv3 says are the same thing.
43String? tagValue(String tags, String key) {
A web version, from the same core 23846db nandi 12h ago44 return host.tagValueOf(tags, key);
The binding is Dart, and it works f7aea3b nandi yesterday45}
46
A web version, from the same core 23846db nandi 12h ago47String unescapeTag(String v) => host.str1('frq_irc_unescape_tag', v) ?? '';
The binding is Dart, and it works f7aea3b nandi yesterday48
A web version, from the same core 23846db nandi 12h ago49String escapeTagValue(String v) => host.str1('frq_irc_escape_tag_value', v) ?? '';
The binding is Dart, and it works f7aea3b nandi yesterday50
51/// The nick half of a `nick!user@host` prefix.
A web version, from the same core 23846db nandi 12h ago52String nickOf(String prefix) => host.str1('frq_irc_nick_of', prefix) ?? '';
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday53
54
Nim under the existing UI, not instead of it 56551a8 nandi yesterday55/// Log through the Nim core's trace facility, so `FRQ_TRACE=1` gives one
56/// interleaved story rather than two half-ones in different places.
57void trace(String topic, String msg) {
A web version, from the same core 23846db nandi 12h ago58 host.traceTo(topic, msg);
Nim under the existing UI, not instead of it 56551a8 nandi yesterday59}
60
61// --------------------------------------------------------------- transport
62//
63// `frq.net`'s three operations, with a Nim socket behind them. This is the
64// wiring that leaves the existing ClojureDart screens, cells and actions
65// alone: only the transport underneath them is Nim.
66
67
68/// Dial. Non-blocking: the socket runs on a Nim thread and progress arrives
69/// through [connEvent].
A web version, from the same core 23846db nandi 12h ago70void connOpen(String hostname, int port, {bool tls = true}) =>
71 host.connOpenAt(hostname, port, tls);
72
Nim under the existing UI, not instead of it 56551a8 nandi yesterday73
74/// Queue a line. The transport adds the CRLF.
A web version, from the same core 23846db nandi 12h ago75void connSend(String line) => host.connSendLine(line);
76
Nim under the existing UI, not instead of it 56551a8 nandi yesterday77
A web version, from the same core 23846db nandi 12h ago78void connClose() => host.connCloseNow();
Nim under the existing UI, not instead of it 56551a8 nandi yesterday79
80/// The next line, or null when none is waiting. Never blocks.
A web version, from the same core 23846db nandi 12h ago81String? connRecv() => host.connRecvLine();
Nim under the existing UI, not instead of it 56551a8 nandi yesterday82
83/// The next transport event — `open`, `close: …`, `error: …` — or null.
A web version, from the same core 23846db nandi 12h ago84String? connEvent() => host.connEventNext();
The renderer, and a transport bug that took four tries 58e6c97 nandi yesterday85
86
87// ---------------------------------------------------------------- the UI
88//
89// Nim owns the state and the screens; Dart owns the pixels. A tree goes out,
90// an event id comes back, and nothing else crosses.
91//
92// `UiNode` is deliberately a dumb bag — a tag, a props map, children. A class
93// per widget would put the tag vocabulary in two places and make every new tag
94// a change on both sides; the point is that Nim can grow a screen without this
95// file being touched.
96
97/// One node of the widget tree Nim emitted.
98class UiNode {
99 final String tag;
100 final Map<String, dynamic> props;
101 final List<UiNode> children;
102
103 const UiNode(this.tag, this.props, this.children);
104
105 factory UiNode.fromJson(Map<String, dynamic> j) => UiNode(
106 j['tag'] as String,
107 (j['props'] as Map?)?.cast<String, dynamic>() ?? const {},
108 ((j['children'] as List?) ?? const [])
109 .map((c) => UiNode.fromJson((c as Map).cast<String, dynamic>()))
110 .toList(growable: false),
111 );
112
113 /// A prop, or [fallback] when it is absent or the wrong shape. Tolerant on
114 /// purpose: a renderer should skip a prop it does not understand rather than
115 /// fail a whole screen over one.
116 T prop<T>(String name, T fallback) {
117 final v = props[name];
118 return v is T ? v : fallback;
119 }
120
121 /// Structural, and that matters: the poll loop compares two trees by this
122 /// string to decide whether to rebuild. A summary showing only tags and prop
123 /// NAMES would call two screens equal when a message had arrived, and the
124 /// room would never appear to fill.
125 @override
126 String toString() =>
127 '<$tag $props ${children.map((c) => c.toString()).join()}>';
128}
129
130UiNode _treeFrom(String? json) =>
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi yesterday131 UiNode.fromJson(jsonDecode(json ?? _emptyTree) as Map<String, dynamic>);
132
133/// A tree and the JSON it came from.
134///
135/// The raw string is kept because it is the cheapest possible change
136/// detector: Nim already produced it, and comparing two strings is free
137/// beside decoding one. The renderer polls ten times a second and the answer
138/// is almost always "nothing changed" — doing a `jsonDecode` and two
139/// recursive `toString()`s to discover that was most of the idle cost of the
140/// app in a busy room.
141const _emptyTree = '{"tag":"vbox"}';
142
143class UiFrame {
144 final String json;
145 final UiNode tree;
146 const UiFrame(this.json, this.tree);
147}
The renderer, and a transport bug that took four tries 58e6c97 nandi yesterday148
149/// The current screen.
150///
151/// Not pure: the Nim side drains the socket's queue first, so two calls with
152/// no [dispatch] between can differ when a line arrived in the gap. That is how
153/// the room fills, and why the renderer polls.
154UiNode render() => _treeFrom(
A web version, from the same core 23846db nandi 12h ago155 host.uiRender());
The renderer, and a transport bug that took four tries 58e6c97 nandi yesterday156
157/// The tree, asked for because time passed rather than because anything
158/// happened. Same work as [render]; named for what the caller means.
159UiNode poll() => _treeFrom(
A web version, from the same core 23846db nandi 12h ago160 host.uiPoll());
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi yesterday161
162/// The current screen, with the JSON it came from. The starting point for
163/// [pollIfChanged].
164UiFrame renderFrame() {
A web version, from the same core 23846db nandi 12h ago165 final json = host.uiRender() ?? _emptyTree;
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi yesterday166 return UiFrame(json, _treeFrom(json));
167}
168
169/// The tree, decoded only when it differs from [since] — otherwise null,
170/// meaning "the screen you already have is current".
171///
172/// This is what the renderer polls with. The comparison is the JSON Nim
173/// already produced, so an unchanged frame costs one string compare rather
174/// than a decode and two recursive `toString()`s.
175UiFrame? pollIfChanged(String since) {
A web version, from the same core 23846db nandi 12h ago176 final json = host.uiPoll() ?? _emptyTree;
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi yesterday177 if (json == since) return null;
178 return UiFrame(json, _treeFrom(json));
179}
The renderer, and a transport bug that took four tries 58e6c97 nandi yesterday180
181/// Apply an event and get the tree it produced.
182///
183/// One call rather than dispatch-then-render, and not to save a crossing: it
184/// makes the pair atomic, so there is no window in which Dart could render a
185/// state nothing asked for.
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi yesterday186UiNode dispatch(String id, [String value = '']) => dispatchFrame(id, value).tree;
187
188/// As [dispatch], but keeping the JSON so the poll loop can compare against
189/// it without re-stringifying the tree it just built.
190UiFrame dispatchFrame(String id, [String value = '']) {
A web version, from the same core 23846db nandi 12h ago191 final json = host.uiDispatch(jsonEncode({'id': id, 'value': value})) ??
192 _emptyTree;
193 return UiFrame(json, _treeFrom(json));
The renderer, and a transport bug that took four tries 58e6c97 nandi yesterday194}
195
A web version, from the same core 23846db nandi 12h ago196
Lay every screen out in a test, and fix what that found 36bdfc5 nandi yesterday197/// Fill a room with a representative conversation, so a test can lay the chat
198/// screen out without a server. See the Nim side for why it exists.
A web version, from the same core 23846db nandi 12h ago199void demoUi() => host.uiDemo();
Lay every screen out in a test, and fix what that found 36bdfc5 nandi yesterday200
The renderer, and a transport bug that took four tries 58e6c97 nandi yesterday201/// Back to a fresh state, for a caller that wants a known starting point.
A web version, from the same core 23846db nandi 12h ago202void resetUi() => host.uiReset();