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

Let the lightbox save the picture it is showing e5adf58 · on ce824252aa511f0e1d68bf8fbf6be9419ab4dfd1 · nandi · 7d ago
io.cljc · 156 lines · 6.1 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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
(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 save-to-downloads!
  "Put a copy of the file at `path` where this reader keeps the things they
  save, under `filename`, and answer with where it landed  or nil.

  Named for the result, like the rest of the seam, because \"where downloads
  go\" is a different question on each of the three targets this has to answer
  on: an XDG directory on a Linux desktop, the shared Download store on
  Android, the app's own storage where neither of those is there. The caller
  has a picture out of the media cache and wants it kept somewhere a file
  manager will find it; which directory that is is the host's business.

  The name is a request rather than a promise: a file already there is not
  overwritten, so what comes back may be `picture-1.png` for a `picture.png`
  that was asked for. Callers show the answer, which is the only honest way to
  say where a thing went."
  [path filename]
  (call :save-to-downloads! [path filename]))

(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]))

(defn after!
  "Run `f` in about `ms` milliseconds, wherever this host runs UI work.

  Named for when rather than for how, like the rest of this seam. The window
  lends the toolkit's own timer — a callback off the UI thread repaints from
  the wrong one  and the phone has an event loop already and needs no
  lending. What hangs on it is the grace period for crossing from a face to
  the card it raised: see `frq.profile/unhover!`."
  [ms f]
  (call :after! [ms f])
  nil)