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>
7b594b1 parent: d740915 modified
justfile +40 -0 | @@ -163,6 +163,46 @@ tui *args: | ||
| 163 | 163 | |
| 164 | 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 | 206 | # The same classpath as `tui`, with an nREPL on it instead of a `-main`: a |
| 167 | 207 | # session that can require `frq.tui` and then redefine a component while it is |
| 168 | 208 | # 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`: a | 206 | # 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 is | 207 | # 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 @@ | ||
| 217 | 217 | [:button {:label "Close" |
| 218 | 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 | 230 | (defn conversation-row [buffer] |
| 221 | 231 | (let [name (:name buffer) |
| 222 | 232 | ;; Defaulted rather than assumed. Everything that builds a buffer |
| @@ -257,7 +267,7 @@ | ||
| 257 | 267 | ;; neither — a room with your name in it always has a line to count. |
| 258 | 268 | (when (pos? unread) |
| 259 | 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 | 271 | (when-not @terminal? |
| 262 | 272 | [:hbox {:spacing 8 :wrap false} |
| 263 | 273 | [open-button name] |
| @@ -521,11 +531,11 @@ | ||
| 521 | 531 | (reset! s/highlight id) |
| 522 | 532 | ;; Off again once the frame that scrolled has |
| 523 | 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 | 535 | ;; The highlight stays long enough to be read, |
| 526 | 536 | ;; and only clears itself: a later jump elsewhere |
| 527 | 537 | ;; owns the highlight from then on. |
| 528 | - (gui/after! 2000 | |
| 538 | + (platform/after! 2000 | |
| 529 | 539 | (fn [] |
| 530 | 540 | (when (= id @s/highlight) |
| 531 | 541 | (reset! s/highlight nil)))))}] |
| @@ -1258,7 +1268,7 @@ | ||
| 1258 | 1268 | ;; be told to leave. A column with `:fill-height` and no width takes the whole |
| 1259 | 1269 | ;; row — so the panel is only ever on screen if the message list is given a |
| 1260 | 1270 | ;; width that stops short of it. |
| 1261 | -(def ^:private users-width 180) | |
| 1271 | +(def ^:private users-width 150) | |
| 1262 | 1272 | |
| 1263 | 1273 | ;; How many lines the compose field is currently drawn as. The window backend |
| 1264 | 1274 | ;; says so when it changes — a field with room to grow wraps a long message |
| @@ -1324,7 +1334,10 @@ | ||
| 1324 | 1334 | ;; The name is a button because a person is somewhere to go: pressing one |
| 1325 | 1335 | ;; opens a conversation with them. |
| 1326 | 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 | 1341 | [:button {:label nick :on-click #(s/open-dm! nick)}]]) |
| 1329 | 1342 | |
| 1330 | 1343 | (defn users-panel |
| @@ -1618,9 +1631,28 @@ | ||
| 1618 | 1631 | |
| 1619 | 1632 | ;; ---------------------------------------------------------------- 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 | 1654 | (defn discover-screen [] |
| 1622 | - [:page {:max-width 620} | |
| 1623 | - [:title {:label "Discover"}] | |
| 1655 | + (tab-screen "Discover" "discover-list" | |
| 1624 | 1656 | [:dim-label {:label "Popular channels on freeq."}] |
| 1625 | 1657 | [error-note] |
| 1626 | 1658 | (for [[name blurb] s/popular-channels] |
| @@ -1630,15 +1662,12 @@ | ||
| 1630 | 1662 | [:dim-label {:label blurb}] |
| 1631 | 1663 | [:button {:label (if joined? "Open" "Join") |
| 1632 | 1664 | :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))}]])))) | |
| 1636 | 1666 | |
| 1637 | 1667 | ;; ---------------------------------------------------------------- settings |
| 1638 | 1668 | |
| 1639 | 1669 | (defn settings-screen [] |
| 1640 | - [:page {:max-width 520} | |
| 1641 | - [:title {:label "Settings"}] | |
| 1670 | + (tab-screen "Settings" "settings-list" | |
| 1642 | 1671 | [:card {} |
| 1643 | 1672 | [:title-2 {:label "Connection"}] |
| 1644 | 1673 | [:status {:label @s/status :live (s/connected?)}] |
| @@ -1671,9 +1700,7 @@ | ||
| 1671 | 1700 | [:card {} |
| 1672 | 1701 | [:title-2 {:label "frq"}] |
| 1673 | 1702 | [: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!}]])) | |
| 1677 | 1704 | |
| 1678 | 1705 | ;; ---------------------------------------------------------------- shell |
| 1679 | 1706 | |
| @@ -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 buffer | 232 | ;; 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 has | 532 | ;; 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 elsewhere | 536 | ;; 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! 2000 | 538 | + (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 whole | 1268 | ;; 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 a | 1269 | ;; 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 backend | 1273 | ;; 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 message | 1274 | ;; 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 one | 1334 | ;; 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-panel | 1343 | (defn users-panel |
| @@ -1618,9 +1631,28 @@ | |||
| 1618 | 1631 | ||
| 1619 | ;; ---------------------------------------------------------------- discover | 1632 | ;; ---------------------------------------------------------------- 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 :primary | 1664 | :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 | ;; ---------------------------------------------------------------- settings | 1667 | ;; ---------------------------------------------------------------- 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 | ;; ---------------------------------------------------------------- shell | 1705 | ;; ---------------------------------------------------------------- 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 @@ | ||
| 29 | 29 | (gui/open-url! (or url "")) |
| 30 | 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 | 66 | (defn return-url |
| 33 | 67 | "The link that brings the app back to the front once the browser is done, or |
| 34 | 68 | 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-url | 66 | (defn return-url |
| 33 | "The link that brings the app back to the front once the browser is done, or | 67 | "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 own | 68 | 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 @@ | ||
| 16 | 16 | [frq.avatars :as avatars] |
| 17 | 17 | [frq.media :as media] |
| 18 | 18 | [frq.oauth :as oauth] |
| 19 | + [frq.platform :as platform] | |
| 19 | 20 | [frq.store :as store] |
| 20 | 21 | [frq.upload :as upload] |
| 21 | 22 | [glimmer-jvui.core :as gui])) |
| @@ -1450,7 +1451,7 @@ | ||
| 1450 | 1451 | [] |
| 1451 | 1452 | (let [path (paste-path)] |
| 1452 | 1453 | (host/mkdirs! (str (media/cache-dir) "/outgoing")) |
| 1453 | - (when (gui/picked-image! path) | |
| 1454 | + (when (platform/picked-image! path) | |
| 1454 | 1455 | (reset! choosing nil) |
| 1455 | 1456 | (attach! path "picture.png") |
| 1456 | 1457 | true))) |
| @@ -1460,7 +1461,7 @@ | ||
| 1460 | 1461 | (when-not (take-chosen!) |
| 1461 | 1462 | (if (pos? left) |
| 1462 | 1463 | (do (reset! choosing (dec left)) |
| 1463 | - (gui/after! choose-poll-ms poll-chosen!)) | |
| 1464 | + (platform/after! choose-poll-ms poll-chosen!)) | |
| 1464 | 1465 | (reset! choosing nil))))) |
| 1465 | 1466 | |
| 1466 | 1467 | (defn choose-image! |
| @@ -1473,10 +1474,10 @@ | ||
| 1473 | 1474 | permission, browsing finds almost nothing to show. False where there is no |
| 1474 | 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 | 1478 | (reset! error nil) |
| 1478 | 1479 | (reset! choosing choose-poll-limit) |
| 1479 | - (gui/after! choose-poll-ms poll-chosen!) | |
| 1480 | + (platform/after! choose-poll-ms poll-chosen!) | |
| 1480 | 1481 | true)) |
| 1481 | 1482 | |
| 1482 | 1483 | (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 no | 1474 | 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! |