Files
web-racket/scribblings/web-wire.scrbl
2026-02-19 11:19:04 +01:00

971 lines
23 KiB
Racket
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#lang scribble/manual
@(require scribble/manual
;scribble/class
(for-label racket/base
racket/class
racket/gui/base
json
"../private/css.rkt"
"../private/menu.rkt"
"../private/webui-wire-download.rkt"
"../private/webui-wire-ipc.rkt"
"../private/web-wire.rkt"))
@title{Web Wire: Low-Level WebUI Command Layer}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[web-wire]
The @racket[web-wire] module is the low-level bridge between Racket and
the external @tt{webui-wire} helper process.
On the Racket side it exposes functions like:
@itemlist[
@item{starting and stopping the backend (@racket[ww-start],
@racket[ww-stop]);}
@item{log inspection and debugging helpers;}
@item{window management (create, move, resize, set title/icon/menu);}
@item{DOM operations (HTML content, attributes, CSS, classes, values);}
@item{element queries and event binding;}
@item{file/directory chooser dialogs.}
]
Most of these functions ultimately send a textual command to
@tt{webui-wire} (via @racket[webui-ipc]) and convert the reply into a
convenient Racket value.
@section{Startup and shutdown}
@defproc[(ww-start [log-level symbol?]) web-rkt?]{
Starts the Web Wire backend (if it is not already running) and returns
the current @racket[web-rkt] handle.
Internally this:
@itemlist[
@item{creates queues and semaphores for events and logs;}
@item{starts an IPC connection via @racket[webui-ipc];}
@item{spawns a background thread that handles incoming events and
log lines;}
@item{stores the handle in an internal variable.}
]
If @racket[log-level] is supplied, it is passed to
@racket[ww-log-level] to configure the remote log level.
Calling @racket[ww-start] when the backend is already running is a
no-op: the existing handle is returned.
The backend type is currently always @racket['ipc]; FFI integration is
not implemented yet.
}
@defproc[(ww-stop) void?]{
Stops the Web Wire backend.
This function:
@itemlist[
@item{notifies registered window event handlers that windows are being
destroyed;}
@item{sends an @tt{"exit"}-style command to @tt{webui-wire};}
@item{stops the event-handling thread;}
@item{clears the current backend handle.}
]
After @racket[ww-stop], calling window/DOM functions will fail until
@racket[ww-start] is used again.
}
@section{Debugging and log inspection}
These functions control and inspect the logging used by this module and
its event thread.
@defproc[(ww-set-debug [on? boolean?]) void?]{
Enables or disables verbose internal debug logging in
@racket[web-wire]. This affects calls through @racket[ww-debug], but
does not change the remote log level of @tt{webui-wire}; for that, use
@racket[ww-log-level].
}
@defform*[[(ww-debug msg-expr) #:contracts ([msg-expr any/c])
(ww-debug id-expr msg-expr) #:contracts ([id-expr any/c] [msg-expr any/c])]]{
Debug logging macros used inside the module.
When @racket[ww-set-debug] has been enabled, these macros:
@itemlist[
@item{format @racket[msg-expr] (and optionally @racket[id-expr]);}
@item{enqueue a debug log line in the in-memory log buffer.}
]
They are primarily intended for development and diagnostics.
}
@defform*[[(ww-error msg-expr) #:contracts ([msg-expr any/c])
(ww-error id-expr msg-expr) #:contracts ([id-expr any/c] [msg-expr any/c])]]{
Error logging macros.
These always log, regardless of the debug flag, and are used for
internal error conditions. Log lines are stored in the same log
buffer used by @racket[ww-display-log] and @racket[ww-tail-log].
}
@defproc[(ww-set-log-lines! [n exact-nonnegative-integer?]) void?]{
Sets the maximum number of log lines kept in the in-memory ring buffer.
The value is clamped between 10 and 10,000. When the buffer exceeds
this limit, the oldest entries are dropped.
}
@defproc[(ww-display-log
[filter (or/c #f string? regexp? (listof string?)) #f])
void?]{
Prints the contents of the log buffer to @racket[current-output-port].
The optional @racket[filter] controls which lines are shown:
@itemlist[
@item{@racket[#f] show all entries;}
@item{@racket[string?] case-insensitive substring match;}
@item{@racket[regexp?] regular expression match;}
@item{@racket[(listof string?)] OR-combination of substring filters.}
]
}
@defproc[(ww-tail-log [args any/c] ...) void?]{
Shows the last part of the log buffer and then follows new entries,
similar to @tt{tail -f}.
The optional @racket[args] are interpreted as:
@itemlist[
@item{a number: how many last lines to show initially;}
@item{a filter (string, regexp, or list of strings) as in
@racket[ww-display-log];}
@item{a boolean @racket[#f] to stop an existing tail session.}
]
This is mainly intended for interactive debugging.
}
@section{Backend settings and protocol info}
@defproc[(ww-log-level [level symbol?]) symbol?]{
Configures the log level inside the @tt{webui-wire} helper.
This function is defined via:
@racketblock[
(def-cmd ww-log-level
loglevel () ((level symbol?)) -> symbol)
]
so @racket[level] is optional:
@itemlist[
@item{with an argument, the remote log level is set to @racket[level]
and the effective level is returned;}
@item{without arguments, the current remote log level is queried and
returned.}
]
}
@defproc[(ww-protocol) exact-integer?]{
Returns the protocol version spoken by the @tt{webui-wire} helper.
Definition (simplified):
@racketblock[
(def-cmd ww-protocol
protocol () () -> int)
]
The result is an integer that should match the protocol version that
the Racket side expects.
}
@defproc[(ww-cwd [path path-or-string?]) path?]{
Gets or sets the current working directory used by @tt{webui-wire}.
This function is defined as:
@racketblock[
(def-cmd ww-cwd
cwd () [(path path-or-string?)] -> path)
]
Semantics:
@itemlist[
@item{With a @racket[path], the helpers working directory is set and
the new directory (as a path) is returned.}
@item{Without arguments, the current directory (as seen by the helper)
is returned.}
]
This directory is used to resolve relative file names (HTML files,
icons, etc.).
}
@section{Global stylesheet}
@defproc[(ww-set-stylesheet [st stylesheet-or-string?]) void?]{
Sets the global stylesheet used by WebUI windows:
@racketblock[
(def-cmd ww-set-stylesheet
set-stylesheet ((st stylesheet-or-string?)) () -> void)
]
The argument @racket[st] may be either:
@itemlist[
@item{a stylesheet value understood by the WebUI side;}
@item{a raw CSS string.}
]
}
@defproc[(ww-get-stylesheet) stylesheet?]{
Returns the currently configured global stylesheet:
@racketblock[
(def-cmd ww-get-stylesheet
get-stylesheet () () -> stylesheet)
]
}
@section{Window handles and window management}
@defstruct*[ww-win ([id exact-integer?])]{
Opaque handle representing a WebUI window.
The internal @racket[id] field corresponds to the numeric window id
used by the @tt{webui-wire} backend.
}
@defproc[(ww-new
[profile symbol?]
[use-browser boolean?]
[parent ww-win?])
ww-win?]{
Creates a new WebUI window:
@racketblock[
(def-cmd ww-new
new ((profile symbol?)) [(use-browser boolean?) (parent ww-win?)]
-> ww-win)
]
Arguments:
@itemlist[
@item{@racket[profile] symbolic profile name used by the helper to
select configuration (e.g. @racket['default]).}
@item{@racket[use-browser] when true, prefers an external browser
instead of an embedded webview where supported.}
@item{@racket[parent] optional parent window (for dialog-like windows).}
]
Returns a @racket[ww-win] handle.
}
@defproc[(ww-close [win-id ww-win?]) void?]{
Closes the given window:
@racketblock[
(def-cmd ww-close
close ((win-id ww-win?)) [] -> void)
]
}
@defproc[(ww-move [win-id ww-win?]
[x number?]
[y number?])
void?]{
Moves the window to position (@racket[x], @racket[y]) in pixels:
@racketblock[
(def-cmd ww-move
move ((win-id ww-win?) (x number?) (y number?)) [] -> void)
]
}
@defproc[(ww-resize [win-id ww-win?]
[width number?]
[height number?])
void?]{
Resizes the window to the given pixel @racket[width] and
@racket[height]:
@racketblock[
(def-cmd ww-resize
resize ((win-id ww-win?) (width? number?) (height number?)) [] -> void)
]
}
@defproc[(ww-set-title [win-id ww-win?]
[title string?])
void?]{
Sets the window title:
@racketblock[
(def-cmd ww-set-title
set-title ((win-id ww-win?) (title string?)) [] -> void)
]
}
@defproc[(ww-set-icon [win-id ww-win?]
[svg-file (is-icon-file? 'svg)]
[png-file (is-icon-file? 'png)])
void?]{
Sets the window icon, given paths to an SVG and PNG file:
@racketblock[
(def-cmd ww-set-icon
set-icon ((win-id ww-win?)
(svg-file (is-icon-file? 'svg))
(png-file (is-icon-file? 'png))) [] -> void)
]
The predicates @racket[(is-icon-file? 'svg)] and
@racket[(is-icon-file? 'png)] ensure that the paths refer to existing
files with the correct file extension.
}
@defproc[(ww-set-menu [win-id ww-win?]
[menu is-menu?])
void?]{
Configures the window menu:
@racketblock[
(def-cmd ww-set-menu
set-menu ((win-id ww-win?)
(menu is-menu?)) [] -> void)
]
The @racket[menu] value is a menu structure as defined in
@racket["menu.rkt"].
}
@defproc[(ww-popup-menu [win-id ww-win?]
[menu is-menu?])
void?]{
Pops up a (context) menu at the current cursor position:
@racketblock[
(def-cmd ww-popup-menu
set-menu ((win-id ww-win?)
(menu is-menu?)) [] -> void)
]
The @racket[menu] value is a menu structure as defined in
@racket["menu.rkt"].
}
@defproc[(ww-set-show-state [win-id ww-win?]
[state symbol?])
void?]{
Sets the show state of the window:
@racketblock[
(def-cmd ww-set-show-state
set-show-state ((win-id ww-win?)
(state symbol?)) () -> void)
]
Typical states are things like @racket['normal], @racket['maximized],
etc., depending on what the backend supports.
}
@defproc[(ww-get-show-state [win-id ww-win?]) symbol?]{
Returns the current show state of the window:
@racketblock[
(def-cmd ww-get-show-state
show-state ((win-id ww-win?)) () -> symbol)
]
}
@section{HTML / URL content}
@defproc[(ww-set-html-file [win-id ww-win?]
[html-file html-file-exists?])
exact-integer?]{
Loads the given HTML file into the window:
@racketblock[
(def-cmd ww-set-html-file
set-html ((win-id ww-win?)
(html-file html-file-exists?)) ()
-> number)
]
The @racket[html-file] argument must exist (resolved against
@racket[ww-cwd]); otherwise an error is raised. The return value is a
numeric status code from the helper.
}
@defproc[(ww-set-url [win-id ww-win?]
[url string?])
void?]{
Navigates the window to the given URL:
@racketblock[
(def-cmd ww-set-url
set-url ((win-id ww-win?)
(url string?)) () -> void)
]
}
@defproc[(ww-set-inner-html [win-id ww-win?]
[element-id symbol-or-string?]
[html-of-file html-or-file?])
void?]{
Replaces the @tt{innerHTML} of the given element:
@racketblock[
(def-cmd ww-set-inner-html
set-inner-html ((win-id ww-win?)
(element-id symbol-or-string?)
(html-of-file html-or-file?)) () -> void)
]
The @racket[html-of-file] value is either a string containing HTML or
a path to a file whose contents are used as HTML.
}
@defproc[(ww-get-inner-html [win-id ww-win?]
[element-id symbol-or-string?])
jsexpr?]{
Gets the @tt{innerHTML} of the element as JSON:
@racketblock[
(def-cmd ww-get-inner-html
get-inner-html ((win-id ww-win?)
(element-id symbol-or-string?)) () -> json)
]
}
@section{Attributes, CSS, classes}
@defproc[(ww-set-attr [win-id ww-win?]
[element-id symbol-or-string?]
[attr symbol-or-string?]
[val any?])
void?]{
Sets an attribute on an element:
@racketblock[
(def-cmd ww-set-attr
set-attr ((win-id ww-win?)
(element-id symbol-or-string?)
(attr symbol-or-string?)
(val any?)) () -> void)
]
}
@defproc[(ww-get-attr [win-id ww-win?]
[element-id symbol-or-string?]
[attr symbol-or-string?])
jsexpr?]{
Returns the value of a specific attribute as JSON:
@racketblock[
(def-cmd ww-get-attr
get-attr ((win-id ww-win?)
(element-id symbol-or-string?)
(attr symbol-or-string?)) () -> json)
]
}
@defproc[(ww-get-attrs [win-id ww-win?]
[element-id symbol-or-string?])
jsexpr?]{
Returns all attributes of the element in JSON form, converted on the
Racket side by a helper:
@racketblock[
(def-cmd ww-get-attrs
get-attrs ((win-id ww-win?)
(element-id symbol-or-string?)) () -> json
-> mk-attrs)
]
}
@defproc[(ww-del-attr [win-id ww-win?]
[element-id symbol-or-string?]
[attr symbol-or-string?])
void?]{
Removes an attribute from the element:
@racketblock[
(def-cmd ww-del-attr
del-attr ((win-id ww-win?)
(element-id symbol-or-string?)
(attr symbol-or-string?)) () -> void)
]
}
@defproc[(ww-add-style [win-id ww-win?]
[element-id symbol-or-string?]
[css-style css-style?])
void?]{
Adds or merges CSS style for an element:
@racketblock[
(def-cmd ww-add-style
add-style ((win-id ww-win?)
(element-id symbol-or-string?)
(css-style css-style?)) () -> void)
]
}
@defproc[(ww-set-style [win-id ww-win?]
[element-id symbol-or-string?]
[css-style css-style?])
void?]{
Replaces the CSS style of an element:
@racketblock[
(def-cmd ww-set-style
set-style ((win-id ww-win?)
(element-id symbol-or-string?)
(css-style css-style?)) () -> void)
]
}
@defproc[(ww-get-style [win-id ww-win?]
[element-id symbol-or-string?])
css-style?]{
Gets the CSS style of an element:
@racketblock[
(def-cmd ww-get-style
get-style ((win-id ww-win?)
(element-id symbol-or-string?)) ()
-> css-style)
]
}
@defproc[(ww-add-class [win-id ww-win?]
[element-id symbol-or-string?]
[class symbol-or-string?])
void?]{
Adds a CSS class:
@racketblock[
(def-cmd ww-add-class
add-class ((win-id ww-win?)
(element-id symbol-or-string?)
(class symbol-or-string?))
() -> void)
]
}
@defproc[(ww-remove-class [win-id ww-win?]
[element-id symbol-or-string?]
[class symbol-or-string?])
void?]{
Removes a CSS class:
@racketblock[
(def-cmd ww-remove-class
remove-class ((win-id ww-win?)
(element-id symbol-or-string?)
(class symbol-or-string?))
() -> void)
]
}
@defproc[(ww-has-class? [win-id ww-win?]
[element-id symbol-or-string?]
[class symbol-or-string?])
boolean?]{
Tests for a CSS class:
@racketblock[
(def-cmd ww-has-class?
has-class ((win-id ww-win?)
(element-id symbol-or-string?)
(class symbol-or-string?)) ()
-> bool)
]
}
@section{Values and element queries}
@defproc[(ww-get-value [win-id ww-win?]
[element-id symbol-or-string?])
string?]{
Gets the “value” of an element:
@racketblock[
(def-cmd ww-get-value
value ((win-id ww-win?)
(element-id symbol-or-string?)) () -> string)
]
}
@defproc[(ww-set-value [win-id ww-win?]
[element-id symbol-or-string?]
[value any?])
void?]{
Sets the “value” of an element:
@racketblock[
(def-cmd ww-set-value
value ((win-id ww-win?)
(element-id symbol-or-string?)
(value any?)) () -> void)
]
}
@defproc[(ww-get-elements [win-id ww-win?]
[selector selector?])
jsexpr?]{
Queries elements matching a CSS-like selector:
@racketblock[
(def-cmd ww-get-elements
get-elements ((win-id ww-win?)
(selector selector?)) () -> json
-> (λ (r) ...))
]
The raw JSON result is converted by a small helper into a more usable
Racket structure (see the implementation).
}
@defproc[(ww-element-info [win-id ww-win?]
[element-id symbol-or-string?])
jsexpr?]{
Returns structural information about an element:
@racketblock[
(def-cmd ww-element-info
element-info ((win-id ww-win?)
(element-id symbol-or-string?)) ()
-> json
-> (λ (r) ...))
]
}
@section{Events}
@defproc[(ww-bind [win-id ww-win?]
[event symbol-or-string?]
[selector selector?])
jsexpr?]{
Binds an @racket[event] to all elements matching @racket[selector]:
@racketblock[
(def-cmd ww-bind
bind ((win-id ww-win?)
(event symbol-or-string?)
(selector selector?)) () -> json
-> (λ (r) ...))
]
The JSON result includes information about the bound elements.
}
@defproc[(ww-on [win-id ww-win?]
[event symbol-or-string?]
[id symbol-or-string?])
void?]{
Binds @racket[event] for a single element id:
@racketblock[
(def-cmd ww-on
on ((win-id ww-win?)
(event symbol-or-string?)
(id symbol-or-string?)) () -> void)
]
}
@section{File and directory dialogs}
@defproc[(ww-file-open [win-id ww-win?]
[caption string?]
[dir string?]
[file-filters string?])
string?]{
Opens a file-open dialog:
@racketblock[
(def-cmd ww-file-open
file-open ((win-id ww-win?)
(caption string?)
(dir string?)
(file-filters string?)) ()
-> string)
]
Returns the selected path as a string, or an empty string on cancel.
}
@defproc[(ww-file-save [win-id ww-win?]
[caption string?]
[dir string?]
[file-filters string?]
[overwrite boolean?])
string?]{
Opens a file-save dialog:
@racketblock[
(def-cmd ww-file-save
file-save ((win-id ww-win?)
(caption string?)
(dir string?)
(file-filters string?)
(overwrite boolean?)) ()
-> string)
]
Returns the chosen file path as a string, or an empty string if the
user cancels.
}
@defproc[(ww-choose-dir [win-id ww-win?]
[caption string?]
[dir string?])
string?]{
Opens a directory chooser dialog:
@racketblock[
(def-cmd ww-choose-dir
choose-dir ((win-id ww-win?)
(caption string?)
(dir string?)) ()
-> string)
]
Returns the selected directory path as a string, or an empty string
on cancel.
}
@section{Low-level command access}
@defproc[(ww-cmd [cmd string?]) cmdr?]{
Sends a raw command string @racket[cmd] to @tt{webui-wire} and returns
a @racket[cmdr] struct describing the reply.
This is the lowest-level escape hatch; all @racket[def-cmd]-generated
functions use this under the hood.
}
@defproc[(ww-cmd-nok? [r any/c]) boolean?]{
Predicate that recognises a failed command reply:
@itemlist[
@item{returns @racket[#t] if @racket[r] is a @racket[cmdr] with a
false @tt{ok} field, or the symbol @racket['cmd-nok];}
@item{returns @racket[#f] otherwise.}
]
}
@defproc[(ww-from-string [s string?]) string?]{
Helper that “unquotes” and unescapes a string previously encoded for
transmission over the wire:
@itemlist[
@item{removes surrounding quotes;}
@item{replaces escaped quotes (@tt{\"\\\"\"}) by plain quotes.}
]
}
@section{Window tables}
@defthing[windows (hash/c exact-integer? ww-win?)]{
Hash table containing the currently known windows, keyed by numeric id.
This is mainly useful for diagnostics and advanced introspection; most
application code keeps explicit @racket[ww-win] handles instead.
}
@defthing[windows-evt-handlers
(hash/c exact-integer? (-> symbol? any/c any))]{
Hash table mapping window ids to their registered event handlers.
Handlers are called with an event kind (a symbol) and associated data.
}
@defproc[(ww-get-window-for-id [win-id exact-integer?])
(or/c ww-win? #f)]{
Looks up the @racket[ww-win] struct for a numeric @racket[win-id] in
the @racket[windows] table. Returns @racket[#f] if no such window is
known.
}
@section{Helper predicates used in contracts}
The following predicates are used as argument checkers in the
@racket[def-cmd]-generated functions above.
@defproc[(stylesheet-or-string? [st any/c]) boolean?]{
Returns @racket[#t] if @racket[st] is either a stylesheet value or a
string:
@racketblock[
(define (stylesheet-or-string? st)
(or (stylesheet? st) (string? st)))
]
}
@defproc[(is-icon-file? [ext symbol?]) (-> any/c boolean?)]{
Returns a predicate that checks “existing file with the right
extension”:
@racketblock[
(define (is-icon-file? ext)
(lambda (v)
(and (string? v)
(string-suffix? v (string-append "." (symbol->string ext)))
(file-exists? v))))
]
Used in @racket[ww-set-icon] with @racket['svg] and @racket['png].
}
@defproc[(html-file-exists? [f path-or-string?]) boolean?]{
Checks whether @racket[f] refers to an existing file, either directly
or resolved relative to @racket[ww-cwd]:
@racketblock[
(define (html-file-exists? f)
(if (file-exists? f)
#t
(let* ((cwd (ww-cwd))
(full-file (build-path cwd f)))
(ww-debug (format "file-exists? '~a'" full-file))
(file-exists? full-file)))
)
]
}
@defproc[(html-or-file? [v any/c]) boolean?]{
Returns @racket[#t] if @racket[v] is either an existing file or a
string:
@racketblock[
(define (html-or-file? v)
(if (file-exists? v)
#t
(string? v)))
]
}
@defproc[(symbol-or-string? [s any/c]) boolean?]{
True if @racket[s] is a symbol or a string:
@racketblock[
(define (symbol-or-string? s)
(or (symbol? s) (string? s)))
]
}
@defproc[(any? [v any/c]) boolean?]{
Predicate that always returns @racket[#t]; used in contracts when no
extra checking is required.
}
@defproc[(path-or-string? [s any/c]) boolean?]{
True if @racket[s] is a path or string:
@racketblock[
(define (path-or-string? s)
(or (path? s) (string? s)))
]
}
@defproc[(selector? [s any/c]) boolean?]{
Predicate for element selectors:
@itemlist[
@item{@racket[symbol?] a single symbolic selector;}
@item{@racket[string?] a string selector;}
@item{a non-empty list of symbols/strings.}
]
Implementation sketch:
@racketblock[
(define (selector? s)
(or (symbol? s) (string? s)
(and (list? s)
(not (null? s))
(letrec ([f (λ (l)
(if (null? l)
#t
(and (or (symbol? (car l))
(string? (car l)))
(f (cdr l)))))]
(f s)))))
)
]
}