Commitdf0e77eaRecorded24 Jul 2026Repositoryslate

slate: pty terminal default + runtime tmux toggle; loopback owner-pairing; slate-shell node renderer + serve auto-launch

Message

Three stream-readiness changes (ship-prep, HARD STOP before merge/tag).

1. Terminal default → pure pty; tmux opt-in via a runtime setting (slate/main.sgl): new persisted app-db setting slate/terminals-use-tmux (default off). tmux-available? reads it live, so a fresh terminal honors the current value with no rebuild (the live-demo affordance). Command-palette toggle + set-options! settable. tmux code untouched; removed the unused hardcoded tmux-backing.

2. Silent owner-pairing default on a loopback bind (slate-cli config/main): silent-pair-grants-for-host keys the grant posture on the bind address — loopback → full owner posture (fs:rw:~ fs:ro:/ exec:full pty:on) with no env var, so opening a terminal works out of the box; non-loopback → the safe restricted default. New --host serve option; banner reports the mode. SLATESILENTPAIR_OWNER preserved as an override. +7 unit tests.

3. slate-shell node renderer + serve auto-launch (slate-shell/main, slate-cli launch/main): slate-shell --url <origin> is a pure webview onto a running node (no lantern-system/services); bare slate-shell keeps the embedded standalone fallback. slate serve auto-launches slate-shell when on $PATH; it is also rung 1 of the renderer ladder. (Live window render needs a companion lantern nav-handler fix, landing via dev-redirect.)

Changed
 packages/slate-cli/src/slate-cli/config.sgl   | 37 ++++++++++++++++++++++++++++++++++++-
 packages/slate-cli/src/slate-cli/launch.sgl   | 34 +++++++++++++++++++++++++++++++++-
 packages/slate-cli/src/slate-cli/main.sgl     | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
 packages/slate-cli/test/test-silent-pair.sgl  | 43 +++++++++++++++++++++++++++++++++++++++++++
 packages/slate-shell/src/slate-shell/main.sgl | 30 ++++++++++++++++++++++++++++--
 packages/slate/src/slate/main.sgl             | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
 6 files changed, 254 insertions(+), 34 deletions(-)
Diff
packages/slate-cli/src/slate-cli/config.sglmodified
@@ -41,7 +41,9 @@
41
default-silent-pair-grants
42
owner-silent-pair-grants
43
served-origins-for-port
44
config-with-served-origins)
+44
config-with-served-origins
+45
loopback-host?
+46
silent-pair-grants-for-host)
47
48
(begin
49
@@ -215,6 +217,39 @@
217
(and (>= (string-length s) (string-length prefix))
218
(string=? (substring s 0 (string-length prefix)) prefix)))
219
+220
;; A bind HOST names the loopback interface iff it is one of the local
+221
;; loopback authorities. Only a loopback bind gets the owner silent-pair
+222
;; posture by default (Change 3, David 2026-07-24): the sole client that can
+223
;; reach a loopback bind is a process on THIS machine, so auto-granting the
+224
;; owner posture is safe. A non-loopback bind (0.0.0.0 / a network interface)
+225
;; is NOT loopback, so it keeps the restricted default unless the owner
+226
;; explicitly opts in — the node never hands full owner access to a
+227
;; potentially network-reachable client without a deliberate choice.
+228
(define (loopback-host? host)
+229
(or (string=? host "127.0.0.1")
+230
(string=? host "localhost")
+231
(string=? host "::1")
+232
(string=? host "[::1]")))
+233
+234
;; The silent-pair grant posture for a given BIND HOST (Change 3, David
+235
;; 2026-07-24). Keyed on the bind address, NOT an env var:
+236
;; - a loopback bind (the default `slate serve`) → the full OWNER posture,
+237
;; so the local "try it" / auto-launched slate-shell experience works with
+238
;; no env var (opening a terminal needs pty:on + exec:full);
+239
;; - a non-loopback bind (0.0.0.0 / a network interface) → the safe
+240
;; restricted default (fs:rw:~ only); owner caps are never auto-granted to
+241
;; a potentially network-reachable client;
+242
;; - SLATE_SILENT_PAIR_OWNER=1 remains an explicit override that forces the
+243
;; owner posture regardless of bind (preserved; no longer REQUIRED locally).
+244
(define (silent-pair-grants-for-host host)
+245
(if (or (owner-env-override?) (loopback-host? host))
+246
owner-silent-pair-grants
+247
default-silent-pair-grants))
+248
+249
(define (owner-env-override?)
+250
(let ((v (getenv "SLATE_SILENT_PAIR_OWNER")))
+251
(and v (or (string=? v "1") (string=? v "true") (string=? v "yes")))))
+252
253
;; $XDG_STATE_HOME/slate, else ~/.local/state/slate. Windows
254
;; (%LOCALAPPDATA%) is a named follow-up; here we target POSIX.
255
(define (discover-state-dir)
packages/slate-cli/src/slate-cli/launch.sglmodified
@@ -30,10 +30,21 @@
30
open-renderer!
31
detect-chromium
32
chromium-candidates
33
build-app-argv)
+33
build-app-argv
+34
detect-slate-shell
+35
serve-launch-shell!)
36
37
(begin
38
+39
;; ---- rung 1: Slate Shell (the native window, design §3.2) -------------
+40
;; The slate-shell binary (the ex-slate-desktop Lantern wrapper) renders a
+41
;; running node's origin as a chromeless native window. It is preferred over
+42
;; a browser when installed. `slate-shell --url <origin>` is its node-
+43
;; renderer role (a pure webview onto the node, system access over the WS).
+44
(define (detect-slate-shell) (command-exists? "slate-shell"))
+45
+46
(define (slate-shell-argv url) (list "--url" url))
+47
48
;; PATH probe order (design §3.2 / spike probe 2). google-chrome
49
;; first (the spike's environment), then the common chromium builds.
50
(define chromium-names
@@ -64,6 +75,10 @@
75
(define (launch-plan url)
76
(let ((chrome (detect-chromium)))
77
(cond
+78
;; Rung 1: the native Slate Shell window when installed (design §3.2).
+79
((detect-slate-shell)
+80
#{ rung: 'slate-shell browser: "slate-shell"
+81
url: url argv: (slate-shell-argv url) })
82
(chrome
83
#{ rung: 'chromium-app
84
browser: chrome
@@ -81,6 +96,10 @@
96
(let* ((plan (launch-plan url))
97
(rung (dict-ref plan rung: 'print)))
98
(cond
+99
((eq? rung 'slate-shell)
+100
(announce "Opening Slate in a native window (slate-shell)…")
+101
(unless (spawn-detached "slate-shell" (dict-ref plan argv: '()))
+102
(print-url url)))
103
((eq? rung 'chromium-app)
104
(announce (string-append "Opening Slate in "
105
(dict-ref plan browser: "chrome") " (app mode)…"))
@@ -101,6 +120,19 @@
120
;; win: App Paths registry probe -> chrome.exe --app=<url>, else
121
;; (spawn-detached "cmd" (list "/c" "start" "" url))
122
+123
;;; `slate serve` hook: if slate-shell is installed, auto-launch it as a
+124
;;; node renderer onto `url` (the click-to-run local-app experience). Unlike
+125
;;; open-renderer!, this does NOT fall back to a browser — serve stays
+126
;;; headless when slate-shell is absent (the caller already printed the
+127
;;; origin). Returns #t iff the shell was spawned.
+128
(define (serve-launch-shell! url)
+129
(and (detect-slate-shell)
+130
(begin
+131
(announce "slate-shell found on PATH — opening the native window…")
+132
(or (spawn-detached "slate-shell" (slate-shell-argv url))
+133
(begin (announce " (slate-shell failed to spawn; staying headless)")
+134
#f)))))
+135
136
(define (spawn-detached cmd argv)
137
(guard (e (#t #f))
138
(let ((p (apply process-spawn cmd argv)))
packages/slate-cli/src/slate-cli/main.sglmodified
@@ -90,27 +90,33 @@
90
(define (run-serve! args)
91
(with-async
92
(let* ((opts (parse-serve-opts args))
+93
(host (bind-host-of opts))
94
(config (config-from-opts opts)))
95
(assert-serving-safe! config)
96
(let* ((candidates (port-candidates-for opts config))
96
(bound (bring-up-serve! config candidates)))
+97
(bound (bring-up-serve! config host candidates)))
98
(cond
99
((not bound)
100
(error "slate serve: no loopback port available" candidates))
101
(else
101
(print-serve-banner config (car bound) (caddr bound))
+102
(print-serve-banner config host (car bound) (caddr bound))
+103
;; Change 2: auto-launch slate-shell as a native node renderer
+104
;; when it is on $PATH (the click-to-run local-app experience);
+105
;; stay headless (just the banner above) when it is absent.
+106
(serve-launch-shell! (origin-url (car bound)))
107
(forever-sleep)))))))
108
109
(define (run-launch! args)
110
(with-async
111
(let* ((opts (parse-serve-opts args))
+112
(host (bind-host-of opts))
113
(config (config-from-opts opts)))
114
(assert-serving-safe! config)
115
(let* ((candidates (port-candidates-for opts config))
110
(bound (bring-up-serve! config candidates)))
+116
(bound (bring-up-serve! config host candidates)))
117
(cond
118
(bound
113
(print-serve-banner config (car bound) (caddr bound))
+119
(print-serve-banner config host (car bound) (caddr bound))
120
(open-renderer! (origin-url (car bound)))
121
(forever-sleep))
122
(else
@@ -157,13 +163,13 @@
163
;; Bind on the first free candidate port, start the background loops
164
;; (revocation watch + session reaper), and return (port listener
165
;; daemon), or #f when every candidate is busy.
160
(define (bring-up-serve! config candidates)
+166
(define (bring-up-serve! config host candidates)
167
(let* ((store (origins-load! (make-origins-store (origins-path config))))
168
(pstate (make-pair-state))
169
(registry (make-conn-registry))
170
(hub (make-session-hub))
171
(instance (make-instance-id))
166
(bound (try-bind candidates config store pstate registry hub instance)))
+172
(bound (try-bind host candidates config store pstate registry hub instance)))
173
(when bound
174
;; Revocation support (finding #1): reload origins.json every
175
;; tick + drop live connections whose origin was revoked.
@@ -172,7 +178,7 @@
178
(start-reaper! hub))
179
bound))
180
175
(define (try-bind ports config store pstate registry hub instance)
+181
(define (try-bind host ports config store pstate registry hub instance)
182
(let loop ((ps ports))
183
(cond
184
((null? ps) #f)
@@ -180,7 +186,7 @@
186
(let* ((port (car ps))
187
(daemon (make-daemon port instance config store pstate registry hub))
188
(listener (guard (e (#t #f))
183
(start-listener! loopback-host port daemon
+189
(start-listener! host port daemon
190
(daemon-handlers)))))
191
(if listener
192
(list port listener daemon)
@@ -189,13 +195,13 @@
195
(define (origin-url port)
196
(string-append "http://localhost:" (number->string port) "/"))
197
192
(define (print-serve-banner config port daemon)
+198
(define (print-serve-banner config host port daemon)
199
(let ((store (dict-ref daemon origins: #f))
200
(instance (dict-ref daemon instance: "")))
201
(display "===========================================================\n")
202
(display " Slate — local capability daemon\n")
203
(display (string-append " origin : " (origin-url port) "\n"))
198
(display (string-append " node ws : ws://" loopback-host ":"
+204
(display (string-append " node ws : ws://" host ":"
205
(number->string port) "/node/ws (/ws alias)\n"))
206
(display (string-append " app mode : "
207
(if (config-app-dir config)
@@ -203,6 +209,14 @@
209
(string-append "proxy " (config-app-upstream config)))
210
"\n"))
211
(display (string-append " instance : " instance "\n"))
+212
;; Change 3: on a loopback bind the owner is silently paired with the
+213
;; full owner posture (no env var). A non-loopback bind keeps the
+214
;; explicit pairing gate — flag which mode this run is in.
+215
(display (string-append " pairing : "
+216
(if (loopback-host? host)
+217
"loopback — owner auto-paired (silent)"
+218
"non-loopback — explicit pairing required")
+219
"\n"))
220
(display (string-append " paired : "
221
(number->string (length (origins-list store)))
222
" origin(s)\n"))
@@ -215,11 +229,21 @@
229
;; ---- option parsing (shared by serve + launch) -----------------
230
;; #{ app-dir: port: app-upstream: }
231
(define (parse-serve-opts args)
218
(let loop ((a args) (opts #{ app-dir: #f port: #f app-upstream: #f silent-pair?: #t }))
+232
(let loop ((a args) (opts #{ app-dir: #f port: #f app-upstream: #f silent-pair?: #t host: #f }))
233
(cond
234
((null? a) opts)
235
((string=? (car a) "--no-silent-pair")
236
(loop (cdr a) (dict-set opts silent-pair?: #f)))
+237
;; --host <addr>: the bind address (default loopback 127.0.0.1). Only a
+238
;; loopback bind gets owner silent-pairing by default (Change 3); a
+239
;; non-loopback bind (0.0.0.0 / a network interface) is an explicit,
+240
;; advanced choice that keeps the restricted posture. NB: the Host-header
+241
;; gate still restricts reachable authorities to the loopback names, so a
+242
;; non-loopback bind is not, by itself, a network-exposed serve.
+243
((string=? (car a) "--host")
+244
(if (pair? (cdr a))
+245
(loop (cddr a) (dict-set opts host: (cadr a)))
+246
(opt-error "--host needs an address")))
247
((string=? (car a) "--app-dir")
248
(if (pair? (cdr a))
249
(loop (cddr a) (dict-set opts app-dir: (cadr a)))
@@ -246,20 +270,25 @@
270
n
271
(opt-error (string-append "invalid port: " s)))))
272
249
;; The silent-pair grant posture (§8-Q2). The shipped PUBLIC default is
250
;; the safe `fs:rw:~` only; a machine owner opts into the full owner
251
;; posture (exec/pty/read-all) explicitly with SLATE_SILENT_PAIR_OWNER
252
;; — a deliberate per-machine choice, NOT a stray env that silently
253
;; widens caps (it only ever ADDS to the owner's own machine; the config
254
;; `silent-pair-grants:` key is the general seam a config loader will use).
+273
;; The effective bind host for this run: --host wins, else loopback.
+274
(define (bind-host-of opts)
+275
(or (dict-ref opts host: #f) loopback-host))
+276
+277
;; The silent-pair grant posture (§8-Q2, Change 3 2026-07-24) is keyed on the
+278
;; BIND ADDRESS by `silent-pair-grants-for-host` (in (slate-cli config), next
+279
;; to the grant constants): a loopback bind (the default) → full owner posture
+280
;; with no env var; a non-loopback bind → the safe restricted default;
+281
;; SLATE_SILENT_PAIR_OWNER stays an explicit override.
+282
;; MULTI-USER CAVEAT: on a shared machine another local user could in principle
+283
;; connect to the loopback port — but the peer-UID accept gate refuses a
+284
;; different-user process before a byte is read, so owner silent-pairing stays
+285
;; bound to the same Unix user that runs the daemon.
286
(define (config-from-opts opts)
287
(make-config dev-mode?: (env-flag? "SLATE_DEV")
288
app-dir: (dict-ref opts app-dir: #f)
289
app-upstream: (dict-ref opts app-upstream: #f)
290
silent-pair-enabled?: (dict-ref opts silent-pair?: #t)
260
silent-pair-grants: (if (env-flag? "SLATE_SILENT_PAIR_OWNER")
261
owner-silent-pair-grants
262
#f)))
+291
silent-pair-grants: (silent-pair-grants-for-host (bind-host-of opts))))
292
293
;; Port candidates: an explicit --port wins; else local-build mode
294
;; (--app-dir) defaults to 32650 (a DISTINCT origin from proxy mode,
@@ -694,6 +723,8 @@
723
(display " --app-dir <path> serve a local PWA build (default port 32650)\n")
724
(display " --app-upstream <url> reverse-proxy upstream (default https://useslate.org)\n")
725
(display " --port <n> bind a specific port\n")
+726
(display " --host <addr> bind address (default 127.0.0.1; a loopback\n")
+727
(display " bind auto-pairs the owner with the full posture)\n")
728
(display " --no-silent-pair disable §2.5 auto-pairing of the served origin\n"))
729
730
))
packages/slate-cli/test/test-silent-pair.sglmodified
@@ -139,6 +139,49 @@
139
(make-config state-dir: dir silent-pair-grants: owner-silent-pair-grants) PORT)))
140
(assert-true (string-contains-spec? (grants-for-origin config served-http) "exec:full")))))
141
+142
;; ============================================================
+143
;; Change 3 (David 2026-07-24): silent owner-pairing is the DEFAULT on a
+144
;; loopback bind, keyed on the BIND ADDRESS (no env var). A non-loopback
+145
;; bind keeps the safe restricted default. This is what makes the local
+146
;; "try it" / auto-launched slate-shell experience work — opening a
+147
;; terminal needs pty:on + exec:full, which the old default withheld.
+148
;; NB: these run with SLATE_SILENT_PAIR_OWNER unset in the test env, so
+149
;; the branches are purely bind-address-driven.
+150
;; ============================================================
+151
(test-group "Change 3 loopback bind → owner silent-pair posture"
+152
(test "loopback host detection"
+153
(assert-true (and (loopback-host? "127.0.0.1")
+154
(loopback-host? "localhost")
+155
(loopback-host? "::1"))))
+156
(test "0.0.0.0 / a network interface is NOT loopback"
+157
(assert-false (or (loopback-host? "0.0.0.0")
+158
(loopback-host? "192.168.1.10"))))
+159
(test "loopback bind → the FULL owner posture (exec:full + pty:on)"
+160
(let ((g (silent-pair-grants-for-host "127.0.0.1")))
+161
(assert-true (and (string-contains-spec? g "exec:full")
+162
(string-contains-spec? g "pty:on")))))
+163
(test "localhost bind → owner posture too"
+164
(assert-true (string-contains-spec? (silent-pair-grants-for-host "localhost") "pty:on")))
+165
(test "non-loopback bind → the safe restricted default (no exec/pty)"
+166
(let ((g (silent-pair-grants-for-host "0.0.0.0")))
+167
(assert-true (string-contains-spec? g "fs:rw:~"))
+168
(assert-false (string-contains-spec? g "exec:full"))
+169
(assert-false (string-contains-spec? g "pty:on"))))
+170
(test "the loopback owner posture flows to the served origin end-to-end"
+171
(let* ((dir (make-temp-directory))
+172
(config (config-with-served-origins
+173
(make-config state-dir: dir
+174
silent-pair-grants: (silent-pair-grants-for-host "127.0.0.1"))
+175
PORT)))
+176
(assert-true (string-contains-spec? (grants-for-origin config served-http) "pty:on"))))
+177
(test "a non-loopback served daemon withholds pty from the served origin"
+178
(let* ((dir (make-temp-directory))
+179
(config (config-with-served-origins
+180
(make-config state-dir: dir
+181
silent-pair-grants: (silent-pair-grants-for-host "0.0.0.0"))
+182
PORT)))
+183
(assert-false (string-contains-spec? (grants-for-origin config served-http) "pty:on")))))
+184
185
;; ============================================================
186
;; MEDIUM-1 (Fable): the silent-pair kill switch. With silent pairing
187
;; disabled, a served origin is NOT eligible (falls through to normal
packages/slate-shell/src/slate-shell/main.sglmodified
@@ -28,7 +28,8 @@
28
(begin
29
30
(define (usage)
31
(display "Usage: slate [--session NAME] launch the Slate window\n")
+31
(display "Usage: slate --url <origin> render a running slate node (e.g. http://localhost:32640)\n")
+32
(display " slate [--session NAME] launch the Slate window\n")
33
(display " slate open <path> [--session NAME] open a file in a running Slate\n")
34
(display " slate run <cmd> [--session NAME] run a command in a running Slate\n")
35
(display " slate eval <expr> [--session NAME] eval an expression there\n")
@@ -74,6 +75,24 @@
75
services: (list (make-slate-client-service session)))
76
title: "Slate" width: 1200 height: 800 transparent: #t))
77
+78
;; ---- node-renderer role (the pivot's canonical Shell) -----------------
+79
;; `slate-shell --url http://localhost:<port>` opens a native window that is
+80
;; a PURE RENDERER onto a running slate node's origin (design
+81
;; [[topics/slate-distribution-architecture]] §1.3). lantern-app's assets:
+82
;; field loads an http(s) URL directly (no lantern:// scheme), so the window
+83
;; shows the SAME PWA the node serves, connecting to the node's WS
+84
;; same-origin — exactly like a browser tab at that URL, just chromeless and
+85
;; native. System access (fs/pty) flows over the node, NOT lantern-system, so
+86
;; this role loads NO lantern-system plugin and NO slate-client service (both
+87
;; speak the lantern:// bridge a node-origin page never uses). This is what
+88
;; `slate serve` auto-launches for the click-to-run local-app experience.
+89
(define (run-server-url url)
+90
(lantern-run
+91
(lantern-app name: "Slate"
+92
assets: url
+93
modules: (list 'core))
+94
title: "Slate" width: 1200 height: 800 transparent: #t))
+95
96
;; ---- client role ------------------------------------------------------
97
;; A client subcommand takes ONE positional (the path/cmd/expr) plus an
98
;; optional --session. Missing positional → usage error.
@@ -105,8 +124,15 @@
124
;; runs the window loop and exits via the quit watcher, so it does not exit here.
125
(define (main . _)
126
(let* ((argv (command-line))
108
(args (if (pair? argv) (cdr argv) (list))))
+127
(args0 (if (pair? argv) (cdr argv) (list)))
+128
;; `--url <origin>` selects the node-renderer role (a pure webview
+129
;; onto a running slate node). It is server-only: when present we
+130
;; render that origin and ignore client subcommands.
+131
(pu (extract-opt args0 "--url"))
+132
(url (car pu))
+133
(args (cdr pu)))
134
(cond
+135
(url (run-server-url url))
136
((null? args) (run-server #f))
137
((string=? (car args) "open") (exit (run-client "open" path: (cdr args))))
138
((string=? (car args) "run") (exit (run-client "run" cmd: (cdr args))))
packages/slate/src/slate/main.sglmodified
@@ -390,6 +390,15 @@
390
;; `glow:` renders; #f = the frame gets a `no-fx` class that forces --glow
391
;; off in CSS regardless of theme. Persisted like the theme name.
392
slate/effects: #t
+393
;; Terminal backend default (design §7). #f = a plain pty (a bare shell in
+394
;; a pty) is the default "Open terminal" backend — the solid, well-worn path
+395
;; a new user hits first. #t = tmux control-mode ("better terminal") becomes
+396
;; the default instead (when a tmux server is present on the node). Either
+397
;; way tmux stays fully reachable; this only flips which backend a FRESH
+398
;; terminal opens with. Persisted (localStorage) + boot-restored + settable
+399
;; live from the command palette / set-options!; a newly-opened terminal
+400
;; reads the current value at open time, so a live flip needs no rebuild.
+401
slate/terminals-use-tmux: #f
402
;; Focus-follows-mouse: hovering a pane focuses it. Global toggle, default
403
;; ON (desktop); the JS hover handler no-ops on touch. Persisted + boot-
404
;; restored; init.sgl-settable.
@@ -484,6 +493,20 @@
493
(guard (e (#t #f)) (local-storage-set! ffm-pref-key (if on? "1" "0"))))
494
(define (ffm-on? state) (dict-ref state slate/focus-follows-mouse: #t))
495
+496
;; Terminals-use-tmux master switch (design §7), persisted as "1"/"0" — same
+497
;; guarded shape as the effects/ffm prefs. Default OFF: a plain pty is the
+498
;; default terminal backend. When ON, "Open terminal" opens a tmux control-mode
+499
;; "better terminal" instead (when a tmux server is present on the node). tmux
+500
;; stays reachable either way; this only flips the DEFAULT. Read live by
+501
;; tmux-available? at terminal-open time, so a flip + a fresh terminal needs no
+502
;; rebuild. Degrades to the default (off) natively / when storage is blocked.
+503
(define tmux-default-pref-key "slate.pref/terminals-use-tmux")
+504
(define (tmux-default-pref-load)
+505
(guard (e (#t #f)) (local-storage-get tmux-default-pref-key)))
+506
(define (tmux-default-pref-save! on?)
+507
(guard (e (#t #f)) (local-storage-set! tmux-default-pref-key (if on? "1" "0"))))
+508
(define (terminals-use-tmux? state) (dict-ref state slate/terminals-use-tmux: #f))
+509
510
;; The translucency AMOUNTS (t-a130): app-db numbers, persisted as their decimal
511
;; string, guarded like the other prefs. Accessors clamp defensively so a bad
512
;; persisted value never breaks the lowering.
@@ -1548,6 +1571,9 @@
1571
(cand "Switch Space" "" "prompt:space")
1572
(cand (if (ffm-on? state) "Focus-follows-mouse: on" "Focus-follows-mouse: off")
1573
"hover a pane to focus it" "ffm-toggle")
+1574
(cand (if (terminals-use-tmux? state)
+1575
"Terminals use tmux: on" "Terminals use tmux: off")
+1576
"default new terminals to the tmux \"better terminal\"" "terminals-use-tmux:toggle")
1577
(cand "Switch theme…"
1578
(string-append (number->string (length (theme-catalog)))
1579
" color schemes · light + dark")
@@ -3614,6 +3640,22 @@
3640
(ffm-pref-save! next)
3641
(with-echo (dict-set (close-prompt state) slate/focus-follows-mouse: next)
3642
(string-append "focus-follows-mouse " (if next "on" "off")))))
+3643
;; Terminals-use-tmux toggle (persisted). Flips the default terminal
+3644
;; backend: off = plain pty, on = tmux control-mode "better terminal".
+3645
;; tmux-available? reads the setting live at terminal-open time, so the
+3646
;; NEXT "Open terminal" honors the new value with no rebuild (the live-demo
+3647
;; affordance). Existing terminals are unaffected. Echoes a hint when tmux
+3648
;; is turned on but no tmux server was found on the node.
+3649
((string=? event "terminals-use-tmux:toggle")
+3650
(let ((next (not (terminals-use-tmux? state))))
+3651
(tmux-default-pref-save! next)
+3652
(with-echo (dict-set (close-prompt state) slate/terminals-use-tmux: next)
+3653
(cond
+3654
((not next) "terminals use tmux: off (plain pty)")
+3655
((and *tmux-caps* (dict-ref *tmux-caps* available?: #f))
+3656
"terminals use tmux: on — new terminals open tmux")
+3657
(else
+3658
"terminals use tmux: on — no tmux server found; new terminals stay pty")))))
3659
;; On-demand translucency (t-a130): flip the flag (persisted). render adds/
3660
;; drops the frame `translucent` class, whose CSS composites a theme-aware
3661
;; alpha (via color-mix on the live --bg-* tokens) so the desktop / demo
@@ -3932,10 +3974,11 @@
3974
;; source's provider (and argv, for a real shell) changes.
3975
((string=? event "open-terminal")
3976
(cond
3935
;; tmux-by-default (design §7): when the node is up AND a tmux server
3936
;; is startable, open a `tmux -C` control-mode "better terminal" — a
3937
;; live tmux client with a window strip + nested panes. Degrades to a
3938
;; plain pty below when tmux is absent (or terminal/backing: 'plain).
+3977
;; tmux "better terminal" (design §7) — OPT-IN via the
+3978
;; `slate/terminals-use-tmux:` setting (default off). When it is on AND
+3979
;; the node has a tmux server, open a `tmux -C` control-mode terminal —
+3980
;; a live tmux client with a window strip + nested panes. Otherwise the
+3981
;; plain-pty arm below runs (the default backend).
3982
((and (node-ready?) (tmux-available?))
3983
(with-echo (close-prompt (open-tmux-terminal state)) "opened tmux terminal"))
3984
;; A real shell on the node (the familiar CLI over the WS) when it is
@@ -6210,14 +6253,15 @@
6253
;; ================================================================
6254
;; tmux control-mode integration (P3, [[t-8269]])
6255
;; ================================================================
6213
;; Opening a terminal is tmux-by-default over the node (design §7): a
6214
;; `tmux -C` control session on a node kind:process session drives ONE Slate
6215
;; terminal pane whose interior is the selected window's nested pane tree +
6216
;; a bottom window strip. Degrades to a plain pty when tmux is absent.
+6256
;; A plain pty is the DEFAULT terminal backend (design §7). tmux control-mode
+6257
;; (the "better terminal": a `tmux -C` control session on a node kind:process
+6258
;; session driving ONE Slate pane with a nested pane tree + a bottom window
+6259
;; strip) is opt-in via the `slate/terminals-use-tmux:` setting. When that
+6260
;; setting is on AND a tmux server is present on the node, "Open terminal"
+6261
;; opens the tmux backend; otherwise it opens a plain pty.
6262
(define *tmux-caps* #f) ; #{ available?: version: } once probed, else #f
6263
(define *tmux-clients* #{}) ; outer view name -> #{ client: control: }
6264
(define *tmux-seq* 0)
6220
(define *tmux-backing* 'tmux) ; 'tmux | 'plain (terminal/backing: override)
6265
(define tmux-exit-missing (list 'no-exit))
6266
6267
;; "tmux 3.6a\n" -> "3.6a"; #f if the output is not a tmux version line.
@@ -6231,8 +6275,12 @@
6275
(loop (+ i 1))))
6276
#f))
6277
+6278
;; #t iff the tmux "better terminal" is BOTH opted-in (the live
+6279
;; `slate/terminals-use-tmux:` setting) AND actually available on the node (a
+6280
;; probed tmux server). Read at terminal-open time, so a live toggle of the
+6281
;; setting takes effect for the NEXT terminal with no rebuild.
6282
(define (tmux-available?)
6235
(and (not (eq? *tmux-backing* 'plain))
+6283
(and (terminals-use-tmux? *state*)
6284
*tmux-caps* (dict-ref *tmux-caps* available?: #f)))
6285
6286
;; ---- version floor + capabilities (design §9) ------------------
@@ -6780,6 +6828,11 @@
6828
(let ((ff (ffm-pref-load)))
6829
(when (and ff (string=? ff "0"))
6830
(set! *state* (dict-set *state* slate/focus-follows-mouse: #f))))
+6831
;; Restore the terminals-use-tmux switch (default off = plain pty; only "1"
+6832
;; makes tmux the default terminal backend).
+6833
(let ((tx (tmux-default-pref-load)))
+6834
(when (and tx (string=? tx "1"))
+6835
(set! *state* (dict-set *state* slate/terminals-use-tmux: #t))))
6836
;; Restore the translucency amounts (content/chrome opacity).
6837
(let ((o (num-pref-load content-opacity-pref-key)))
6838
(when o (set! *state* (dict-set *state* slate/opacity: (clamp-num o 10 100)))))