Commitcc7e5752Recorded22 Jul 2026Repositorylantern

webview: result-returning eval (webview-eval-js-result)

Message

Add an optional result-returning JS eval to the webview interface, closing the fire-and-forget gap: webkitwebviewevaluatejavascript was called with a NULL GAsyncReadyCallback, so the page's return value died in GLib.

- Bind webkitwebviewevaluatejavascriptfinish and add on-eval-ready, a 3-arg GAsyncReadyCallback (same shape as the script-message handler) that resolves the JSCValue to a string and fills a parked slot. - Correlate async completions to callers via an integer token passed as the C userdata + a per-process pending alist; the caller polls its slot with a cooperative sleep so the pump goroutine dispatches the completion in between. Single scheduler thread, so only the caller mutates the table. - webview-eval-js-result is an OPTIONAL op (backends without it return #f), so callers degrade to no-result rather than erroring.

Consumed by the slate-desktop control server to return real eval values/errors over the slate-client channel (the 0.2.0 MCP server dependency).

Changed
 lantern/src/lantern/backend/gtk.sgl | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 lantern/src/lantern/webview.sgl     |  8 ++++++++
 2 files changed, 80 insertions(+)
Diff
lantern/src/lantern/backend/gtk.sglmodified
@@ -102,6 +102,7 @@
102
(define webkit-web-view-get-ucm #f)
103
(define webkit-web-view-set-background-color #f)
104
(define webkit-web-view-eval-js #f)
+105
(define webkit-web-view-eval-js-finish #f)
106
(define webkit-ucm-register #f)
107
(define webkit-ucm-add-script #f)
108
(define webkit-user-script-new #f)
@@ -133,6 +134,28 @@
134
(define policy-cb #f)
135
(define close-cb #f)
136
(define keypress-cb #f)
+137
(define eval-ready-cb #f)
+138
+139
;; Result-returning eval correlation. webkit_web_view_evaluate_javascript is
+140
;; async: the value arrives LATER on the GLib loop via a GAsyncReadyCallback,
+141
;; not from the call itself. Each `eval-js-result` call mints a token, parks a
+142
;; slot (a 1-vector) in *eval-pending* under it, and passes the token as the C
+143
;; `user_data`; on-eval-ready reads the token back, resolves the JS value to a
+144
;; string, and fills that slot. The caller (a service/handler goroutine) polls
+145
;; the slot with a cooperative `sleep`, so the pump goroutine runs and dispatches
+146
;; the completion in between. Single scheduler thread → only the CALLER mutates
+147
;; the alist (put/del); the callback only reads it + mutates the slot vector, so
+148
;; there is no table race. Token is a plain int (assv/eqv? keying).
+149
(define *eval-pending* (list))
+150
(define *eval-token* 0)
+151
+152
(define (eval-pending-put! token slot)
+153
(set! *eval-pending* (cons (cons token slot) *eval-pending*)))
+154
(define (eval-pending-ref token)
+155
(let ((e (assv token *eval-pending*))) (and e (cdr e))))
+156
(define (eval-pending-del! token)
+157
(set! *eval-pending*
+158
(filter (lambda (e) (not (= (car e) token))) *eval-pending*)))
159
160
;; ---- per-process backend state ---------------------------------------
161
;;
@@ -205,6 +228,26 @@
228
(*msg-handler* (jsc-value-to-string value)))
229
#f)))
230
+231
;; GAsyncReadyCallback for webkit_web_view_evaluate_javascript (3-arg:
+232
;; source_object=the WebKitWebView, res=GAsyncResult, user_data=our token).
+233
;; IRON RULE: marshal + fill the parked slot + return — no suspension. The
+234
;; JS value comes back as a JSCValue; we stringify it (window.slateClientEval
+235
;; returns a JS string, so this is the printed eval result). A NULL finish
+236
;; (page gone / JS-level error) delivers "" — the caller maps empty to an
+237
;; error. The slot holds (list str) so #f still means "not yet delivered".
+238
(define (on-eval-ready source res user-data)
+239
(guarded "eval-ready" #f
+240
(lambda ()
+241
(let* ((token (pointer-address user-data))
+242
(slot (eval-pending-ref token)))
+243
(when slot
+244
(let* ((jsc (webkit-web-view-eval-js-finish source res NULL))
+245
(str (if (and jsc (not (null-pointer? jsc)))
+246
(jsc-value-to-string jsc)
+247
"")))
+248
(vector-set! slot 0 (list str)))))
+249
#f)))
+250
251
;; decide-policy (4-arg -> exercises sigil_applyN). Route navigations: keep
252
;; app-origin ones in the view, send everything else to the system browser.
253
(define (on-decide-policy web-view decision decision-type user-data)
@@ -379,6 +422,11 @@
422
(set! webkit-web-view-eval-js
423
(c-function libwebkit "webkit_web_view_evaluate_javascript"
424
(list ffi/pointer ffi/string ffi/long ffi/pointer ffi/pointer ffi/pointer ffi/pointer ffi/pointer) ffi/void))
+425
;; webkit_web_view_evaluate_javascript_finish(view, GAsyncResult*, GError**)
+426
;; -> JSCValue* (or NULL on error; we pass NULL for the GError**).
+427
(set! webkit-web-view-eval-js-finish
+428
(c-function libwebkit "webkit_web_view_evaluate_javascript_finish"
+429
(list ffi/pointer ffi/pointer ffi/pointer) ffi/pointer))
430
(set! webkit-ucm-register
431
(c-function libwebkit "webkit_user_content_manager_register_script_message_handler"
432
(list ffi/pointer ffi/string ffi/pointer) ffi/bool))
@@ -415,6 +463,8 @@
463
;; process lifetime so the traced-callback list never collects them).
464
(set! scheme-cb (c-callback (list ffi/pointer ffi/pointer) ffi/void on-scheme-request))
465
(set! message-cb (c-callback (list ffi/pointer ffi/pointer ffi/pointer) ffi/void on-script-message))
+466
;; GAsyncReadyCallback: (GObject* source, GAsyncResult* res, gpointer user_data) -> void
+467
(set! eval-ready-cb (c-callback (list ffi/pointer ffi/pointer ffi/pointer) ffi/void on-eval-ready))
468
(set! policy-cb (c-callback (list ffi/pointer ffi/pointer ffi/int ffi/pointer) ffi/bool on-decide-policy))
469
(set! close-cb (c-callback (list ffi/pointer ffi/pointer) ffi/bool on-close-request))
470
;; GtkEventControllerKey::key-pressed = (controller keyval keycode state user-data) -> gboolean
@@ -507,6 +557,27 @@
557
(define (eval-js window js)
558
(webkit-web-view-eval-js (dict-ref window view: #f) js -1 NULL NULL NULL NULL NULL))
559
+560
;; Result-returning eval. Fires the async webkit eval with our ready
+561
;; callback + a correlation token, then polls the parked slot with a
+562
;; cooperative sleep (yielding so the pump goroutine dispatches the GLib
+563
;; completion) up to `timeout-ticks` × ~20ms. Returns the JS result string,
+564
;; or #f on timeout (the caller maps that to an "eval timed out" error).
+565
;; Runs on a service/handler goroutine, NOT the callback extent — so the
+566
;; sleep-yield is legal (unlike inside on-eval-ready itself).
+567
(define (eval-js-result window js timeout-ticks)
+568
(set! *eval-token* (+ *eval-token* 1))
+569
(let ((token *eval-token*)
+570
(slot (make-vector 1 #f))
+571
(view (dict-ref window view: #f)))
+572
(eval-pending-put! token slot)
+573
(webkit-web-view-eval-js view js -1 NULL NULL NULL eval-ready-cb (make-pointer token))
+574
(let loop ((n timeout-ticks))
+575
(let ((v (vector-ref slot 0)))
+576
(cond
+577
((pair? v) (eval-pending-del! token) (car v))
+578
((<= n 0) (eval-pending-del! token) #f)
+579
(else (sleep 0.02) (loop (- n 1))))))))
+580
581
(define (set-background window r g b a)
582
(set-view-background (dict-ref window view: #f) r g b a))
583
@@ -626,6 +697,7 @@
697
register-messages: register-messages
698
inject-script: inject-script
699
eval-js: eval-js
+700
eval-js-result: eval-js-result
701
set-background: set-background
702
set-nav-handler: set-nav-handler
703
set-close-handler: set-close-handler
lantern/src/lantern/webview.sglmodified
@@ -44,6 +44,7 @@
44
webview-register-messages
45
webview-inject-script
46
webview-eval-js
+47
webview-eval-js-result
48
webview-set-background
49
webview-set-nav-handler
50
webview-set-close-handler
@@ -103,6 +104,13 @@
104
((op wv inject-script:) window js))
105
(define (webview-eval-js wv window js)
106
((op wv eval-js:) window js))
+107
;; Optional op: result-returning eval (host <- page). Returns the JS result
+108
;; string, #f on timeout, or #f when the backend does not implement it — so a
+109
;; caller that wants a value degrades to "no result" rather than erroring.
+110
;; timeout-ticks bounds the wait (× the backend's poll tick, ~20ms).
+111
(define (webview-eval-js-result wv window js timeout-ticks)
+112
(let ((f (dict-ref wv eval-js-result: #f)))
+113
(and f (f window js timeout-ticks))))
114
(define (webview-set-background wv window r g b a)
115
((op wv set-background:) window r g b a))
116
(define (webview-set-nav-handler wv window handler)