nandi/frqpublic Fork 0
7b594b1
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 frq with glimmer-cosmic, and give the tabs one shape

`just cosmic` runs the same screens on jolt-native's libcosmic backend, from
a checkout named by FRQ_JOLT_NATIVE with libjoltcosmic built in its own nix
shell. frq.cosmic installs the backend last, reports the window's size so
the columns are laid out against it, and FRQ_COSMIC_DUMP writes the tree to a
file for looking at what was rendered.

What frq asked of glimmer-jvui directly — its timers, quit, and the picture
chooser — goes through frq.platform now, which a backend with a loop of its
own can override. Under libcosmic the image button opens the desktop's own
chooser.

Discover and Settings take the chats screen's shape, so switching tabs moves
only the middle. The people panel is narrower with its names in line, and a
conversation's preview is one line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-11T03:21:42-07:00 Browse files
7b594b1 parent: d740915
modified justfile +40 -0
@@ -163,6 +163,46 @@ tui *args:
163163
164164 exec jolt -Sdeps "$deps" -m frq.tui "$@"
165165
166+# `run` with glimmer-cosmic under it, libcosmic doing the painting. There is
167+# no pin to fall back on: the flake's jolt-native builds no libjoltcosmic, so
168+# this needs a jolt-native checkout with one built, named by FRQ_JOLT_NATIVE
169+# or found beside this tree the way the shell always looks.
170+#
171+# nix develop --command cargo build --release -p jolt-cosmic in that checkout
172+#
173+# Inside jolt-native's shell and not with the host's cargo: jolt runs on the
174+# store's glibc, and an object linked against a newer host one (Arch's, here)
175+# asks for symbol versions that glibc does not have — dlopen refuses it, and
176+# jolt reports that as the library not being found at all.
177+#
178+# nixGL for the same reason as `run`: libcosmic paints through wgpu.
179+#
180+# The same screens, painted by libcosmic. A spike: most tags paint as columns.
181+cosmic *args:
182+ #!/usr/bin/env bash
183+ set -euo pipefail
184+ cd "{{justfile_directory()}}"
185+ if [ -z "${JOLT_NATIVE_LIB:-}" ]; then
186+ exec {{nix}} develop . --max-jobs {{jobs}} --command just cosmic "$@"
187+ fi
188+
189+ if [ -z "${FRQ_JOLT_NATIVE:-}" ] || [ ! -e "$FRQ_JOLT_NATIVE/target/release/libjoltcosmic.so" ]; then
190+ echo "frq: no libjoltcosmic.so in ${FRQ_JOLT_NATIVE:-the pin} — nix develop --command cargo build --release -p jolt-cosmic in a jolt-native checkout, then FRQ_JOLT_NATIVE=<it> just cosmic" >&2
191+ exit 1
192+ fi
193+
194+ deps="{:deps {jolt-lang/glimmer {:local/root \"$GLIMMER_SRC\"}"
195+ deps="$deps nandi/glimmer-jvui {:local/root \"$GLIMMER_JVUI_SRC\"}"
196+ deps="$deps jvui/jvui {:local/root \"$JVUI_SRC\"}"
197+ deps="$deps nandi/glimmer-cosmic {:local/root \"$FRQ_JOLT_NATIVE/glimmer-backends/glimmer-cosmic\"}}}"
198+
199+ export LD_LIBRARY_PATH="$JOLT_NATIVE_LIB:$FRQ_LIB_PATH${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
200+
201+ runner=()
202+ [ -e /run/current-system ] || runner=("$NIXGL")
203+
204+ exec "${runner[@]}" jolt -Sdeps "$deps" -m frq.cosmic "$@"
205+
166206 # The same classpath as `tui`, with an nREPL on it instead of a `-main`: a
167207 # session that can require `frq.tui` and then redefine a component while it is
168208 # on screen, which is a second and not the minute a rebuild costs.
@@ -163,6 +163,46 @@ tui *args:
163 163
164 exec jolt -Sdeps "$deps" -m frq.tui "$@"164 exec jolt -Sdeps "$deps" -m frq.tui "$@"
165 165
166+# `run` with glimmer-cosmic under it, libcosmic doing the painting. There is
167+# no pin to fall back on: the flake's jolt-native builds no libjoltcosmic, so
168+# this needs a jolt-native checkout with one built, named by FRQ_JOLT_NATIVE
169+# or found beside this tree the way the shell always looks.
170+#
171+# nix develop --command cargo build --release -p jolt-cosmic in that checkout
172+#
173+# Inside jolt-native's shell and not with the host's cargo: jolt runs on the
174+# store's glibc, and an object linked against a newer host one (Arch's, here)
175+# asks for symbol versions that glibc does not have — dlopen refuses it, and
176+# jolt reports that as the library not being found at all.
177+#
178+# nixGL for the same reason as `run`: libcosmic paints through wgpu.
179+#
180+# The same screens, painted by libcosmic. A spike: most tags paint as columns.
181+cosmic *args:
182+ #!/usr/bin/env bash
183+ set -euo pipefail
184+ cd "{{justfile_directory()}}"
185+ if [ -z "${JOLT_NATIVE_LIB:-}" ]; then
186+ exec {{nix}} develop . --max-jobs {{jobs}} --command just cosmic "$@"
187+ fi
188+
189+ if [ -z "${FRQ_JOLT_NATIVE:-}" ] || [ ! -e "$FRQ_JOLT_NATIVE/target/release/libjoltcosmic.so" ]; then
190+ echo "frq: no libjoltcosmic.so in ${FRQ_JOLT_NATIVE:-the pin} — nix develop --command cargo build --release -p jolt-cosmic in a jolt-native checkout, then FRQ_JOLT_NATIVE=<it> just cosmic" >&2
191+ exit 1
192+ fi
193+
194+ deps="{:deps {jolt-lang/glimmer {:local/root \"$GLIMMER_SRC\"}"
195+ deps="$deps nandi/glimmer-jvui {:local/root \"$GLIMMER_JVUI_SRC\"}"
196+ deps="$deps jvui/jvui {:local/root \"$JVUI_SRC\"}"
197+ deps="$deps nandi/glimmer-cosmic {:local/root \"$FRQ_JOLT_NATIVE/glimmer-backends/glimmer-cosmic\"}}}"
198+
199+ export LD_LIBRARY_PATH="$JOLT_NATIVE_LIB:$FRQ_LIB_PATH${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
200+
201+ runner=()
202+ [ -e /run/current-system ] || runner=("$NIXGL")
203+
204+ exec "${runner[@]}" jolt -Sdeps "$deps" -m frq.cosmic "$@"
205+
166 # The same classpath as `tui`, with an nREPL on it instead of a `-main`: a206 # The same classpath as `tui`, with an nREPL on it instead of a `-main`: a
167 # session that can require `frq.tui` and then redefine a component while it is207 # session that can require `frq.tui` and then redefine a component while it is
168 # on screen, which is a second and not the minute a rebuild costs.208 # on screen, which is a second and not the minute a rebuild costs.
modified src/frq/app.clj +42 -15
@@ -217,6 +217,16 @@
217217 [:button {:label "Close"
218218 :on-click #(s/leave-channel! name)}])
219219
220+(defn- preview-line
221+ "The last line of a conversation, as one line: a pasted shell script or a
222+ long link is a card's worth of text otherwise, and the cards stop reading as
223+ a list of rooms."
224+ [text]
225+ (let [line (str/replace (str text) #"\s+" " ")]
226+ (if (> (count line) 90)
227+ (str (subs line 0 89) "…")
228+ line)))
229+
220230 (defn conversation-row [buffer]
221231 (let [name (:name buffer)
222232 ;; Defaulted rather than assumed. Everything that builds a buffer
@@ -257,7 +267,7 @@
257267 ;; neither — a room with your name in it always has a line to count.
258268 (when (pos? unread)
259269 [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])]
260- [:dim-label {:label (s/last-preview buffer)}]
270+ [:dim-label {:label (preview-line (s/last-preview buffer))}]
261271 (when-not @terminal?
262272 [:hbox {:spacing 8 :wrap false}
263273 [open-button name]
@@ -521,11 +531,11 @@
521531 (reset! s/highlight id)
522532 ;; Off again once the frame that scrolled has
523533 ;; been painted, so the reader keeps the view.
524- (gui/after! 120 (fn [] (reset! s/jump-to nil)))
534+ (platform/after! 120 (fn [] (reset! s/jump-to nil)))
525535 ;; The highlight stays long enough to be read,
526536 ;; and only clears itself: a later jump elsewhere
527537 ;; owns the highlight from then on.
528- (gui/after! 2000
538+ (platform/after! 2000
529539 (fn []
530540 (when (= id @s/highlight)
531541 (reset! s/highlight nil)))))}]
@@ -1258,7 +1268,7 @@
12581268 ;; be told to leave. A column with `:fill-height` and no width takes the whole
12591269 ;; row — so the panel is only ever on screen if the message list is given a
12601270 ;; width that stops short of it.
1261-(def ^:private users-width 180)
1271+(def ^:private users-width 150)
12621272
12631273 ;; How many lines the compose field is currently drawn as. The window backend
12641274 ;; says so when it changes — a field with room to grow wraps a long message
@@ -1324,7 +1334,10 @@
13241334 ;; The name is a button because a person is somewhere to go: pressing one
13251335 ;; opens a conversation with them.
13261336 [:hbox {:key nick :spacing 6 :wrap false}
1327- [:dim-label {:label (if (seq prefix) prefix " ")}]
1337+ ;; In a slot of its own width, so an "@" and a space take the same room and
1338+ ;; the names line up whatever mode is in front of them.
1339+ [:vbox {:key :mode :width-request 14}
1340+ [:dim-label {:label (if (seq prefix) prefix " ")}]]
13281341 [:button {:label nick :on-click #(s/open-dm! nick)}]])
13291342
13301343 (defn users-panel
@@ -1618,9 +1631,28 @@
16181631
16191632 ;; ---------------------------------------------------------------- discover
16201633
1634+(defn- tab-screen
1635+ "One of the three screens the tab bar moves between, in the chats screen's
1636+ shape: the title at the top, the tabs pinned at the bottom, and `body`
1637+ scrolling between them.
1638+
1639+ The same shape for all three, so switching tabs moves nothing but the
1640+ middle. As pages they were centred columns of their own widths with the tabs
1641+ wherever the content happened to end, and every switch resized the screen
1642+ under the pointer."
1643+ [title scroll-key & body]
1644+ [:vbox {:spacing 8 :margin 12 :fill-height true}
1645+ [:title {:label title}]
1646+ [:vbox {:key :list :fill-height true}
1647+ (into [:scroll {:scroll-key scroll-key :orientation :vertical
1648+ :reserve (below-list) :spacing 8}]
1649+ body)]
1650+ [:vbox {:key :foot :spacing 8}
1651+ [:separator {}]
1652+ [tab-bar]]])
1653+
16211654 (defn discover-screen []
1622- [:page {:max-width 620}
1623- [:title {:label "Discover"}]
1655+ (tab-screen "Discover" "discover-list"
16241656 [:dim-label {:label "Popular channels on freeq."}]
16251657 [error-note]
16261658 (for [[name blurb] s/popular-channels]
@@ -1630,15 +1662,12 @@
16301662 [:dim-label {:label blurb}]
16311663 [:button {:label (if joined? "Open" "Join")
16321664 :kind :primary
1633- :on-click #(if joined? (s/open-channel! name) (s/join! name))}]]))
1634- [:separator {}]
1635- [tab-bar]])
1665+ :on-click #(if joined? (s/open-channel! name) (s/join! name))}]]))))
16361666
16371667 ;; ---------------------------------------------------------------- settings
16381668
16391669 (defn settings-screen []
1640- [:page {:max-width 520}
1641- [:title {:label "Settings"}]
1670+ (tab-screen "Settings" "settings-list"
16421671 [:card {}
16431672 [:title-2 {:label "Connection"}]
16441673 [:status {:label @s/status :live (s/connected?)}]
@@ -1671,9 +1700,7 @@
16711700 [:card {}
16721701 [:title-2 {:label "frq"}]
16731702 [:dim-label {:label "freeq client in jolt — glimmer components on the Vidya/egui backend."}]
1674- [:button {:label "Quit" :on-click gui/quit!}]]
1675- [:separator {}]
1676- [tab-bar]])
1703+ [:button {:label "Quit" :on-click platform/quit!}]]))
16771704
16781705 ;; ---------------------------------------------------------------- shell
16791706
@@ -217,6 +217,16 @@
217 [:button {:label "Close"217 [:button {:label "Close"
218 :on-click #(s/leave-channel! name)}])218 :on-click #(s/leave-channel! name)}])
219 219
220+(defn- preview-line
221+ "The last line of a conversation, as one line: a pasted shell script or a
222+ long link is a card's worth of text otherwise, and the cards stop reading as
223+ a list of rooms."
224+ [text]
225+ (let [line (str/replace (str text) #"\s+" " ")]
226+ (if (> (count line) 90)
227+ (str (subs line 0 89) "…")
228+ line)))
229+
220 (defn conversation-row [buffer]230 (defn conversation-row [buffer]
221 (let [name (:name buffer)231 (let [name (:name buffer)
222 ;; Defaulted rather than assumed. Everything that builds a buffer232 ;; Defaulted rather than assumed. Everything that builds a buffer
@@ -257,7 +267,7 @@
257 ;; neither — a room with your name in it always has a line to count.267 ;; neither — a room with your name in it always has a line to count.
258 (when (pos? unread)268 (when (pos? unread)
259 [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])]269 [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])]
260- [:dim-label {:label (s/last-preview buffer)}]270+ [:dim-label {:label (preview-line (s/last-preview buffer))}]
261 (when-not @terminal?271 (when-not @terminal?
262 [:hbox {:spacing 8 :wrap false}272 [:hbox {:spacing 8 :wrap false}
263 [open-button name]273 [open-button name]
@@ -521,11 +531,11 @@
521 (reset! s/highlight id)531 (reset! s/highlight id)
522 ;; Off again once the frame that scrolled has532 ;; Off again once the frame that scrolled has
523 ;; been painted, so the reader keeps the view.533 ;; been painted, so the reader keeps the view.
524- (gui/after! 120 (fn [] (reset! s/jump-to nil)))534+ (platform/after! 120 (fn [] (reset! s/jump-to nil)))
525 ;; The highlight stays long enough to be read,535 ;; The highlight stays long enough to be read,
526 ;; and only clears itself: a later jump elsewhere536 ;; and only clears itself: a later jump elsewhere
527 ;; owns the highlight from then on.537 ;; owns the highlight from then on.
528- (gui/after! 2000538+ (platform/after! 2000
529 (fn []539 (fn []
530 (when (= id @s/highlight)540 (when (= id @s/highlight)
531 (reset! s/highlight nil)))))}]541 (reset! s/highlight nil)))))}]
@@ -1258,7 +1268,7 @@
1258 ;; be told to leave. A column with `:fill-height` and no width takes the whole1268 ;; be told to leave. A column with `:fill-height` and no width takes the whole
1259 ;; row — so the panel is only ever on screen if the message list is given a1269 ;; row — so the panel is only ever on screen if the message list is given a
1260 ;; width that stops short of it.1270 ;; width that stops short of it.
1261-(def ^:private users-width 180)1271+(def ^:private users-width 150)
1262 1272
1263 ;; How many lines the compose field is currently drawn as. The window backend1273 ;; How many lines the compose field is currently drawn as. The window backend
1264 ;; says so when it changes — a field with room to grow wraps a long message1274 ;; says so when it changes — a field with room to grow wraps a long message
@@ -1324,7 +1334,10 @@
1324 ;; The name is a button because a person is somewhere to go: pressing one1334 ;; The name is a button because a person is somewhere to go: pressing one
1325 ;; opens a conversation with them.1335 ;; opens a conversation with them.
1326 [:hbox {:key nick :spacing 6 :wrap false}1336 [:hbox {:key nick :spacing 6 :wrap false}
1327- [:dim-label {:label (if (seq prefix) prefix " ")}]1337+ ;; In a slot of its own width, so an "@" and a space take the same room and
1338+ ;; the names line up whatever mode is in front of them.
1339+ [:vbox {:key :mode :width-request 14}
1340+ [:dim-label {:label (if (seq prefix) prefix " ")}]]
1328 [:button {:label nick :on-click #(s/open-dm! nick)}]])1341 [:button {:label nick :on-click #(s/open-dm! nick)}]])
1329 1342
1330 (defn users-panel1343 (defn users-panel
@@ -1618,9 +1631,28 @@
1618 1631
1619 ;; ---------------------------------------------------------------- discover1632 ;; ---------------------------------------------------------------- discover
1620 1633
1634+(defn- tab-screen
1635+ "One of the three screens the tab bar moves between, in the chats screen's
1636+ shape: the title at the top, the tabs pinned at the bottom, and `body`
1637+ scrolling between them.
1638+
1639+ The same shape for all three, so switching tabs moves nothing but the
1640+ middle. As pages they were centred columns of their own widths with the tabs
1641+ wherever the content happened to end, and every switch resized the screen
1642+ under the pointer."
1643+ [title scroll-key & body]
1644+ [:vbox {:spacing 8 :margin 12 :fill-height true}
1645+ [:title {:label title}]
1646+ [:vbox {:key :list :fill-height true}
1647+ (into [:scroll {:scroll-key scroll-key :orientation :vertical
1648+ :reserve (below-list) :spacing 8}]
1649+ body)]
1650+ [:vbox {:key :foot :spacing 8}
1651+ [:separator {}]
1652+ [tab-bar]]])
1653+
1621 (defn discover-screen []1654 (defn discover-screen []
1622- [:page {:max-width 620}1655+ (tab-screen "Discover" "discover-list"
1623- [:title {:label "Discover"}]
1624 [:dim-label {:label "Popular channels on freeq."}]1656 [:dim-label {:label "Popular channels on freeq."}]
1625 [error-note]1657 [error-note]
1626 (for [[name blurb] s/popular-channels]1658 (for [[name blurb] s/popular-channels]
@@ -1630,15 +1662,12 @@
1630 [:dim-label {:label blurb}]1662 [:dim-label {:label blurb}]
1631 [:button {:label (if joined? "Open" "Join")1663 [:button {:label (if joined? "Open" "Join")
1632 :kind :primary1664 :kind :primary
1633- :on-click #(if joined? (s/open-channel! name) (s/join! name))}]]))1665+ :on-click #(if joined? (s/open-channel! name) (s/join! name))}]]))))
1634- [:separator {}]
1635- [tab-bar]])
1636 1666
1637 ;; ---------------------------------------------------------------- settings1667 ;; ---------------------------------------------------------------- settings
1638 1668
1639 (defn settings-screen []1669 (defn settings-screen []
1640- [:page {:max-width 520}1670+ (tab-screen "Settings" "settings-list"
1641- [:title {:label "Settings"}]
1642 [:card {}1671 [:card {}
1643 [:title-2 {:label "Connection"}]1672 [:title-2 {:label "Connection"}]
1644 [:status {:label @s/status :live (s/connected?)}]1673 [:status {:label @s/status :live (s/connected?)}]
@@ -1671,9 +1700,7 @@
1671 [:card {}1700 [:card {}
1672 [:title-2 {:label "frq"}]1701 [:title-2 {:label "frq"}]
1673 [:dim-label {:label "freeq client in jolt — glimmer components on the Vidya/egui backend."}]1702 [:dim-label {:label "freeq client in jolt — glimmer components on the Vidya/egui backend."}]
1674- [:button {:label "Quit" :on-click gui/quit!}]]1703+ [:button {:label "Quit" :on-click platform/quit!}]]))
1675- [:separator {}]
1676- [tab-bar]])
1677 1704
1678 ;; ---------------------------------------------------------------- shell1705 ;; ---------------------------------------------------------------- shell
1679 1706
added src/frq/cosmic.clj +60 -0
new file mode 100644
@@ -0,0 +1,60 @@
1+(ns frq.cosmic
2+ "frq's own screens, painted by libcosmic.
3+
4+ The same trick as `frq.tui`: the components in `frq.app` do not know what is
5+ under the reconciler, so requiring `glimmer-cosmic.core` after `frq.app`
6+ makes it the backend glimmer renders with. Nothing in `frq.app` changes.
7+
8+ glimmer-cosmic is a spike, and it shows. It paints a short list of tags and
9+ treats every other one as a column, so most of frq comes out as stacked text
10+ and buttons rather than its real layout. The jvui calls frq makes elsewhere
11+ (a picture chooser, call frames) have no window of their own to act on and do
12+ nothing, the same way they do in the terminal.
13+
14+ The one real difference from the other two: libcosmic takes the main thread
15+ and keeps it, so glimmer's loop runs on a worker. The timers handed to
16+ `start!` are glimmer-cosmic's, which run on that worker.
17+
18+ just cosmic"
19+ (:require [frq.app :as app]
20+ [frq.platform :as platform]
21+ [frq.state :as s]
22+ [glimmer.core :as ui]
23+ [glimmer-cosmic.ffi :as cosmic-ffi]
24+ ;; last, so its install! is the one that stands
25+ [glimmer-cosmic.core :as cosmic]))
26+
27+(defn- dump-tree-to!
28+ "Write the tree as the reconciler left it to `path` every few seconds.
29+
30+ There is no REPL into a libcosmic window — it owns the main thread — so this
31+ is how to tell a row that is not in the tree from a row that is in it and not
32+ painted. Off unless FRQ_COSMIC_DUMP names a file."
33+ [path]
34+ (cosmic/every! 3000 #(spit path (cosmic/dump-str))))
35+
36+(defn -main [& _]
37+ ;; What frq asks of glimmer-jvui directly, answered by this backend instead:
38+ ;; jvui's timers only run inside jvui's loop, its quit closes jvui's window,
39+ ;; and it has no picture chooser on a desktop.
40+ (platform/override! {:after! cosmic/after!
41+ :quit! cosmic/quit!
42+ :pick-image! cosmic/pick-image!
43+ :picked-image! cosmic/picked-image!})
44+ (when-let [path (not-empty (System/getenv "FRQ_COSMIC_DUMP"))]
45+ (dump-tree-to! path))
46+ (app/start! {:after! cosmic/after!
47+ :every! cosmic/every!
48+ :title! cosmic-ffi/set-title!
49+ ;; The window's size, which frq lays its columns out against —
50+ ;; the message list is told how much room the people panel
51+ ;; leaves it.
52+ :measure! (fn []
53+ (let [[w h] (cosmic/window-size)]
54+ (when (and (pos? w) (not= w @s/window-width))
55+ (reset! s/window-width w))
56+ (when (and (pos? h) (not= h @s/window-height))
57+ (reset! s/window-height h))))
58+ ;; Call frames are painted into a jvui texture, which is not here.
59+ :av? false})
60+ (ui/run app/app :title "frq" :width 520 :height 860))
new file mode 100644
@@ -0,0 +1,60 @@
1+(ns frq.cosmic
2+ "frq's own screens, painted by libcosmic.
3+
4+ The same trick as `frq.tui`: the components in `frq.app` do not know what is
5+ under the reconciler, so requiring `glimmer-cosmic.core` after `frq.app`
6+ makes it the backend glimmer renders with. Nothing in `frq.app` changes.
7+
8+ glimmer-cosmic is a spike, and it shows. It paints a short list of tags and
9+ treats every other one as a column, so most of frq comes out as stacked text
10+ and buttons rather than its real layout. The jvui calls frq makes elsewhere
11+ (a picture chooser, call frames) have no window of their own to act on and do
12+ nothing, the same way they do in the terminal.
13+
14+ The one real difference from the other two: libcosmic takes the main thread
15+ and keeps it, so glimmer's loop runs on a worker. The timers handed to
16+ `start!` are glimmer-cosmic's, which run on that worker.
17+
18+ just cosmic"
19+ (:require [frq.app :as app]
20+ [frq.platform :as platform]
21+ [frq.state :as s]
22+ [glimmer.core :as ui]
23+ [glimmer-cosmic.ffi :as cosmic-ffi]
24+ ;; last, so its install! is the one that stands
25+ [glimmer-cosmic.core :as cosmic]))
26+
27+(defn- dump-tree-to!
28+ "Write the tree as the reconciler left it to `path` every few seconds.
29+
30+ There is no REPL into a libcosmic window — it owns the main thread — so this
31+ is how to tell a row that is not in the tree from a row that is in it and not
32+ painted. Off unless FRQ_COSMIC_DUMP names a file."
33+ [path]
34+ (cosmic/every! 3000 #(spit path (cosmic/dump-str))))
35+
36+(defn -main [& _]
37+ ;; What frq asks of glimmer-jvui directly, answered by this backend instead:
38+ ;; jvui's timers only run inside jvui's loop, its quit closes jvui's window,
39+ ;; and it has no picture chooser on a desktop.
40+ (platform/override! {:after! cosmic/after!
41+ :quit! cosmic/quit!
42+ :pick-image! cosmic/pick-image!
43+ :picked-image! cosmic/picked-image!})
44+ (when-let [path (not-empty (System/getenv "FRQ_COSMIC_DUMP"))]
45+ (dump-tree-to! path))
46+ (app/start! {:after! cosmic/after!
47+ :every! cosmic/every!
48+ :title! cosmic-ffi/set-title!
49+ ;; The window's size, which frq lays its columns out against —
50+ ;; the message list is told how much room the people panel
51+ ;; leaves it.
52+ :measure! (fn []
53+ (let [[w h] (cosmic/window-size)]
54+ (when (and (pos? w) (not= w @s/window-width))
55+ (reset! s/window-width w))
56+ (when (and (pos? h) (not= h @s/window-height))
57+ (reset! s/window-height h))))
58+ ;; Call frames are painted into a jvui texture, which is not here.
59+ :av? false})
60+ (ui/run app/app :title "frq" :width 520 :height 860))
modified src/frq/platform.clj +34 -0
@@ -29,6 +29,40 @@
2929 (gui/open-url! (or url ""))
3030 (catch Exception _ false)))
3131
32+(defonce ^:private overrides
33+ ;; What a backend other than glimmer-jvui does in place of jvui's own. Filled
34+ ;; in by the entry point that installs that backend — `frq.cosmic` — before
35+ ;; the app starts; empty means the window's.
36+ (atom {}))
37+
38+(defn override!
39+ "Replace some of what the platform does, for a backend other than jvui.
40+ Keys: :after! :quit! :pick-image! :picked-image!."
41+ [m]
42+ (swap! overrides merge m)
43+ nil)
44+
45+(defn after!
46+ "Run `f` on the UI thread in about `ms` milliseconds. jvui's timers only run
47+ inside jvui's loop, so a backend with a loop of its own has to lend its own."
48+ [ms f]
49+ ((get @overrides :after! gui/after!) ms f))
50+
51+(defn quit!
52+ "Close the window."
53+ []
54+ ((get @overrides :quit! gui/quit!)))
55+
56+(defn pick-image!
57+ "Open the platform's picture chooser; true when there is one and it opened."
58+ []
59+ ((get @overrides :pick-image! gui/pick-image!)))
60+
61+(defn picked-image!
62+ "Write the chosen picture to `path`; true once, when one has been chosen."
63+ [path]
64+ ((get @overrides :picked-image! gui/picked-image!) path))
65+
3266 (defn return-url
3367 "The link that brings the app back to the front once the browser is done, or
3468 nil where the browser never covered it. `frq://auth` is the manifest's own
@@ -29,6 +29,40 @@
29 (gui/open-url! (or url ""))29 (gui/open-url! (or url ""))
30 (catch Exception _ false)))30 (catch Exception _ false)))
31 31
32+(defonce ^:private overrides
33+ ;; What a backend other than glimmer-jvui does in place of jvui's own. Filled
34+ ;; in by the entry point that installs that backend — `frq.cosmic` — before
35+ ;; the app starts; empty means the window's.
36+ (atom {}))
37+
38+(defn override!
39+ "Replace some of what the platform does, for a backend other than jvui.
40+ Keys: :after! :quit! :pick-image! :picked-image!."
41+ [m]
42+ (swap! overrides merge m)
43+ nil)
44+
45+(defn after!
46+ "Run `f` on the UI thread in about `ms` milliseconds. jvui's timers only run
47+ inside jvui's loop, so a backend with a loop of its own has to lend its own."
48+ [ms f]
49+ ((get @overrides :after! gui/after!) ms f))
50+
51+(defn quit!
52+ "Close the window."
53+ []
54+ ((get @overrides :quit! gui/quit!)))
55+
56+(defn pick-image!
57+ "Open the platform's picture chooser; true when there is one and it opened."
58+ []
59+ ((get @overrides :pick-image! gui/pick-image!)))
60+
61+(defn picked-image!
62+ "Write the chosen picture to `path`; true once, when one has been chosen."
63+ [path]
64+ ((get @overrides :picked-image! gui/picked-image!) path))
65+
32 (defn return-url66 (defn return-url
33 "The link that brings the app back to the front once the browser is done, or67 "The link that brings the app back to the front once the browser is done, or
34 nil where the browser never covered it. `frq://auth` is the manifest's own68 nil where the browser never covered it. `frq://auth` is the manifest's own
modified src/frq/state.clj +5 -4
@@ -16,6 +16,7 @@
1616 [frq.avatars :as avatars]
1717 [frq.media :as media]
1818 [frq.oauth :as oauth]
19+ [frq.platform :as platform]
1920 [frq.store :as store]
2021 [frq.upload :as upload]
2122 [glimmer-jvui.core :as gui]))
@@ -1450,7 +1451,7 @@
14501451 []
14511452 (let [path (paste-path)]
14521453 (host/mkdirs! (str (media/cache-dir) "/outgoing"))
1453- (when (gui/picked-image! path)
1454+ (when (platform/picked-image! path)
14541455 (reset! choosing nil)
14551456 (attach! path "picture.png")
14561457 true)))
@@ -1460,7 +1461,7 @@
14601461 (when-not (take-chosen!)
14611462 (if (pos? left)
14621463 (do (reset! choosing (dec left))
1463- (gui/after! choose-poll-ms poll-chosen!))
1464+ (platform/after! choose-poll-ms poll-chosen!))
14641465 (reset! choosing nil)))))
14651466
14661467 (defn choose-image!
@@ -1473,10 +1474,10 @@
14731474 permission, browsing finds almost nothing to show. False where there is no
14741475 chooser, which is every desktop, and there browsing is the answer."
14751476 []
1476- (when (gui/pick-image!)
1477+ (when (platform/pick-image!)
14771478 (reset! error nil)
14781479 (reset! choosing choose-poll-limit)
1479- (gui/after! choose-poll-ms poll-chosen!)
1480+ (platform/after! choose-poll-ms poll-chosen!)
14801481 true))
14811482
14821483 (defn open-image-picker!
@@ -16,6 +16,7 @@
16 [frq.avatars :as avatars]16 [frq.avatars :as avatars]
17 [frq.media :as media]17 [frq.media :as media]
18 [frq.oauth :as oauth]18 [frq.oauth :as oauth]
19+ [frq.platform :as platform]
19 [frq.store :as store]20 [frq.store :as store]
20 [frq.upload :as upload]21 [frq.upload :as upload]
21 [glimmer-jvui.core :as gui]))22 [glimmer-jvui.core :as gui]))
@@ -1450,7 +1451,7 @@
1450 []1451 []
1451 (let [path (paste-path)]1452 (let [path (paste-path)]
1452 (host/mkdirs! (str (media/cache-dir) "/outgoing"))1453 (host/mkdirs! (str (media/cache-dir) "/outgoing"))
1453- (when (gui/picked-image! path)1454+ (when (platform/picked-image! path)
1454 (reset! choosing nil)1455 (reset! choosing nil)
1455 (attach! path "picture.png")1456 (attach! path "picture.png")
1456 true)))1457 true)))
@@ -1460,7 +1461,7 @@
1460 (when-not (take-chosen!)1461 (when-not (take-chosen!)
1461 (if (pos? left)1462 (if (pos? left)
1462 (do (reset! choosing (dec left))1463 (do (reset! choosing (dec left))
1463- (gui/after! choose-poll-ms poll-chosen!))1464+ (platform/after! choose-poll-ms poll-chosen!))
1464 (reset! choosing nil)))))1465 (reset! choosing nil)))))
1465 1466
1466 (defn choose-image!1467 (defn choose-image!
@@ -1473,10 +1474,10 @@
1473 permission, browsing finds almost nothing to show. False where there is no1474 permission, browsing finds almost nothing to show. False where there is no
1474 chooser, which is every desktop, and there browsing is the answer."1475 chooser, which is every desktop, and there browsing is the answer."
1475 []1476 []
1476- (when (gui/pick-image!)1477+ (when (platform/pick-image!)
1477 (reset! error nil)1478 (reset! error nil)
1478 (reset! choosing choose-poll-limit)1479 (reset! choosing choose-poll-limit)
1479- (gui/after! choose-poll-ms poll-chosen!)1480+ (platform/after! choose-poll-ms poll-chosen!)
1480 true))1481 true))
1481 1482
1482 (defn open-image-picker!1483 (defn open-image-picker!