nandi/frqpublic Fork 0
23b79998fbb128adef99c3a4909ba4f6057d9b55
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.

Sign in with Bluesky on the phone, browser and all 939d2ac · on 23b79998fbb128adef99c3a4909ba4f6057d9b55 · nandi · 7d ago
io.cljc · 126 lines · 4.7 KBGDScript3 Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
(ns frq.io
  "What the host does, named once so both compilers can answer it.

  `src/` is jolt: jolt.host, jolt.socket, jolt.ffi, a Chez runtime and glimmer
  under the screens. `android/src/` is ClojureDart: dart:io, dart:ffi and
  Flutter. Everything in `common/` is compiled by both, so it cannot mention
  either  a `(:require [jolt.host])` at the top of a namespace is what keeps
  it out of the Android build, not anything about what the code does.

  So this is the seam. Nothing here has an implementation; the backend installs
  one before anything else runs  `frq.io.jolt` on the desktop side,
  `frq.io.dart` on the phone. A namespace under `common/` requires this and
  stays portable.

  The functions are chosen by *intent* rather than by what either platform
  happens to call it. `write-private-file!` rather than a chmod, because Dart
  has no chmod and jolt has no `File.setPermissions`; `local-offset-seconds`
  rather than a zone name, because finding the zone is four platform-specific
  guesses on Linux and one property read on Android. Anywhere the seam names a
  mechanism instead of a result, one of the two sides ends up faking it."
  (:refer-clojure :exclude [slurp spit]))

(defonce ^:private impl
  ;; Keyword  fn. Empty until a backend installs into it, which is a load-time
  ;; effect of requiring `frq.io.jolt` or `frq.io.dart`.
  (atom {}))

(defn install!
  "Register the host's answers. Called once, by the backend, before `-main`
  does anything  see the require list of `frq.app` and of the Flutter entry
  point. Merges, so a backend may install in pieces."
  [m]
  (swap! impl merge m)
  nil)

(defn installed?
  "Whether a backend has answered yet. For the entry points to assert on; the
  wrappers below throw on their own."
  []
  (boolean (seq @impl)))

(defn- call
  [k args]
  (if-let [f (get @impl k)]
    (apply f args)
    (throw (ex-info (str "frq.io: no host installed for " k
                         " — require frq.io.jolt (desktop) or frq.io.dart (android) first")
                    {:op k}))))

;; ------------------------------------------------------------ environment

(defn getenv [n] (call :getenv [n]))

(defn config-dir
  "The directory this client keeps its own files in, already frq-specific.

  Not a rule about XDG: on Android there is no `HOME` and no config directory
  to be relative to, and what the platform hands back is the app's own storage.
  The caller wants somewhere to put `session.edn` and does not care which."
  []
  (call :config-dir []))

;; ------------------------------------------------------------------ files

(defn file-exists? [path] (call :file-exists? [path]))
(defn directory? [path] (call :directory? [path]))
(defn list-dir [path] (call :list-dir [path]))
(defn mkdirs! [path] (call :mkdirs! [path]))
(defn delete-file! [path] (call :delete-file! [path]))

(defn slurp
  "The file as a string, or nil where it cannot be read. Shadows core's, which
  wants a JVM reader."
  [path]
  (call :slurp [path]))

(defn spit
  "Write the string whole. True when it landed."
  [path s]
  (call :spit [path s]))

(defn write-private-file!
  "`spit`, for a file nobody else may read — mode 600 where that means
  something. The broker token goes through this and nothing else does."
  [path s]
  (call :write-private-file! [path s]))

;; -------------------------------------------------------------------- text

(defn utf8-bytes
  "A string as a sequence of byte values, 0-255.

  In the seam because there is no portable way to say it: jolt has
  `.getBytes`, which is Java, and ClojureDart has `dart:convert`. `frq.atproto`
  needs it for base64url  SASL is bytes, and a handle with a non-ASCII
  character in it encodes to more of them than it has characters."
  [s]
  (call :utf8-bytes [s]))

(defn utf8-string
  "The inverse: byte values back to the text they spell."
  [bytes]
  (call :utf8-string [bytes]))

;; ------------------------------------------------------------------- time

(defn open-url!
  "Hand `url` to whatever shows web pages here, and say whether that worked.

  Named for the result and not the mechanism, like the rest of this seam: the
  desktop shells out to the portal and the phone asks Android to pick an
  activity, and neither is the other's business. A false answer is not fatal —
  the OAuth screen shows the URL so it can be opened by hand."
  [url]
  (boolean (call :open-url! [url])))

(defn wall-nanos [] (call :wall-nanos []))
(defn mono-nanos [] (call :mono-nanos []))

(defn local-offset-seconds
  "How far the reader's zone is from UTC at `epoch-secs`, DST included.

  An instant rather than a constant: the offset moves twice a year, and a
  backlog read in November carries messages from August."
  [epoch-secs]
  (call :local-offset-seconds [epoch-secs]))