Merge remote-tracking branch 'origin/main' into claude/copy-profiles-sleek-aee53c
modified
android/build-jolt-boot.sh +6 -1 | @@ -9,7 +9,12 @@ VIDYA="${VIDYA:-$(cd "$ROOT/../vidya" && pwd)}" | ||
| 9 | 9 | # has to be named here. |
| 10 | 10 | GLIMMER="${GLIMMER:-$HOME/.jolt/gitlibs/https___github.com_jolt-lang_glimmer/5581c331c51aff989259b9e8e92ec920fe5e6741/src}" |
| 11 | 11 | OUT="${1:?usage: build-jolt-boot.sh OUTPUT_DIRECTORY}" |
| 12 | -JOLT="${JOLT:-$(command -v jolt)}" | |
| 12 | +# The DotSlash-pinned jolt beside this script, not whatever is on PATH: an | |
| 13 | +# upstream jolt cannot open a TLS connection on Android — it reads the socket | |
| 14 | +# address out of `struct addrinfo` at glibc's offset, which is Bionic's | |
| 15 | +# `ai_canonname` — so a build made with one produces an APK that cannot sign in | |
| 16 | +# or send a picture. Override with JOLT= to use another. | |
| 17 | +JOLT="${JOLT:-$ROOT/scripts/jolt}" | |
| 13 | 18 | MODULE="${MODULE:-frq.app}" |
| 14 | 19 | CHEZ_ANDROID="${CHEZ_ANDROID:-$HOME/.cache/vidya-chez-android}" |
| 15 | 20 | HOST_SCHEME="$CHEZ_ANDROID/ta6le/bin/ta6le/scheme" |
| @@ -9,7 +9,12 @@ VIDYA="${VIDYA:-$(cd "$ROOT/../vidya" && pwd)}" | |||
| 9 | # has to be named here. | 9 | # has to be named here. |
| 10 | GLIMMER="${GLIMMER:-$HOME/.jolt/gitlibs/https___github.com_jolt-lang_glimmer/5581c331c51aff989259b9e8e92ec920fe5e6741/src}" | 10 | GLIMMER="${GLIMMER:-$HOME/.jolt/gitlibs/https___github.com_jolt-lang_glimmer/5581c331c51aff989259b9e8e92ec920fe5e6741/src}" |
| 11 | OUT="${1:?usage: build-jolt-boot.sh OUTPUT_DIRECTORY}" | 11 | OUT="${1:?usage: build-jolt-boot.sh OUTPUT_DIRECTORY}" |
| 12 | -JOLT="${JOLT:-$(command -v jolt)}" | 12 | +# The DotSlash-pinned jolt beside this script, not whatever is on PATH: an |
| 13 | +# upstream jolt cannot open a TLS connection on Android — it reads the socket | ||
| 14 | +# address out of `struct addrinfo` at glibc's offset, which is Bionic's | ||
| 15 | +# `ai_canonname` — so a build made with one produces an APK that cannot sign in | ||
| 16 | +# or send a picture. Override with JOLT= to use another. | ||
| 17 | +JOLT="${JOLT:-$ROOT/scripts/jolt}" | ||
| 13 | MODULE="${MODULE:-frq.app}" | 18 | MODULE="${MODULE:-frq.app}" |
| 14 | CHEZ_ANDROID="${CHEZ_ANDROID:-$HOME/.cache/vidya-chez-android}" | 19 | CHEZ_ANDROID="${CHEZ_ANDROID:-$HOME/.cache/vidya-chez-android}" |
| 15 | HOST_SCHEME="$CHEZ_ANDROID/ta6le/bin/ta6le/scheme" | 20 | HOST_SCHEME="$CHEZ_ANDROID/ta6le/bin/ta6le/scheme" |
added
scripts/jolt +28 -0 | new file mode 100755 | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +#!/usr/bin/env dotslash | |
| 2 | + | |
| 3 | +// jolt with the Bionic `struct addrinfo` fix, from the fork at | |
| 4 | +// gitlab.com/nandithebull/jolt (commit 2b80d68d). Upstream's own release reads | |
| 5 | +// `ai_addr` at glibc's offset, which on Android is `ai_canonname` — a null | |
| 6 | +// pointer — so every TLS connect fails there and reports a refusal the network | |
| 7 | +// never made. Nothing else differs from v0.7.28. | |
| 8 | +// | |
| 9 | +// Linux x86_64 only: it is what the APK is built on. Other platforms fall back | |
| 10 | +// to whatever `jolt` is on PATH, which is upstream's, which is fine for | |
| 11 | +// everything but an Android build. | |
| 12 | +{ | |
| 13 | + "name": "jolt", | |
| 14 | + "platforms": { | |
| 15 | + "linux-x86_64": { | |
| 16 | + "size": 15782379, | |
| 17 | + "hash": "sha256", | |
| 18 | + "digest": "aabb71f809aebd9d607b7a5036229f933c733ae90785cb29df899f116be9589f", | |
| 19 | + "format": "tar.gz", | |
| 20 | + "path": "jolt-v0.7.28-1-g2b80d68d-x86_64-linux/jolt", | |
| 21 | + "providers": [ | |
| 22 | + { | |
| 23 | + "url": "https://gitlab.com/nandithebull/jolt/-/releases/v0.7.28-android/downloads/jolt-v0.7.28-1-g2b80d68d-x86_64-linux.tar.gz" | |
| 24 | + } | |
| 25 | + ] | |
| 26 | + } | |
| 27 | + } | |
| 28 | +} | |
| new file mode 100755 | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | +#!/usr/bin/env dotslash | ||
| 2 | + | ||
| 3 | +// jolt with the Bionic `struct addrinfo` fix, from the fork at | ||
| 4 | +// gitlab.com/nandithebull/jolt (commit 2b80d68d). Upstream's own release reads | ||
| 5 | +// `ai_addr` at glibc's offset, which on Android is `ai_canonname` — a null | ||
| 6 | +// pointer — so every TLS connect fails there and reports a refusal the network | ||
| 7 | +// never made. Nothing else differs from v0.7.28. | ||
| 8 | +// | ||
| 9 | +// Linux x86_64 only: it is what the APK is built on. Other platforms fall back | ||
| 10 | +// to whatever `jolt` is on PATH, which is upstream's, which is fine for | ||
| 11 | +// everything but an Android build. | ||
| 12 | +{ | ||
| 13 | + "name": "jolt", | ||
| 14 | + "platforms": { | ||
| 15 | + "linux-x86_64": { | ||
| 16 | + "size": 15782379, | ||
| 17 | + "hash": "sha256", | ||
| 18 | + "digest": "aabb71f809aebd9d607b7a5036229f933c733ae90785cb29df899f116be9589f", | ||
| 19 | + "format": "tar.gz", | ||
| 20 | + "path": "jolt-v0.7.28-1-g2b80d68d-x86_64-linux/jolt", | ||
| 21 | + "providers": [ | ||
| 22 | + { | ||
| 23 | + "url": "https://gitlab.com/nandithebull/jolt/-/releases/v0.7.28-android/downloads/jolt-v0.7.28-1-g2b80d68d-x86_64-linux.tar.gz" | ||
| 24 | + } | ||
| 25 | + ] | ||
| 26 | + } | ||
| 27 | + } | ||
| 28 | +} | ||
modified
src/frq/app.jolt +47 -21 | @@ -158,29 +158,37 @@ | ||
| 158 | 158 | (defn chats-screen [] |
| 159 | 159 | (let [buffers (s/channel-list)] |
| 160 | 160 | [:page {:max-width 620} |
| 161 | - [:title {:label "Chats"}] | |
| 161 | + ;; Your nick in the title, where "Chats" alone said nothing you did not | |
| 162 | + ;; already know. It is what every channel calls you and what your own | |
| 163 | + ;; lines are signed with, and the only other place it showed was Settings. | |
| 164 | + [:title {:label (if (s/connected?) (str "Chats as " @s/form-nick) "Chats")}] | |
| 162 | 165 | [error-note] |
| 166 | + ;; Join and search are the same shape — a channel box with a button | |
| 167 | + ;; beside it — so they read as one control block rather than a titled | |
| 168 | + ;; card and a stray row: same card, same widths, same gap. The screen | |
| 169 | + ;; title already says what the box is for, which is what the "Join | |
| 170 | + ;; channel" header was doing. | |
| 163 | 171 | [:card {} |
| 164 | - [:title-2 {:label "Join channel"}] | |
| 165 | - [:hbox {:spacing 8} | |
| 166 | - [:entry {:text @s/join-input | |
| 167 | - :width-request 380 | |
| 168 | - :placeholder "#channel" | |
| 169 | - :on-change #(reset! s/join-input %) | |
| 170 | - :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}] | |
| 171 | - [:button {:label "Join" | |
| 172 | - :kind :primary | |
| 173 | - :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]] | |
| 174 | - ;; The clear button only shows while there is something to clear: an | |
| 175 | - ;; empty box has nothing to undo, and a dead button beside it reads as | |
| 176 | - ;; one that stopped working. | |
| 177 | - [:hbox {:spacing 6} | |
| 178 | - [:entry {:text @s/search | |
| 179 | - :width-request 380 | |
| 180 | - :placeholder "Search channels" | |
| 181 | - :on-change #(reset! s/search %)}] | |
| 182 | - (when (seq @s/search) | |
| 183 | - [:button {:label "✕" :on-click #(reset! s/search "")}])] | |
| 172 | + [:vbox {:spacing 8} | |
| 173 | + [:hbox {:spacing 8} | |
| 174 | + [:entry {:text @s/join-input | |
| 175 | + :width-request 380 | |
| 176 | + :placeholder "#channel" | |
| 177 | + :on-change #(reset! s/join-input %) | |
| 178 | + :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}] | |
| 179 | + [:button {:label "Join" | |
| 180 | + :kind :primary | |
| 181 | + :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]] | |
| 182 | + ;; The clear button only shows while there is something to clear: an | |
| 183 | + ;; empty box has nothing to undo, and a dead button beside it reads as | |
| 184 | + ;; one that stopped working. | |
| 185 | + [:hbox {:spacing 8} | |
| 186 | + [:entry {:text @s/search | |
| 187 | + :width-request 380 | |
| 188 | + :placeholder "Search channels" | |
| 189 | + :on-change #(reset! s/search %)}] | |
| 190 | + (when (seq @s/search) | |
| 191 | + [:button {:label "✕" :on-click #(reset! s/search "")}])]]] | |
| 184 | 192 | (if (seq buffers) |
| 185 | 193 | (for [b buffers] [conversation-row b]) |
| 186 | 194 | [:card {} [:dim-label {:label "No conversations yet — join a channel."}]]) |
| @@ -1024,4 +1032,22 @@ | ||
| 1024 | 1032 | (vidya/every! 200 #(let [w (vidya/window-width)] |
| 1025 | 1033 | (when (not= w @s/window-width) |
| 1026 | 1034 | (reset! s/window-width w)))) |
| 1035 | + ;; The nick in the window title, so a second window of this client is told | |
| 1036 | + ;; apart from the first by the one thing that differs — and so the answer to | |
| 1037 | + ;; "who am I here?" is on screen without opening Settings. | |
| 1038 | + ;; | |
| 1039 | + ;; Polled, like the width above and for the same reason: the title belongs to | |
| 1040 | + ;; the window rather than to the tree, so no render puts it there, and the | |
| 1041 | + ;; nick is only settled once a connection has been made. Twice a second is | |
| 1042 | + ;; far more often than a nick changes, and a string compare is what a tick | |
| 1043 | + ;; costs when it has not. | |
| 1044 | + ;; Seeded with the title `run` opens the window under, so the first tick of | |
| 1045 | + ;; a launch that has nobody signed in yet sets nothing. | |
| 1046 | + (let [shown (atom "frq")] | |
| 1047 | + (vidya/every! 500 #(let [title (if (and (s/connected?) (seq @s/form-nick)) | |
| 1048 | + (str "frq — " @s/form-nick) | |
| 1049 | + "frq")] | |
| 1050 | + (when (not= title @shown) | |
| 1051 | + (reset! shown title) | |
| 1052 | + (vidya/set-title! title))))) | |
| 1027 | 1053 | (ui/run app :title "frq" :width 520 :height 860)) |
| @@ -158,29 +158,37 @@ | |||
| 158 | (defn chats-screen [] | 158 | (defn chats-screen [] |
| 159 | (let [buffers (s/channel-list)] | 159 | (let [buffers (s/channel-list)] |
| 160 | [:page {:max-width 620} | 160 | [:page {:max-width 620} |
| 161 | - [:title {:label "Chats"}] | 161 | + ;; Your nick in the title, where "Chats" alone said nothing you did not |
| 162 | + ;; already know. It is what every channel calls you and what your own | ||
| 163 | + ;; lines are signed with, and the only other place it showed was Settings. | ||
| 164 | + [:title {:label (if (s/connected?) (str "Chats as " @s/form-nick) "Chats")}] | ||
| 162 | [error-note] | 165 | [error-note] |
| 166 | + ;; Join and search are the same shape — a channel box with a button | ||
| 167 | + ;; beside it — so they read as one control block rather than a titled | ||
| 168 | + ;; card and a stray row: same card, same widths, same gap. The screen | ||
| 169 | + ;; title already says what the box is for, which is what the "Join | ||
| 170 | + ;; channel" header was doing. | ||
| 163 | [:card {} | 171 | [:card {} |
| 164 | - [:title-2 {:label "Join channel"}] | 172 | + [:vbox {:spacing 8} |
| 165 | - [:hbox {:spacing 8} | 173 | + [:hbox {:spacing 8} |
| 166 | - [:entry {:text @s/join-input | 174 | + [:entry {:text @s/join-input |
| 167 | - :width-request 380 | 175 | + :width-request 380 |
| 168 | - :placeholder "#channel" | 176 | + :placeholder "#channel" |
| 169 | - :on-change #(reset! s/join-input %) | 177 | + :on-change #(reset! s/join-input %) |
| 170 | - :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}] | 178 | + :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}] |
| 171 | - [:button {:label "Join" | 179 | + [:button {:label "Join" |
| 172 | - :kind :primary | 180 | + :kind :primary |
| 173 | - :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]] | 181 | + :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]] |
| 174 | - ;; The clear button only shows while there is something to clear: an | 182 | + ;; The clear button only shows while there is something to clear: an |
| 175 | - ;; empty box has nothing to undo, and a dead button beside it reads as | 183 | + ;; empty box has nothing to undo, and a dead button beside it reads as |
| 176 | - ;; one that stopped working. | 184 | + ;; one that stopped working. |
| 177 | - [:hbox {:spacing 6} | 185 | + [:hbox {:spacing 8} |
| 178 | - [:entry {:text @s/search | 186 | + [:entry {:text @s/search |
| 179 | - :width-request 380 | 187 | + :width-request 380 |
| 180 | - :placeholder "Search channels" | 188 | + :placeholder "Search channels" |
| 181 | - :on-change #(reset! s/search %)}] | 189 | + :on-change #(reset! s/search %)}] |
| 182 | - (when (seq @s/search) | 190 | + (when (seq @s/search) |
| 183 | - [:button {:label "✕" :on-click #(reset! s/search "")}])] | 191 | + [:button {:label "✕" :on-click #(reset! s/search "")}])]]] |
| 184 | (if (seq buffers) | 192 | (if (seq buffers) |
| 185 | (for [b buffers] [conversation-row b]) | 193 | (for [b buffers] [conversation-row b]) |
| 186 | [:card {} [:dim-label {:label "No conversations yet — join a channel."}]]) | 194 | [:card {} [:dim-label {:label "No conversations yet — join a channel."}]]) |
| @@ -1024,4 +1032,22 @@ | |||
| 1024 | (vidya/every! 200 #(let [w (vidya/window-width)] | 1032 | (vidya/every! 200 #(let [w (vidya/window-width)] |
| 1025 | (when (not= w @s/window-width) | 1033 | (when (not= w @s/window-width) |
| 1026 | (reset! s/window-width w)))) | 1034 | (reset! s/window-width w)))) |
| 1035 | + ;; The nick in the window title, so a second window of this client is told | ||
| 1036 | + ;; apart from the first by the one thing that differs — and so the answer to | ||
| 1037 | + ;; "who am I here?" is on screen without opening Settings. | ||
| 1038 | + ;; | ||
| 1039 | + ;; Polled, like the width above and for the same reason: the title belongs to | ||
| 1040 | + ;; the window rather than to the tree, so no render puts it there, and the | ||
| 1041 | + ;; nick is only settled once a connection has been made. Twice a second is | ||
| 1042 | + ;; far more often than a nick changes, and a string compare is what a tick | ||
| 1043 | + ;; costs when it has not. | ||
| 1044 | + ;; Seeded with the title `run` opens the window under, so the first tick of | ||
| 1045 | + ;; a launch that has nobody signed in yet sets nothing. | ||
| 1046 | + (let [shown (atom "frq")] | ||
| 1047 | + (vidya/every! 500 #(let [title (if (and (s/connected?) (seq @s/form-nick)) | ||
| 1048 | + (str "frq — " @s/form-nick) | ||
| 1049 | + "frq")] | ||
| 1050 | + (when (not= title @shown) | ||
| 1051 | + (reset! shown title) | ||
| 1052 | + (vidya/set-title! title))))) | ||
| 1027 | (ui/run app :title "frq" :width 520 :height 860)) | 1053 | (ui/run app :title "frq" :width 520 :height 860)) |