nandi/frqpublic Fork 0
fc5f63c
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 the desktop with Flutter too, beside libcosmic

Two desktop GUIs now, and both first-class: `just run` is libcosmic under
jolt, `just flutter-desktop` is Flutter's Linux target over frq.hiccup. The
same screens out of common/frq/screens/, two renderers.

Almost all of it is the APK's work. One `clojure -M:cljd compile` over the
same flutter/src and common/ feeds both targets, and path_provider and
url_launcher each have a Linux implementation, so the seam needed no new
call. What is new is the toolchain and the runner:

  * devShells.flutter-desktop — the APK shell with the Android half swapped
    out. The same pinned clojure and flutter, with cmake, ninja, pkg-config
    and gtk3 where the JDK and the SDK are. Separate rather than merged,
    because a desktop build has no use for a few hundred megabytes of
    Android SDK — the same argument that keeps Flutter out of the default
    shell.
  * just flutter-desktop [build|run] — the re-entry shape every recipe here
    has, and nixGL off NixOS for the reason `run` needs it and `tui` does
    not. Still impure for the network half of what makes `apk` impure, but
    with no writable-ANDROID_HOME dance, since nothing writes to the store.
  * flutter/linux/ — the template runner renamed. frq rather than
    cljd_flutter, uk.nandi.frq rather than com.example.cljd_flutter, so the
    binary, the window title and the GTK application id agree with the APK's
    applicationId.

And two bugs the second target exposed, both of them places where "the
phone" was assumed rather than asked:

  * frq.io.dart/write-private-file! was a plain write, on the grounds that
    Android storage is already private to the app. On a Linux desktop it is
    not — the file lands under the XDG data directory with the process
    umask, world-readable on most systems, and what it holds is a broker
    token. It branches on Platform.isAndroid now and otherwise does what
    frq.io.jolt does, in the same order: create, chmod, then write, so the
    token is never on disk readable even for an instant. Dart has no chmod,
    which is why that is a process and not a call.
  * frq.oauth.dart handed the capture page frq://auth unconditionally, to
    raise the app from behind Chrome. Nothing on a desktop claims that
    scheme, so it was a button that does nothing. nil there instead — which
    core/capture-html already documented as the desktop case and already
    handled.

The stale UNVERIFIED note on frq.io.dart goes with them: two targets compile
that file now.

Verified: builds, and the window opens on COSMIC under Impeller/GL with the
connect screen painted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-12T01:41:02-07:00 Browse files
fc5f63c parent: 709af9f
modified CLAUDE.md +25 -8
@@ -39,11 +39,28 @@ Adding a host call means adding it to the seam in `common/frq/io.cljc` and to
3939 both implementations. Name it for the result rather than the mechanism — the
4040 seam has `write-private-file!` and not a chmod, because Dart has no chmod.
4141
42-`flutter/` builds with `just apk`, out of the flake's own `.#flutter` shell
43-(clojure, jdk17, flutter) and its `.#android-sdk` package. Impure on purpose:
44-Gradle fetches its own dependencies and writes into `ANDROID_HOME`, so the
45-recipe copies the store SDK to `flutter/.home` and lets it finish there. See
46-flutter/README.md. It is the only APK there is — the jolt APK,
47-`nix/android.nix`, `android/` and the `.#apk` outputs are gone, because every
48-backend that APK could paint with is retired. jvui and Vidya were experiments;
49-libcosmic is the desktop window and does not cross to a phone.
42+`flutter/` builds two things, from one `clojure -M:cljd compile`:
43+
44+`just apk`, out of the flake's own `.#flutter` shell (clojure, jdk17, flutter)
45+and its `.#android-sdk` package. Impure on purpose: Gradle fetches its own
46+dependencies and writes into `ANDROID_HOME`, so the recipe copies the store SDK
47+to `flutter/.home` and lets it finish there. It is the only APK there is — the
48+jolt APK, `nix/android.nix`, `android/` and the `.#apk` outputs are gone,
49+because every backend that APK could paint with is retired.
50+
51+`just flutter-desktop`, out of `.#flutter-desktop` (the same clojure and
52+flutter, with cmake, ninja, pkg-config and gtk3 where the JDK and the SDK are).
53+Impure for the network half of the same reasons and no writable-SDK dance, since
54+nothing writes into the store. nixGL off NixOS, like `just run`.
55+
56+So there are two desktop GUIs and they are both first-class: `just run` is
57+libcosmic under jolt, `just flutter-desktop` is Flutter's Linux target over
58+`frq.hiccup`. Same screens out of `common/frq/screens/`, two renderers. jvui and
59+Vidya were experiments and are gone; libcosmic is a desktop window and does not
60+cross to a phone, which is what the Flutter half is for.
61+
62+The consequence for `common/` is that "the phone" is no longer a synonym for
63+"the ClojureDart side" — two targets compile it. An implementation that branches
64+on the platform has to ask (`Platform.isAndroid`) rather than assume; see
65+`frq.io.dart/write-private-file!`, where assuming cost a token its file mode.
66+See flutter/README.md.
@@ -39,11 +39,28 @@ Adding a host call means adding it to the seam in `common/frq/io.cljc` and to
39 both implementations. Name it for the result rather than the mechanism — the39 both implementations. Name it for the result rather than the mechanism — the
40 seam has `write-private-file!` and not a chmod, because Dart has no chmod.40 seam has `write-private-file!` and not a chmod, because Dart has no chmod.
41 41
42-`flutter/` builds with `just apk`, out of the flake's own `.#flutter` shell42+`flutter/` builds two things, from one `clojure -M:cljd compile`:
43-(clojure, jdk17, flutter) and its `.#android-sdk` package. Impure on purpose:43+
44-Gradle fetches its own dependencies and writes into `ANDROID_HOME`, so the44+`just apk`, out of the flake's own `.#flutter` shell (clojure, jdk17, flutter)
45-recipe copies the store SDK to `flutter/.home` and lets it finish there. See45+and its `.#android-sdk` package. Impure on purpose: Gradle fetches its own
46-flutter/README.md. It is the only APK there is — the jolt APK,46+dependencies and writes into `ANDROID_HOME`, so the recipe copies the store SDK
47-`nix/android.nix`, `android/` and the `.#apk` outputs are gone, because every47+to `flutter/.home` and lets it finish there. It is the only APK there is — the
48-backend that APK could paint with is retired. jvui and Vidya were experiments;48+jolt APK, `nix/android.nix`, `android/` and the `.#apk` outputs are gone,
49-libcosmic is the desktop window and does not cross to a phone.49+because every backend that APK could paint with is retired.
50+
51+`just flutter-desktop`, out of `.#flutter-desktop` (the same clojure and
52+flutter, with cmake, ninja, pkg-config and gtk3 where the JDK and the SDK are).
53+Impure for the network half of the same reasons and no writable-SDK dance, since
54+nothing writes into the store. nixGL off NixOS, like `just run`.
55+
56+So there are two desktop GUIs and they are both first-class: `just run` is
57+libcosmic under jolt, `just flutter-desktop` is Flutter's Linux target over
58+`frq.hiccup`. Same screens out of `common/frq/screens/`, two renderers. jvui and
59+Vidya were experiments and are gone; libcosmic is a desktop window and does not
60+cross to a phone, which is what the Flutter half is for.
61+
62+The consequence for `common/` is that "the phone" is no longer a synonym for
63+"the ClojureDart side" — two targets compile it. An implementation that branches
64+on the platform has to ask (`Platform.isAndroid`) rather than assume; see
65+`frq.io.dart/write-private-file!`, where assuming cost a token its file mode.
66+See flutter/README.md.
modified flake.nix +53 -0
@@ -607,6 +607,59 @@
607607 FRQ_ANDROID_SDK =
608608 "${androidSdkFor pkgs.stdenv.hostPlatform.system}/libexec/android-sdk";
609609 };
610+
611+ # The other desktop GUI. Same ClojureDart half as the APK — one
612+ # `clojure -M:cljd compile`, one flutter/src — over Flutter's Linux
613+ # target instead of its Android one, so `flutter/linux/` is the
614+ # runner and CMake and Ninja are the build rather than Gradle.
615+ #
616+ # A separate shell from `flutter` rather than one that carries both,
617+ # because the halves are disjoint: this wants GTK and a C++ toolchain
618+ # and no JDK, and the APK wants a JDK and an SDK and no GTK. Sharing
619+ # them would mean every desktop build paying for a few hundred
620+ # megabytes of Android SDK it never opens, which is the same argument
621+ # that keeps Flutter out of the default shell.
622+ #
623+ # mkShell and not mkShellNoCC, unlike every other shell here: this is
624+ # the one that actually compiles C++. stdenv brings the compiler, and
625+ # gtk3 in buildInputs is what puts its .pc file where the runner's
626+ # `pkg_check_modules(GTK gtk+-3.0)` can find it.
627+ flutter-desktop = pkgs.mkShell {
628+ name = "frq-flutter-desktop";
629+
630+ # clojure and flutter are the APK shell's, and deliberately the
631+ # same two: the Dart that runs here is generated by the same
632+ # compiler from the same source, and a second Flutter version
633+ # under it would be a second set of engine artifacts and a second
634+ # answer to "does the phone build match the desktop one".
635+ nativeBuildInputs = [
636+ pkgs.clojure
637+ pkgs.flutter
638+ pkgs.just
639+ pkgs.cmake
640+ pkgs.ninja
641+ pkgs.pkg-config
642+ ];
643+
644+ # gtk3 is the runner's own dependency; the rest are url_launcher's
645+ # Linux implementation, which is a GTK plugin compiled into the
646+ # bundle. path_provider needs nothing here — its Linux half is
647+ # pure Dart over the XDG directories.
648+ buildInputs = [ pkgs.gtk3 pkgs.glib ];
649+
650+ # Same reason `just run` reaches for it: Flutter paints through
651+ # GL, and off NixOS the driver that can do that is the host's, not
652+ # the store's. The recipe reads this exactly as `run` does.
653+ NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel";
654+
655+ # The recipe's re-entry test, the way JOLT_NATIVE_LIB is the
656+ # default shell's. Nothing else sets it, so `just flutter-desktop`
657+ # outside the shell re-enters and lands back on the same recipe —
658+ # no flag to forget, and no second code path for someone who runs
659+ # `nix develop .#flutter-desktop --command just flutter-desktop`
660+ # by hand.
661+ FRQ_FLUTTER_DESKTOP = "1";
662+ };
610663 });
611664
612665 apps = forEachSystem (pkgs: {
@@ -607,6 +607,59 @@
607 FRQ_ANDROID_SDK =607 FRQ_ANDROID_SDK =
608 "${androidSdkFor pkgs.stdenv.hostPlatform.system}/libexec/android-sdk";608 "${androidSdkFor pkgs.stdenv.hostPlatform.system}/libexec/android-sdk";
609 };609 };
610+
611+ # The other desktop GUI. Same ClojureDart half as the APK — one
612+ # `clojure -M:cljd compile`, one flutter/src — over Flutter's Linux
613+ # target instead of its Android one, so `flutter/linux/` is the
614+ # runner and CMake and Ninja are the build rather than Gradle.
615+ #
616+ # A separate shell from `flutter` rather than one that carries both,
617+ # because the halves are disjoint: this wants GTK and a C++ toolchain
618+ # and no JDK, and the APK wants a JDK and an SDK and no GTK. Sharing
619+ # them would mean every desktop build paying for a few hundred
620+ # megabytes of Android SDK it never opens, which is the same argument
621+ # that keeps Flutter out of the default shell.
622+ #
623+ # mkShell and not mkShellNoCC, unlike every other shell here: this is
624+ # the one that actually compiles C++. stdenv brings the compiler, and
625+ # gtk3 in buildInputs is what puts its .pc file where the runner's
626+ # `pkg_check_modules(GTK gtk+-3.0)` can find it.
627+ flutter-desktop = pkgs.mkShell {
628+ name = "frq-flutter-desktop";
629+
630+ # clojure and flutter are the APK shell's, and deliberately the
631+ # same two: the Dart that runs here is generated by the same
632+ # compiler from the same source, and a second Flutter version
633+ # under it would be a second set of engine artifacts and a second
634+ # answer to "does the phone build match the desktop one".
635+ nativeBuildInputs = [
636+ pkgs.clojure
637+ pkgs.flutter
638+ pkgs.just
639+ pkgs.cmake
640+ pkgs.ninja
641+ pkgs.pkg-config
642+ ];
643+
644+ # gtk3 is the runner's own dependency; the rest are url_launcher's
645+ # Linux implementation, which is a GTK plugin compiled into the
646+ # bundle. path_provider needs nothing here — its Linux half is
647+ # pure Dart over the XDG directories.
648+ buildInputs = [ pkgs.gtk3 pkgs.glib ];
649+
650+ # Same reason `just run` reaches for it: Flutter paints through
651+ # GL, and off NixOS the driver that can do that is the host's, not
652+ # the store's. The recipe reads this exactly as `run` does.
653+ NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel";
654+
655+ # The recipe's re-entry test, the way JOLT_NATIVE_LIB is the
656+ # default shell's. Nothing else sets it, so `just flutter-desktop`
657+ # outside the shell re-enters and lands back on the same recipe —
658+ # no flag to forget, and no second code path for someone who runs
659+ # `nix develop .#flutter-desktop --command just flutter-desktop`
660+ # by hand.
661+ FRQ_FLUTTER_DESKTOP = "1";
662+ };
610 });663 });
611 664
612 apps = forEachSystem (pkgs: {665 apps = forEachSystem (pkgs: {
modified flutter/README.md +56 -4
@@ -76,13 +76,65 @@ plugins and is its own project.
7676
7777 ## Building it
7878
79+Two targets out of one tree. The ClojureDart compile is the same command for
80+both — `clojure -M:cljd compile` over `src/` and `../common` — and what differs
81+is only what Flutter is asked to wrap it in.
82+
7983 ```bash
80-just apk # the debug APK
81-just apk install # and onto a connected device
82-just apk run # and launched
83-just apk log # logcat
84+just apk # the debug APK
85+just apk install # and onto a connected device
86+just apk run # and launched
87+just apk log # logcat
88+
89+just flutter-desktop # the debug Linux bundle
90+just flutter-desktop run # and the window
8491 ```
8592
93+### The desktop one
94+
95+There are two desktop GUIs now, and they are not a fallback for each other:
96+`just run` is libcosmic under jolt, and `just flutter-desktop` is this tree
97+under Flutter's Linux target. Same screens out of `common/frq/screens/`, two
98+renderers — `glimmer-cosmic` walks the hiccup on one side and `frq.hiccup`
99+emits Flutter widgets on the other.
100+
101+Its toolchain is `devShells.flutter-desktop`, which is the APK shell with the
102+Android half swapped out: clojure and Flutter are the same two packages at the
103+same pinned rev, and CMake, Ninja, pkg-config and GTK stand where the JDK and
104+the SDK do. Kept separate rather than merged into one shell because the halves
105+are disjoint — a desktop build has no use for a few hundred megabytes of
106+Android SDK, which is the same argument that keeps Flutter out of the default
107+shell.
108+
109+Still impure, for one of the two reasons the APK is: pub.dev resolution and
110+Flutter's engine artifacts are network. What it does *not* need is the
111+writable-`ANDROID_HOME` dance, since nothing here writes into the store — so
112+there is no `flutter/.home` on this path.
113+
114+nixGL off NixOS, for the reason `just run` needs it and `just tui` does not:
115+Flutter paints through GL and the driver that can do that is the host's.
116+
117+`linux/` is the Flutter template's GTK runner, renamed — `frq` rather than
118+`cljd_flutter`, and `uk.nandi.frq` rather than `com.example.cljd_flutter`, so
119+the binary, the window title and the GTK application id agree with the APK's
120+`applicationId`.
121+
122+Two things the desktop target changed in the Dart, both of them cases where
123+"the phone" had been assumed rather than asked:
124+
125+* `frq.io.dart/write-private-file!` was a plain write, on the grounds that
126+ Android storage is already private to the app. On a Linux desktop it is not:
127+ the file lands under the XDG data directory with the process umask, and it
128+ holds a broker token. The desktop branch now does what `frq.io.jolt` does —
129+ create, chmod, then write — and Dart having no chmod is why that is a
130+ process.
131+* `frq.oauth.dart` handed the capture page `frq://auth` unconditionally, to
132+ raise the app from behind Chrome. Nothing on a desktop claims that scheme, so
133+ it is now nil there — which `core/capture-html` already documented as the
134+ desktop case and already handled.
135+
136+### The APK
137+
86138 Impure on purpose. Gradle resolves its own dependencies over the network and
87139 installs build-tools and a platform into `ANDROID_HOME` as it goes, so it can
88140 neither run in a sandbox nor write to the store. What nix gives is the
@@ -76,13 +76,65 @@ plugins and is its own project.
76 76
77 ## Building it77 ## Building it
78 78
79+Two targets out of one tree. The ClojureDart compile is the same command for
80+both — `clojure -M:cljd compile` over `src/` and `../common` — and what differs
81+is only what Flutter is asked to wrap it in.
82+
79 ```bash83 ```bash
80-just apk # the debug APK84+just apk # the debug APK
81-just apk install # and onto a connected device85+just apk install # and onto a connected device
82-just apk run # and launched86+just apk run # and launched
83-just apk log # logcat87+just apk log # logcat
88+
89+just flutter-desktop # the debug Linux bundle
90+just flutter-desktop run # and the window
84 ```91 ```
85 92
93+### The desktop one
94+
95+There are two desktop GUIs now, and they are not a fallback for each other:
96+`just run` is libcosmic under jolt, and `just flutter-desktop` is this tree
97+under Flutter's Linux target. Same screens out of `common/frq/screens/`, two
98+renderers — `glimmer-cosmic` walks the hiccup on one side and `frq.hiccup`
99+emits Flutter widgets on the other.
100+
101+Its toolchain is `devShells.flutter-desktop`, which is the APK shell with the
102+Android half swapped out: clojure and Flutter are the same two packages at the
103+same pinned rev, and CMake, Ninja, pkg-config and GTK stand where the JDK and
104+the SDK do. Kept separate rather than merged into one shell because the halves
105+are disjoint — a desktop build has no use for a few hundred megabytes of
106+Android SDK, which is the same argument that keeps Flutter out of the default
107+shell.
108+
109+Still impure, for one of the two reasons the APK is: pub.dev resolution and
110+Flutter's engine artifacts are network. What it does *not* need is the
111+writable-`ANDROID_HOME` dance, since nothing here writes into the store — so
112+there is no `flutter/.home` on this path.
113+
114+nixGL off NixOS, for the reason `just run` needs it and `just tui` does not:
115+Flutter paints through GL and the driver that can do that is the host's.
116+
117+`linux/` is the Flutter template's GTK runner, renamed — `frq` rather than
118+`cljd_flutter`, and `uk.nandi.frq` rather than `com.example.cljd_flutter`, so
119+the binary, the window title and the GTK application id agree with the APK's
120+`applicationId`.
121+
122+Two things the desktop target changed in the Dart, both of them cases where
123+"the phone" had been assumed rather than asked:
124+
125+* `frq.io.dart/write-private-file!` was a plain write, on the grounds that
126+ Android storage is already private to the app. On a Linux desktop it is not:
127+ the file lands under the XDG data directory with the process umask, and it
128+ holds a broker token. The desktop branch now does what `frq.io.jolt` does —
129+ create, chmod, then write — and Dart having no chmod is why that is a
130+ process.
131+* `frq.oauth.dart` handed the capture page `frq://auth` unconditionally, to
132+ raise the app from behind Chrome. Nothing on a desktop claims that scheme, so
133+ it is now nil there — which `core/capture-html` already documented as the
134+ desktop case and already handled.
135+
136+### The APK
137+
86 Impure on purpose. Gradle resolves its own dependencies over the network and138 Impure on purpose. Gradle resolves its own dependencies over the network and
87 installs build-tools and a platform into `ANDROID_HOME` as it goes, so it can139 installs build-tools and a platform into `ANDROID_HOME` as it goes, so it can
88 neither run in a sandbox nor write to the store. What nix gives is the140 neither run in a sandbox nor write to the store. What nix gives is the
modified flutter/linux/CMakeLists.txt +2 -2
@@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
44
55 # The name of the executable created for the application. Change this to change
66 # the on-disk name of your application.
7-set(BINARY_NAME "cljd_flutter")
7+set(BINARY_NAME "frq")
88 # The unique GTK application identifier for this application. See:
99 # https://wiki.gnome.org/HowDoI/ChooseApplicationID
10-set(APPLICATION_ID "com.example.cljd_flutter")
10+set(APPLICATION_ID "uk.nandi.frq")
1111
1212 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
1313 # versions of CMake.
@@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
4 4
5 # The name of the executable created for the application. Change this to change5 # The name of the executable created for the application. Change this to change
6 # the on-disk name of your application.6 # the on-disk name of your application.
7-set(BINARY_NAME "cljd_flutter")7+set(BINARY_NAME "frq")
8 # The unique GTK application identifier for this application. See:8 # The unique GTK application identifier for this application. See:
9 # https://wiki.gnome.org/HowDoI/ChooseApplicationID9 # https://wiki.gnome.org/HowDoI/ChooseApplicationID
10-set(APPLICATION_ID "com.example.cljd_flutter")10+set(APPLICATION_ID "uk.nandi.frq")
11 11
12 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent12 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
13 # versions of CMake.13 # versions of CMake.
modified flutter/linux/runner/my_application.cc +2 -2
@@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
4545 if (use_header_bar) {
4646 GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
4747 gtk_widget_show(GTK_WIDGET(header_bar));
48- gtk_header_bar_set_title(header_bar, "cljd_flutter");
48+ gtk_header_bar_set_title(header_bar, "frq");
4949 gtk_header_bar_set_show_close_button(header_bar, TRUE);
5050 gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
5151 } else {
52- gtk_window_set_title(window, "cljd_flutter");
52+ gtk_window_set_title(window, "frq");
5353 }
5454
5555 gtk_window_set_default_size(window, 1280, 720);
@@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
45 if (use_header_bar) {45 if (use_header_bar) {
46 GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());46 GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
47 gtk_widget_show(GTK_WIDGET(header_bar));47 gtk_widget_show(GTK_WIDGET(header_bar));
48- gtk_header_bar_set_title(header_bar, "cljd_flutter");48+ gtk_header_bar_set_title(header_bar, "frq");
49 gtk_header_bar_set_show_close_button(header_bar, TRUE);49 gtk_header_bar_set_show_close_button(header_bar, TRUE);
50 gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));50 gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
51 } else {51 } else {
52- gtk_window_set_title(window, "cljd_flutter");52+ gtk_window_set_title(window, "frq");
53 }53 }
54 54
55 gtk_window_set_default_size(window, 1280, 720);55 gtk_window_set_default_size(window, 1280, 720);
modified flutter/src/frq/io/dart.cljd +32 -10
@@ -1,5 +1,5 @@
11 (ns frq.io.dart
2- "The phone's answers to `frq.io`, over dart:io.
2+ "Flutter's answers to `frq.io`, over dart:io.
33
44 The mirror of `frq.io.jolt`, and the reason the seam exists at all: every
55 line here is something the desktop gets from libc through jolt.ffi and the
@@ -12,9 +12,11 @@
1212 Flutter entry point awaits it and calls `install!` with the answer before it
1313 runs anything else.
1414
15- UNVERIFIED. Nothing in this tree compiles ClojureDart yet there is no cljd
16- toolchain in the flake and no Flutter SDK so treat the interop here as the
17- shape it should take rather than as code known to compile."
15+ Two Flutter targets share this file, not one: `just apk` and
16+ `just flutter-desktop` compile the same ClojureDart, so \"the phone\" is no
17+ longer a safe assumption to bury in an implementation here. Where the answer
18+ genuinely differs by platform it is asked rather than assumed see
19+ `write-private-file!`, which is the one place the difference has teeth."
1820 (:require ["dart:convert" :as conv]
1921 ["dart:io" :as io]
2022 ["package:url_launcher/url_launcher.dart" :as launcher]
@@ -37,13 +39,33 @@
3739 (catch Exception _ false)))
3840
3941 (defn- write-private-file!
40- "On Android the app's own storage is already private to the app — there is no
41- other uid that can read it so mode 600 has nobody to exclude and this is an
42- ordinary write. That is the whole reason `frq.io` names the intent: the
43- desktop shells out to `install -m 600` for the same guarantee the platform
44- makes here for free."
42+ "600 where that means something, an ordinary write where it does not.
43+
44+ On Android the app's own storage is already private to the app — there is no
45+ other uid that can read it so mode 600 has nobody to exclude, and this is
46+ the ordinary write it always was. On a Linux desktop, where this same backend
47+ now paints the Flutter window, there very much is another uid: the file lands
48+ under the XDG data directory with the process umask, which is world-readable
49+ on most systems, and what it holds is a broker token.
50+
51+ So the desktop branch does what `frq.io.jolt` does and in the same order
52+ create, restrict, *then* write so the token is never on disk readable even
53+ for an instant. Dart has no chmod of its own, which is why this is a process
54+ and not a call.
55+
56+ This is the seam earning its keep. `frq.io` names the intent rather than the
57+ mechanism, so one `write-private-file!` can mean three different things and
58+ no caller has to know which."
4559 [path s]
46- (spit* path s))
60+ (if (.-isAndroid io/Platform)
61+ (spit* path s)
62+ (try
63+ (let [f (io/File. path)]
64+ (when-not (.existsSync f) (.createSync f .recursive true))
65+ (io/Process.runSync "chmod" ["600" path])
66+ (.writeAsStringSync f s)
67+ true)
68+ (catch Exception _ false))))
4769
4870 (defn- local-offset-seconds
4971 "Dart carries the zone in the runtime; no TZ, no /etc/localtime, no getprop.
@@ -1,5 +1,5 @@
1 (ns frq.io.dart1 (ns frq.io.dart
2- "The phone's answers to `frq.io`, over dart:io.2+ "Flutter's answers to `frq.io`, over dart:io.
3 3
4 The mirror of `frq.io.jolt`, and the reason the seam exists at all: every4 The mirror of `frq.io.jolt`, and the reason the seam exists at all: every
5 line here is something the desktop gets from libc through jolt.ffi and the5 line here is something the desktop gets from libc through jolt.ffi and the
@@ -12,9 +12,11 @@
12 Flutter entry point awaits it and calls `install!` with the answer before it12 Flutter entry point awaits it and calls `install!` with the answer before it
13 runs anything else.13 runs anything else.
14 14
15- UNVERIFIED. Nothing in this tree compiles ClojureDart yet there is no cljd15+ Two Flutter targets share this file, not one: `just apk` and
16- toolchain in the flake and no Flutter SDK so treat the interop here as the16+ `just flutter-desktop` compile the same ClojureDart, so \"the phone\" is no
17- shape it should take rather than as code known to compile."17+ longer a safe assumption to bury in an implementation here. Where the answer
18+ genuinely differs by platform it is asked rather than assumed see
19+ `write-private-file!`, which is the one place the difference has teeth."
18 (:require ["dart:convert" :as conv]20 (:require ["dart:convert" :as conv]
19 ["dart:io" :as io]21 ["dart:io" :as io]
20 ["package:url_launcher/url_launcher.dart" :as launcher]22 ["package:url_launcher/url_launcher.dart" :as launcher]
@@ -37,13 +39,33 @@
37 (catch Exception _ false)))39 (catch Exception _ false)))
38 40
39 (defn- write-private-file!41 (defn- write-private-file!
40- "On Android the app's own storage is already private to the app — there is no42+ "600 where that means something, an ordinary write where it does not.
41- other uid that can read it so mode 600 has nobody to exclude and this is an43+
42- ordinary write. That is the whole reason `frq.io` names the intent: the44+ On Android the app's own storage is already private to the app — there is no
43- desktop shells out to `install -m 600` for the same guarantee the platform45+ other uid that can read it so mode 600 has nobody to exclude, and this is
44- makes here for free."46+ the ordinary write it always was. On a Linux desktop, where this same backend
47+ now paints the Flutter window, there very much is another uid: the file lands
48+ under the XDG data directory with the process umask, which is world-readable
49+ on most systems, and what it holds is a broker token.
50+
51+ So the desktop branch does what `frq.io.jolt` does and in the same order
52+ create, restrict, *then* write so the token is never on disk readable even
53+ for an instant. Dart has no chmod of its own, which is why this is a process
54+ and not a call.
55+
56+ This is the seam earning its keep. `frq.io` names the intent rather than the
57+ mechanism, so one `write-private-file!` can mean three different things and
58+ no caller has to know which."
45 [path s]59 [path s]
46- (spit* path s))60+ (if (.-isAndroid io/Platform)
61+ (spit* path s)
62+ (try
63+ (let [f (io/File. path)]
64+ (when-not (.existsSync f) (.createSync f .recursive true))
65+ (io/Process.runSync "chmod" ["600" path])
66+ (.writeAsStringSync f s)
67+ true)
68+ (catch Exception _ false))))
47 69
48 (defn- local-offset-seconds70 (defn- local-offset-seconds
49 "Dart carries the zone in the runtime; no TZ, no /etc/localtime, no getprop.71 "Dart carries the zone in the runtime; no TZ, no /etc/localtime, no getprop.
modified flutter/src/frq/oauth/dart.cljd +17 -6
@@ -21,11 +21,22 @@
2121 [frq.io :as host]
2222 [frq.oauth.core :as core]))
2323
24-(def ^:private return-url
25- "What brings the app back to the front once the browser is done. The activity
26- is `singleTask`, so this raises the app that is already running rather than
27- starting a second copy of it."
28- "frq://auth")
24+(defn- return-url
25+ "What brings the app back to the front once the browser is done, or nil where
26+ nothing needs to.
27+
28+ On Android the browser is in front of the app and something has to raise it:
29+ `frq://auth` is a scheme the manifest claims, and the activity is
30+ `singleTask`, so it raises the app that is already running rather than
31+ starting a second copy of it.
32+
33+ On a Linux desktop the window is already beside the browser and the reader
34+ switches to it, exactly as it is for `frq.oauth` under libcosmic — and
35+ nothing on that machine claims `frq://`, so offering the link would be a
36+ button that does nothing. `core/capture-html` takes nil for precisely this
37+ and says so; this is the caller it was written for."
38+ []
39+ (when (.-isAndroid io/Platform) "frq://auth"))
2940
3041 (defn ^:async await-callback!
3142 "Serve the loopback capture until the browser posts the handoff back.
@@ -79,7 +90,7 @@
7990 (do
8091 (.set (.-headers resp) "content-type"
8192 "text/html; charset=utf-8")
82- (.write resp (core/capture-html return-url))
93+ (.write resp (core/capture-html (return-url)))
8394 (.close resp)))
8495 nil)))
8596 (await (.-future done))
@@ -21,11 +21,22 @@
21 [frq.io :as host]21 [frq.io :as host]
22 [frq.oauth.core :as core]))22 [frq.oauth.core :as core]))
23 23
24-(def ^:private return-url24+(defn- return-url
25- "What brings the app back to the front once the browser is done. The activity25+ "What brings the app back to the front once the browser is done, or nil where
26- is `singleTask`, so this raises the app that is already running rather than26+ nothing needs to.
27- starting a second copy of it."27+
28- "frq://auth")28+ On Android the browser is in front of the app and something has to raise it:
29+ `frq://auth` is a scheme the manifest claims, and the activity is
30+ `singleTask`, so it raises the app that is already running rather than
31+ starting a second copy of it.
32+
33+ On a Linux desktop the window is already beside the browser and the reader
34+ switches to it, exactly as it is for `frq.oauth` under libcosmic — and
35+ nothing on that machine claims `frq://`, so offering the link would be a
36+ button that does nothing. `core/capture-html` takes nil for precisely this
37+ and says so; this is the caller it was written for."
38+ []
39+ (when (.-isAndroid io/Platform) "frq://auth"))
29 40
30 (defn ^:async await-callback!41 (defn ^:async await-callback!
31 "Serve the loopback capture until the browser posts the handoff back.42 "Serve the loopback capture until the browser posts the handoff back.
@@ -79,7 +90,7 @@
79 (do90 (do
80 (.set (.-headers resp) "content-type"91 (.set (.-headers resp) "content-type"
81 "text/html; charset=utf-8")92 "text/html; charset=utf-8")
82- (.write resp (core/capture-html return-url))93+ (.write resp (core/capture-html (return-url)))
83 (.close resp)))94 (.close resp)))
84 nil)))95 nil)))
85 (await (.-future done))96 (await (.-future done))
modified justfile +52 -0
@@ -271,3 +271,55 @@ gen-moq lib="":
271271 { sed -n '1,/^ (:require \[jolt.ffi :as ffi\]))$/p' src/frq/moq/raw.clj; echo; cat "$work/body.clj"; } > "$work/raw.clj"
272272 mv "$work/raw.clj" src/frq/moq/raw.clj
273273 echo "wrote src/frq/moq/raw.clj ($(grep -c '^(ffi/defcfn' src/frq/moq/raw.clj) entry points)"
274+
275+# The other desktop GUI: the same screens, painted by Flutter instead of
276+# libcosmic.
277+#
278+# `just run` and this one are two frontends over one tree, and the split is
279+# the same one the APK already draws. Everything under `common/` — the
280+# screens, the cells, `frq.io` — is shared; what differs is who paints it and
281+# who answers the host. So this recipe is `just apk` with the Android half
282+# taken out: the same `clojure -M:cljd compile` over the same flutter/src,
283+# then Flutter's Linux target rather than its Android one. CMake and Ninja
284+# instead of Gradle, `flutter/linux/` as the runner, no SDK and no JDK.
285+#
286+# Still impure, for one of the two reasons `apk` is: pub.dev resolution and
287+# Flutter's own engine artifacts are network. What it does NOT need is the
288+# writable-ANDROID_HOME dance — nothing here writes into the store — so there
289+# is no `flutter/.home` on this path.
290+#
291+# nixGL for the reason `run` needs it and `tui` does not: Flutter paints
292+# through GL, and off NixOS the driver is the host's.
293+#
294+# just flutter-desktop build the debug bundle
295+# just flutter-desktop run build it and open the window
296+flutter-desktop action="build":
297+ #!/usr/bin/env bash
298+ set -euo pipefail
299+ cd "{{justfile_directory()}}"
300+ if [ -z "${FRQ_FLUTTER_DESKTOP:-}" ]; then
301+ exec {{nix}} develop .#flutter-desktop --max-jobs {{jobs}} \
302+ --command just flutter-desktop "$@"
303+ fi
304+ cd flutter
305+
306+ clojure -M:cljd compile
307+ flutter build linux --debug
308+
309+ # x64/arm64 is Flutter's own name for the host arch, not uname's.
310+ case "$(uname -m)" in
311+ x86_64) arch=x64 ;;
312+ aarch64) arch=arm64 ;;
313+ *) echo "unknown arch $(uname -m)" >&2; exit 1 ;;
314+ esac
315+ bundle="build/linux/$arch/debug/bundle"
316+
317+ case "{{action}}" in
318+ build) echo "built $PWD/$bundle/frq" ;;
319+ run)
320+ runner=()
321+ [ -e /run/current-system ] || runner=("$NIXGL")
322+ exec "${runner[@]}" "$bundle/frq"
323+ ;;
324+ *) echo "usage: just flutter-desktop [build|run]" >&2; exit 1 ;;
325+ esac
@@ -271,3 +271,55 @@ gen-moq lib="":
271 { sed -n '1,/^ (:require \[jolt.ffi :as ffi\]))$/p' src/frq/moq/raw.clj; echo; cat "$work/body.clj"; } > "$work/raw.clj"271 { sed -n '1,/^ (:require \[jolt.ffi :as ffi\]))$/p' src/frq/moq/raw.clj; echo; cat "$work/body.clj"; } > "$work/raw.clj"
272 mv "$work/raw.clj" src/frq/moq/raw.clj272 mv "$work/raw.clj" src/frq/moq/raw.clj
273 echo "wrote src/frq/moq/raw.clj ($(grep -c '^(ffi/defcfn' src/frq/moq/raw.clj) entry points)"273 echo "wrote src/frq/moq/raw.clj ($(grep -c '^(ffi/defcfn' src/frq/moq/raw.clj) entry points)"
274+
275+# The other desktop GUI: the same screens, painted by Flutter instead of
276+# libcosmic.
277+#
278+# `just run` and this one are two frontends over one tree, and the split is
279+# the same one the APK already draws. Everything under `common/` — the
280+# screens, the cells, `frq.io` — is shared; what differs is who paints it and
281+# who answers the host. So this recipe is `just apk` with the Android half
282+# taken out: the same `clojure -M:cljd compile` over the same flutter/src,
283+# then Flutter's Linux target rather than its Android one. CMake and Ninja
284+# instead of Gradle, `flutter/linux/` as the runner, no SDK and no JDK.
285+#
286+# Still impure, for one of the two reasons `apk` is: pub.dev resolution and
287+# Flutter's own engine artifacts are network. What it does NOT need is the
288+# writable-ANDROID_HOME dance — nothing here writes into the store — so there
289+# is no `flutter/.home` on this path.
290+#
291+# nixGL for the reason `run` needs it and `tui` does not: Flutter paints
292+# through GL, and off NixOS the driver is the host's.
293+#
294+# just flutter-desktop build the debug bundle
295+# just flutter-desktop run build it and open the window
296+flutter-desktop action="build":
297+ #!/usr/bin/env bash
298+ set -euo pipefail
299+ cd "{{justfile_directory()}}"
300+ if [ -z "${FRQ_FLUTTER_DESKTOP:-}" ]; then
301+ exec {{nix}} develop .#flutter-desktop --max-jobs {{jobs}} \
302+ --command just flutter-desktop "$@"
303+ fi
304+ cd flutter
305+
306+ clojure -M:cljd compile
307+ flutter build linux --debug
308+
309+ # x64/arm64 is Flutter's own name for the host arch, not uname's.
310+ case "$(uname -m)" in
311+ x86_64) arch=x64 ;;
312+ aarch64) arch=arm64 ;;
313+ *) echo "unknown arch $(uname -m)" >&2; exit 1 ;;
314+ esac
315+ bundle="build/linux/$arch/debug/bundle"
316+
317+ case "{{action}}" in
318+ build) echo "built $PWD/$bundle/frq" ;;
319+ run)
320+ runner=()
321+ [ -e /run/current-system ] || runner=("$NIXGL")
322+ exec "${runner[@]}" "$bundle/frq"
323+ ;;
324+ *) echo "usage: just flutter-desktop [build|run]" >&2; exit 1 ;;
325+ esac