webview: result-returning eval (webview-eval-js-result)
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).
lantern/src/lantern/backend/gtk.sgl | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
lantern/src/lantern/webview.sgl | 8 ++++++++
2 files changed, 80 insertions(+)lantern/src/lantern/backend/gtk.sglmodified
(define webkit-web-view-get-ucm #f) (define webkit-web-view-set-background-color #f) (define webkit-web-view-eval-js #f) (define webkit-web-view-eval-js-finish #f) (define webkit-ucm-register #f) (define webkit-ucm-add-script #f) (define webkit-user-script-new #f) (define policy-cb #f) (define close-cb #f) (define keypress-cb #f) (define eval-ready-cb #f) ;; Result-returning eval correlation. webkit_web_view_evaluate_javascript is ;; async: the value arrives LATER on the GLib loop via a GAsyncReadyCallback, ;; not from the call itself. Each `eval-js-result` call mints a token, parks a ;; slot (a 1-vector) in *eval-pending* under it, and passes the token as the C ;; `user_data`; on-eval-ready reads the token back, resolves the JS value to a ;; string, and fills that slot. The caller (a service/handler goroutine) polls ;; the slot with a cooperative `sleep`, so the pump goroutine runs and dispatches ;; the completion in between. Single scheduler thread → only the CALLER mutates ;; the alist (put/del); the callback only reads it + mutates the slot vector, so ;; there is no table race. Token is a plain int (assv/eqv? keying). (define *eval-pending* (list)) (define *eval-token* 0) (define (eval-pending-put! token slot) (set! *eval-pending* (cons (cons token slot) *eval-pending*))) (define (eval-pending-ref token) (let ((e (assv token *eval-pending*))) (and e (cdr e)))) (define (eval-pending-del! token) (set! *eval-pending* (filter (lambda (e) (not (= (car e) token))) *eval-pending*))) ;; ---- per-process backend state --------------------------------------- ;; (*msg-handler* (jsc-value-to-string value))) #f))) ;; GAsyncReadyCallback for webkit_web_view_evaluate_javascript (3-arg: ;; source_object=the WebKitWebView, res=GAsyncResult, user_data=our token). ;; IRON RULE: marshal + fill the parked slot + return — no suspension. The ;; JS value comes back as a JSCValue; we stringify it (window.slateClientEval ;; returns a JS string, so this is the printed eval result). A NULL finish ;; (page gone / JS-level error) delivers "" — the caller maps empty to an ;; error. The slot holds (list str) so #f still means "not yet delivered". (define (on-eval-ready source res user-data) (guarded "eval-ready" #f (lambda () (let* ((token (pointer-address user-data)) (slot (eval-pending-ref token))) (when slot (let* ((jsc (webkit-web-view-eval-js-finish source res NULL)) (str (if (and jsc (not (null-pointer? jsc))) (jsc-value-to-string jsc) ""))) (vector-set! slot 0 (list str))))) #f))) ;; decide-policy (4-arg -> exercises sigil_applyN). Route navigations: keep ;; app-origin ones in the view, send everything else to the system browser. (define (on-decide-policy web-view decision decision-type user-data) (set! webkit-web-view-eval-js (c-function libwebkit "webkit_web_view_evaluate_javascript" (list ffi/pointer ffi/string ffi/long ffi/pointer ffi/pointer ffi/pointer ffi/pointer ffi/pointer) ffi/void)) ;; webkit_web_view_evaluate_javascript_finish(view, GAsyncResult*, GError**) ;; -> JSCValue* (or NULL on error; we pass NULL for the GError**). (set! webkit-web-view-eval-js-finish (c-function libwebkit "webkit_web_view_evaluate_javascript_finish" (list ffi/pointer ffi/pointer ffi/pointer) ffi/pointer)) (set! webkit-ucm-register (c-function libwebkit "webkit_user_content_manager_register_script_message_handler" (list ffi/pointer ffi/string ffi/pointer) ffi/bool)) ;; process lifetime so the traced-callback list never collects them). (set! scheme-cb (c-callback (list ffi/pointer ffi/pointer) ffi/void on-scheme-request)) (set! message-cb (c-callback (list ffi/pointer ffi/pointer ffi/pointer) ffi/void on-script-message)) ;; GAsyncReadyCallback: (GObject* source, GAsyncResult* res, gpointer user_data) -> void (set! eval-ready-cb (c-callback (list ffi/pointer ffi/pointer ffi/pointer) ffi/void on-eval-ready)) (set! policy-cb (c-callback (list ffi/pointer ffi/pointer ffi/int ffi/pointer) ffi/bool on-decide-policy)) (set! close-cb (c-callback (list ffi/pointer ffi/pointer) ffi/bool on-close-request)) ;; GtkEventControllerKey::key-pressed = (controller keyval keycode state user-data) -> gboolean (define (eval-js window js) (webkit-web-view-eval-js (dict-ref window view: #f) js -1 NULL NULL NULL NULL NULL)) ;; Result-returning eval. Fires the async webkit eval with our ready ;; callback + a correlation token, then polls the parked slot with a ;; cooperative sleep (yielding so the pump goroutine dispatches the GLib ;; completion) up to `timeout-ticks` × ~20ms. Returns the JS result string, ;; or #f on timeout (the caller maps that to an "eval timed out" error). ;; Runs on a service/handler goroutine, NOT the callback extent — so the ;; sleep-yield is legal (unlike inside on-eval-ready itself). (define (eval-js-result window js timeout-ticks) (set! *eval-token* (+ *eval-token* 1)) (let ((token *eval-token*) (slot (make-vector 1 #f)) (view (dict-ref window view: #f))) (eval-pending-put! token slot) (webkit-web-view-eval-js view js -1 NULL NULL NULL eval-ready-cb (make-pointer token)) (let loop ((n timeout-ticks)) (let ((v (vector-ref slot 0))) (cond ((pair? v) (eval-pending-del! token) (car v)) ((<= n 0) (eval-pending-del! token) #f) (else (sleep 0.02) (loop (- n 1)))))))) (define (set-background window r g b a) (set-view-background (dict-ref window view: #f) r g b a)) register-messages: register-messages inject-script: inject-script eval-js: eval-js eval-js-result: eval-js-result set-background: set-background set-nav-handler: set-nav-handler set-close-handler: set-close-handlerlantern/src/lantern/webview.sglmodified
webview-register-messages webview-inject-script webview-eval-js webview-eval-js-result webview-set-background webview-set-nav-handler webview-set-close-handler ((op wv inject-script:) window js)) (define (webview-eval-js wv window js) ((op wv eval-js:) window js)) ;; Optional op: result-returning eval (host <- page). Returns the JS result ;; string, #f on timeout, or #f when the backend does not implement it — so a ;; caller that wants a value degrades to "no result" rather than erroring. ;; timeout-ticks bounds the wait (× the backend's poll tick, ~20ms). (define (webview-eval-js-result wv window js timeout-ticks) (let ((f (dict-ref wv eval-js-result: #f))) (and f (f window js timeout-ticks)))) (define (webview-set-background wv window r g b a) ((op wv set-background:) window r g b a)) (define (webview-set-nav-handler wv window handler)