This commit is contained in:
2026-02-16 10:40:06 +01:00
parent ce4830623a
commit d73ad9a8e6
8 changed files with 2168 additions and 4 deletions

View File

@@ -1,14 +1,18 @@
#lang info
(define pkg-authors '(hnmdijkema))
(define version "0.1.0")
(define license 'GPL-3.0-or-later) ; The liboa library has this license
(define version "0.1.3")
(define license 'MIT)
(define collection "web-racket")
(define pkg-desc "web-racket - A Web Based GUI library, based on web-wire")
(define pkg-desc "web-racket - A Web Based GUI library, based on webui-wire")
(define scribblings
'(
("scribblings/web-racket.scrbl" () (gui-library) "web-racket")
("scribblings/web-wire.scrbl" () (gui-library) "web-wire")
("scribblings/webui-wire-download.scrbl" () (gui-library) "webui-wire-download")
("scribblings/webui-wire-ipc.scrbl" () (gui-library) "webui-wire-ipc")
("scribblings/webui-wire-version.scrbl" () (gui-library) "webui-wire-version")
)
)

Binary file not shown.

View File

@@ -33,7 +33,7 @@
(define ww-version (mk-version ww-version-major ww-version-minor ww-version-patch))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Web Wire FFI Version
;; Web Wire IPC Version
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define ww-wire-version-major 0)

View File

@@ -0,0 +1,108 @@
#lang scribble/manual
@(require scribble/manual
(for-label racket/base
racket/string
"../private/web-racket-version.rkt"))
@title{Web Racket Version Information}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[web-racket-version]
The @racket[web-racket-version] module centralises version information
for the Web Racket framework and the associated @emph{Web Wire} FFI
layer. It provides both numeric version components and a
pre-formatted string for each of these version identifiers.
The numeric bindings are useful when you want to compare versions in
code; the string bindings are convenient for display in logs, about
dialogs, and diagnostic output.
Internally, versions are formatted as @tt{"MAJOR.MINOR.PATCH"}.
@section{Web Racket version}
@defthing[ww-version-major exact-nonnegative-integer?]{
The major version number of the Web Racket framework.
This component is incremented for changes that are not backwards
compatible or that represent a significant evolution of the system.
}
@defthing[ww-version-minor exact-nonnegative-integer?]{
The minor version number of the Web Racket framework.
This component is typically incremented for backwards-compatible
feature additions.
}
@defthing[ww-version-patch exact-nonnegative-integer?]{
The patch version number of the Web Racket framework.
This component is incremented for small, backwards-compatible
bug fixes and maintenance releases.
}
@defthing[ww-version string?]{
A human-readable string representation of the Web Racket framework
version, combining @racket[ww-version-major],
@racket[ww-version-minor], and @racket[ww-version-patch] in the form:
@verbatim{"MAJOR.MINOR.PATCH"}
For example, if the numeric components are @racket[0], @racket[1],
and @racket[3], then @racket[ww-version] is @racket["0.1.3"].
}
@section{Web Wire IPC version}
The Web Wire IPC version describes the version of the low-level
interface between Racket and the embedded web UI (for example, a
shared library or external command used to drive the webview).
This version is tracked separately from the main Web Racket framework
version so that protocol or binary compatibility changes can be
managed independently.
@defthing[ww-wire-version-major exact-nonnegative-integer?]{
The major version number of the Web Wire IPC interface.
A change in the major version generally indicates that the IPC
protocol is not backwards compatible with previous major versions.
}
@defthing[ww-wire-version-minor exact-nonnegative-integer?]{
The minor version number of the Web Wire IPC interface.
This component is typically incremented for backwards-compatible
extensions or enhancements to the IPC protocol.
}
@defthing[ww-wire-version-patch exact-nonnegative-integer?]{
The patch version number of the Web Wire IPC interface.
This is usually incremented for small, backwards-compatible bug
fixes or internal refinements that do not change the protocol
surface.
}
@defthing[ww-wire-version string?]{
A human-readable string representation of the Web Wire IPC version,
combining @racket[ww-wire-version-major],
@racket[ww-wire-version-minor], and @racket[ww-wire-version-patch]
in the form:
@verbatim{"MAJOR.MINOR.PATCH"}
For example, with numeric components @racket[0], @racket[2], and
@racket[8], the resulting version string is @racket["0.2.8"].
}

View File

@@ -0,0 +1,641 @@
#lang scribble/manual
@(require scribble/manual
;scribble/class
(for-label racket/base
racket/class
racket/gui/base
json
;gregor
;gregor/time
net/sendurl
racket/path
"../private/web-wire.rkt"
"../private/webui-wire-download.rkt"
"../private/webui-wire-ipc.rkt"
"../private/css.rkt"
"../private/menu.rkt"
"../private/web-racket.rkt"))
@title{High-Level Web Racket GUI API}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[web-racket]
The @racket[web-racket] module provides a higher-level GUI layer on top
of the low-level @racketmodname[web-wire] backend. It is intended as the
main entry point for desktop applications that embed a WebUI.
The module exports:
@itemlist[
@item{OO wrappers for DOM elements and form inputs
(@racket[ww-element%], @racket[ww-input%]);}
@item{classes for main windows and dialogs
(@racket[ww-webview%], @racket[ww-webview-dialog%],
@racket[ww-webview-message%]);}
@item{an abstract settings interface (@racket[ww-settings%]);}
@item{backend control and logging helpers re-exported from
@racketmodname[web-wire];}
@item{backend installation/override helpers re-exported from
@racketmodname[webui-wire-download];}
@item{CSS and menu construction helpers re-exported from
@racket["css.rkt"] and @racket["menu.rkt"].}
]
The goal of this layer is:
@itemlist[
@item{You rarely call @racket[web-wire] directly;}
@item{you work with windows, elements, and inputs as Racket objects;}
@item{only when you need fine-grained control do you drop to the
underlying wire API.}
]
@section{Re-exported backend control and logging}
The following bindings are re-exported directly from
@racketmodname[web-wire]. See that modules documentation for full
details; here is a short summary for convenience.
@defproc[(ww-start [log-level symbol?]) web-rkt?]{
Start the Web Wire backend if it is not running already, and return the
current backend handle.
Most applications do not call this directly: @racket[ww-webview%]
constructors will automatically call @racket[ww-start] when the first
window is created.
}
@defproc[(ww-stop) void?]{
Stop the Web Wire backend.
This is called automatically when the last window created via
@racket[ww-webview%] is closed; you normally do not need to call it
directly, but you can use it for explicit shutdown.
}
@defproc[(ww-set-debug [on? boolean?]) void?]{
Enable or disable internal debug logging in the @racket[web-wire]
layer. This affects @racket[ww-debug] output.
}
@defform[(ww-debug msg-expr) #:contracts ([msg-expr any/c])]
@defform[(ww-debug id-expr msg-expr)
#:contracts ([id-expr any/c] [msg-expr any/c])]{
Debug logging macros that write to the shared log buffer when debug is
enabled.
}
@defform[(ww-error msg-expr) #:contracts ([msg-expr any/c])]
@defform[(ww-error id-expr msg-expr)
#:contracts ([id-expr any/c] [msg-expr any/c])]{
Error logging macros that always log, regardless of the debug setting.
}
@defproc[(ww-display-log
[filter (or/c #f string? regexp? (listof string?)) #f])
void?]{
Display the in-memory log buffer on @racket[current-output-port],
optionally filtered.
}
@defproc[(ww-tail-log [args any/c] ...) void?]{
Show the tail of the log buffer and follow new entries (like
@tt{tail -f}). Optional arguments control how many lines to show
initially and which lines to filter.
}
@section{Re-exported WebUI helper command control}
The following bindings are re-exported from
@racketmodname[webui-wire-download]; they are documented in more detail
in that module. Briefly:
@defproc[(ww-set-custom-webui-wire-command!
[cmd string?])
string?]{
Override the command used to start the @tt{webui-wire} helper. Useful in
development or when the helper is installed in a non-standard location.
}
@defproc[(ww-get-webui-wire-version) string?]{
Return the version string reported by the installed @tt{webui-wire}
helper.
}
@section{Re-exported CSS and menu helpers}
The module also re-exports:
@itemlist[
@item{@racket[(all-from-out "css.rkt")] — CSS helper functions and
@racket[css-style] values;}
@item{@racket[(all-from-out "menu.rkt")] — menu construction helpers
for building menu structures passed to @racket[ww-webview%].}
]
See the documentation of those modules for the exact API.
@section{Element wrapper classes}
These classes provide OO wrappers around DOM elements managed by
@tt{webui-wire}. They work on top of the lower-level @racket[web-wire]
functions that address elements by id.
@subsection{Generic element}
@defclass[ww-element% object% ()]{
Base class for element wrappers.
@defconstructor/auto-super[([win-id ww-win?]
[id (or/c symbol? string?)])]
The constructor normally isnt called directly by user code. Objects are
created automatically by @racket[ww-webview%] when you bind inputs and
buttons or when you explicitly request an element.
Fields:
@itemlist[
@item{@racket[win-id] — a @racket[ww-win] handle identifying the WebUI
window that owns this element}
@item{@racket[id] — the element id (symbol or string), as used in the
DOM.}
]
Public methods:
@defmethod[(get-win-id) ww-win?]{
Return the window handle used for this element.}
@defmethod[(get-id) (or/c symbol? string?)]{
Return the element id.}
@defmethod[(win) (or/c ww-webview% #f)]{
Look up the @racket[ww-webview%] instance for @racket[win-id] in the
global window table. Returns @racket[#f] if the window no longer
exists.}
@defmethod[(callback [evt symbol?] [args any/c] ...) any]{
Call a previously registered callback for @racket[evt] with the
given @racket[args]. This is used internally when DOM events
arrive from @tt{webui-wire}.}
@defmethod[(connect [evt symbol?] [func (-> any)]) void?]{
Register @racket[func] as the handler for event kind @racket[evt] on
this element.}
@defmethod[(disconnect [evt symbol?]) void?]{
Remove any callback registered for @racket[evt].}
@defmethod[(add-style! [st css-style?]) void?]{
Merge the given style into the elements existing style.}
@defmethod[(set-style! [st css-style?]) void?]{
Replace the elements style with @racket[st].}
@defmethod[(style) css-style?]{
Return the current style as a @racket[css-style] value.}
@defmethod[(get-attr [a (or/c symbol? string?)]) jsexpr?]{
Get a single attribute value as JSON.}
@defmethod[(set-attr! [a (or/c symbol? string?)] [val any/c]) void?]{
Set an attribute value.}
@defmethod[(del-attr [a (or/c symbol? string?)]) void?]{
Remove an attribute.}
@defmethod[(get-attrs) (hash/c symbol? any/c)]{
Return all attributes as a hash table (symbol → value).}
@defmethod[(add-class! [cl (or/c symbol? string?)]) void?]{
Add a CSS class to the element.}
@defmethod[(remove-class! [cl (or/c symbol? string?)]) void?]{
Remove a CSS class.}
@defmethod[(has-class? [cl (or/c symbol? string?)]) boolean?]{
Test whether the element has a given CSS class.}
@defmethod[(enable) void?]{
Remove the @tt{disabled} state from the element
(by manipulating its CSS/attributes).}
@defmethod[(enabled?) boolean?]{
Return @racket[#t] when the element is not disabled.}
@defmethod[(disable) void?]{
Mark the element as disabled (e.g. by adding a @tt{disabled} attribute
and/or class).}
@defmethod[(disabled?) boolean?]{
Return @racket[#t] when the element is disabled.}
@defmethod[(display [d string? "block"]) void?]{
Set the CSS @tt{display} style; default is @tt{"block"}.}
@defmethod[(hide) void?]{
Convenience for @racket[(display "none")].}
@defmethod[(show) void?]{
Show the element again (typically restoring display to a non-@tt{"none"}
value).}
}
@subsection{Form input element}
@defclass[ww-input% ww-element% ()]{
Wrapper for input-type elements (fields that have a “value” and that
emit change/input events). Instances are usually created by
@racket[ww-webview%] when you call @racket[bind-inputs].
Internally, a @racket[ww-input%] keeps track of:
@itemlist[
@item{the current value (mirrored from the DOM);}
@item{an optional change callback.}
]
Public methods:
@defmethod[(get) any/c]{
Return the last known value of the input.}
@defmethod[(on-change! [callback (-> any/c any)]) void?]{
Register a change callback:
@itemlist[
@item{The callback is stored;}
@item{It is immediately called once with the current value;}
@item{Later, when an @tt{input} event with a @tt{value} field arrives
from the browser, the stored callback is called with the new
value.}
]
}
@defmethod[(set! [v any/c]) void?]{
Set the inputs value both locally and in the DOM. This will normally
update the form control on screen and may also trigger change/input
events in the browser.
}
@defmethod[(disable) void?]{Disable the input (overrides base behaviour).}
@defmethod[(enable) void?]{Enable the input again (overrides base
behaviour).}
}
@section{Webview windows}
@subsection{Main windows}
@defclass[ww-webview% object% ()]{
Represents a WebUI window driven by the @tt{webui-wire} backend.
@defconstructor/auto-super[
([profile symbol? 'default-profile]
[settings (or/c #f ww-settings%) #f]
[use-browser boolean? #f]
[parent-id (or/c #f ww-win?) #f]
[parent (or/c #f ww-webview%) #f]
[title string? "Racket HTML Window"]
[x number?]
[y number?]
[width number?]
[height number?]
[icon (or/c #f path? string?) #f]
[menu any/c #f]
[html-file (or/c #f path? string?) #f])
]{
The numeric geometry defaults (@racket[x], @racket[y], @racket[width],
@racket[height]) are taken either from @racket[settings] or from builtin
defaults.
If @racket[parent] or @racket[parent-id] is provided, a child window
is created and positioned relative to the parent; otherwise a normal
top-level window is created.
If @racket[menu], @racket[icon], or @racket[html-file] are provided,
they are applied after window creation.
}
Important behaviour during construction:
@itemlist[
@item{when the first window is created, @racket[ww-start] is called;}
@item{the new window is registered in the global @racket[windows]
table;}
@item{an event handler is installed so that incoming events from
@tt{webui-wire} are dispatched to methods like
@racket[handle-click] and @racket[handle-change];}
@item{when the last window is destroyed, @racket[ww-stop] is called.}
]
Key public methods (grouped by responsibility):
@subsubsection{Settings and cloning}
@defmethod[(clone-settings [section symbol?]) (or/c #f ww-settings%)]{
If @racket[settings] is non-@racket[#f], call @racket[clone] on it
to obtain a section-specific settings object, otherwise return
@racket[#f].
}
@subsubsection{Event handling hooks}
These are normally invoked internally when events arrive from
@tt{webui-wire}, but can be overridden in subclasses.
@defmethod[(handle-click [element-id symbol?] [data jsexpr?]) void?]{
Find the element object for @racket[element-id] (if present) and call
its @racket[callback] method with the @racket['click] event.}
@defmethod[(handle-change [element-id symbol?] [data jsexpr?]) void?]{
Find the element object and delegate a @racket['change] event.}
@defmethod[(handle-input [element-id symbol?] [data jsexpr?]) void?]{
Find the element object and delegate an @tt{input} event.}
@defmethod[(handle-navigate [url string?]
[type symbol?]
[kind symbol?])
void?]{
Handle navigation events (for example link clicks) as reported by
@tt{webui-wire}. The default implementation may call
@racket[send-url] depending on @racket[type] and @racket[kind].}
@subsubsection{Element management}
@defmethod[(get-win-id) ww-win?]{
Return the underlying @racket[ww-win] handle.}
@defmethod[(element [id (or/c symbol? string?)]) ww-element%]{
Return (and lazily create) an element wrapper for a given DOM id.
The concrete class is chosen based on the elements tag and type
(@racket[ww-input%] for input fields, or @racket[ww-element%]
otherwise).
}
@defmethod[(get-elements [selector selector?])
(listof ww-element%)]{
Query elements matching @racket[selector] via @racket[ww-get-elements]
and return a list of element wrapper objects.
}
@defmethod[(bind [event symbol?]
[selector selector?]
[forced-cl (or/c #f (is-a?/c ww-element%)) #f])
void?]{
Bind @racket[event] to all elements matching @racket[selector].
A suitable element class is chosen automatically based on tag/type,
unless @racket[forced-cl] is provided; in that case the given class
is used for all matched elements.
}
@defmethod[(bind-inputs) boolean?]{
Convenience: bind @racket['change] events for @tt{input} and
@tt{textarea} elements and create @racket[ww-input%] wrappers.}
@defmethod[(bind-buttons) void?]{
Convenience: bind @racket['click] events for @tt{button} elements.}
@subsubsection{Window geometry and title}
@defmethod[(move [x number?] [y number?]) void?]{
Move the window to (@racket[x], @racket[y]).}
@defmethod[(resize [x number?] [y number?]) void?]{
Resize the window to width @racket[x], height @racket[y].}
@defmethod[(get-x) number?]{Current x position (cached).}
@defmethod[(get-y) number?]{Current y position (cached).}
@defmethod[(get-width) number?]{Current width (cached).}
@defmethod[(get-height) number?]{Current height (cached).}
@defmethod[(geom) (list number? number? number? number?)]{
Return @racket[(list x y width height)].}
@defmethod[(set-title! [t string?]) void?]{
Set the window title (cached locally and sent to the backend).}
@defmethod[(get-title) string?]{
Return the last title set.}
@subsubsection{Show/hide and lifetime}
@defmethod[(show) void?]{
Show the window (set show state to @racket['show]).}
@defmethod[(hide) void?]{
Hide the window.}
@defmethod[(maximize) void?]{
Maximise the window.}
@defmethod[(normalize) void?]{
Restore the window to its normal state.}
@defmethod[(minimize) void?]{
Minimise the window.}
@defmethod[(fullscreen) void?]{
Request a fullscreen display (if supported).}
@defmethod[(show-state) symbol?]{
Return the current show state as reported by the backend.}
@defmethod[(can-close?) boolean?]{
Return @racket[#t] if the window may be closed; subclasses can
override this to veto close requests.}
@defmethod[(close) void?]{
Close the window, unregister it from the global tables, and if this is
the last window, stop the backend by calling @racket[ww-stop].
}
@subsubsection{Menus}
@defmethod[(set-menu! [menu-def is-menu?]) void?]{
Set the window menu using a structure created with the helpers from
@racket["menu.rkt"].}
@defmethod[(connect-menu! [id symbol?] [cb (-> any)]) void?]{
Associate a callback @racket[cb] with a menu item id. When the menu
item is activated, the callback is called.
}
@subsubsection{HTML and navigation}
@defmethod[(set-icon! [icn (or/c path? string?)]) void?]{
Set the window icon; the file is validated by @racket[ww-set-icon].}
@defmethod[(set-html-file! [file (or/c path? string?)]) void?]{
Load the given HTML file. The directory part is used to adjust
@racket[ww-cwd], and the file name is passed to
@racket[ww-set-html-file].}
@defmethod[(set-html [html string?]) void?]{
Write @racket[html] to a temporary file and call
@racket[set-html-file!] with it. Intended for simple dynamic content.}
@defmethod[(set-url [url string?]) void?]{
Open the given URL using @racket[send-url].}
@defmethod[(html-loaded) void?]{
Hook that is called when a page is fully loaded and matches the
current HTML handle. The default implementation binds buttons and
inputs by calling @racket[bind-buttons] and @racket[bind-inputs].
}
@subsubsection{File and directory dialogs}
@defmethod[(file-open [caption string?]
[base-dir string?]
[filters string?])
(or/c #f path?)]{
Show a file-open dialog and return the selected path as a @racket[path],
or @racket[#f] if the operation failed or was cancelled.
}
@defmethod[(file-save [caption string?]
[base-dir string?]
[filters string?]
[overwrite boolean? #f])
(or/c #f path?)]{
Show a file-save dialog. The @racket[overwrite] flag controls whether
overwriting existing files is allowed. Returns the chosen path, or
@racket[#f] if cancelled or if the command failed.
}
@defmethod[(choose-dir [caption string?]
[base-dir string?])
(or/c #f path?)]{
Show a directory chooser dialog; return the chosen directory, or
@racket[#f] on cancel/failure.
}
@subsubsection{Hook for subclasses}
@defmethod[(inherit-checks) boolean?]{
Called early during construction to allow subclasses to enforce
preconditions (for example: “dialog windows must have a parent”).
The default implementation simply returns @racket[#t].
}
}
@subsection{Dialog windows}
@defclass[ww-webview-dialog% ww-webview% ()]{
Subclass of @racket[ww-webview%] representing dialog-style windows.
The constructor is the same as @racket[ww-webview%], but
@racket[inherit-checks] is overridden to enforce that a @racket[parent]
was supplied:
@racketblock[
(define/override (inherit-checks)
(when (eq? parent #f)
(error "A parent must be given")))
]
So, to create a dialog, always pass a parent window:
@racketblock[
(define main (new ww-webview%))
(define dlg (new ww-webview-dialog% [parent main]))
]
}
@subsection{Message dialogs}
@defclass[ww-webview-message% ww-webview-dialog% ()]{
Simple message-dialog subclass.
The constructor:
@itemlist[
@item{calls the @racket[ww-webview-dialog%] constructor;}
@item{sets a minimal HTML page containing a message header and a
submessage, with known element ids (e.g. @tt{"msg"} and
@tt{"submsg"}).}
]
You can then obtain element wrappers for those ids via
@racket[send] @racket[this] @racket[element] and update their inner
HTML or text using the usual element methods.
}
@section{Settings abstraction}
@defclass[ww-settings% object% ()]{
Abstract base class for storing and retrieving configuration values
used by @racket[ww-webview%] (for example window geometry).
The default implementation only signals errors; you are expected to
subclass @racket[ww-settings%] and override the methods.
Public methods:
@defmethod[(set [key symbol?] [value any/c]) void?]{
Set a configuration value for @racket[key]. Default implementation
raises an error.}
@defmethod[(get [key symbol?] [default any/c]) any/c]{
Look up a configuration value for @racket[key]. If the key is not
present, return @racket[default] (if provided) or raise an error. The
default implementation always raises an error.
}
@defmethod[(clone [new-section symbol?]) ww-settings%]{
Return a “cloned” settings object for a given section or profile.
Used by @racket[ww-webview%] to derive per-window settings from a
shared base. Default implementation raises an error.
}
@defmethod[(set! [key symbol?] [value any/c]) void?]{
Convenience that forwards to @racket[set]. Provided for symmetry with
Rackets @tt{set!} naming style.
}
}

965
scribblings/web-wire.scrbl Normal file
View File

@@ -0,0 +1,965 @@
#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])]
@defform[(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])]
@defform[(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-win-id [win ww-win?]) exact-integer?]{
Accessor for the underlying numeric id of a @racket[ww-win] struct.
}
@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-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)))))
)
]
}

View File

@@ -0,0 +1,187 @@
#lang scribble/manual
@(require scribble/manual
(for-label racket/base
racket/file
racket/path
racket/system
racket/string
racket/port
"../private/webui-wire-download.rkt"))
@title{WebUI Wire: Automatic Downloader}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[webui-wire-download]
This module centralises how the @tt{webui-wire} helper executable is
invoked and how its version is obtained.
It takes care of:
@itemlist[
@item{determining the command used to run @tt{webui-wire}
(platform-specific, Flatpak on Linux, a direct executable on
other platforms);}
@item{checking whether the helper is installed and matches the
expected protocol version (see @racket[ww-wire-version]);}
@item{optionally allowing user code to override the command string.}
]
The public surface of this module is intentionally small and stable:
most callers only need “get me the command” and “tell me the version”.
@section{Getting the @tt{webui-wire} command}
@defproc[(ww-get-webui-wire-command) string?]{
Returns the command that should be used to invoke the @tt{webui-wire}
helper.
Typical usage is:
@racketblock[
(define cmd (ww-get-webui-wire-command))
(define-values (proc in out err) (process cmd))
]
Internally, this function:
@itemlist[
@item{checks whether a custom command has been installed via
@racket[ww-set-custom-webui-wire-command!]; if so, that value
is returned immediately;}
@item{otherwise, determines a platform-specific default command:
on Linux this is typically a @tt{flatpak run ...} invocation,
while on other platforms it is the path to the installed
executable;}
@item{ensures that the helper is actually present and executable;}
@item{verifies that the running helper reports a compatible
@racket[ww-wire-version] (from @racketmodname[web-racket-version]).}
]
If the helper is not installed, cannot be found, or reports an
unexpected version, an error is raised with a message indicating that
@tt{webui-wire} needs to be installed (or upgraded) in order to use
Web Racket.
The returned string is intended to be passed to @racket[process] or
similar functions from @racketmodname[racket/system]. The result may
contain spaces (for example for Flatpak invocations), so treat it as a
single shell command, not as a list of arguments.
}
@section{Getting the installed version}
@defproc[(ww-get-webui-wire-version) string?]{
Returns the version string of the installed @tt{webui-wire} helper.
Conceptually, the function:
@itemlist[
@item{obtains the command via @racket[ww-get-webui-wire-command];}
@item{invokes @tt{<command> --version};}
@item{reads the stdout of that process;}
@item{trims whitespace and returns the resulting string.}
]
The returned value is the version as reported by the helper itself,
for example:
@racketblock[
"0.2.3"
]
In normal operation, this version is expected to match (or be
compatible with) the protocol version @racket[ww-wire-version] that
the current Web Racket library was built for.
If the helper is missing or cannot be executed, the same error
conditions as @racket[ww-get-webui-wire-command] apply, and an
exception is raised.
}
@section{Overriding the command}
@defproc[(ww-set-custom-webui-wire-command!
[cmd string?])
string?]{
Installs a custom command string @racket[cmd] that will be used by
@racket[ww-get-webui-wire-command] instead of the automatically
computed, platform-specific default.
This is useful when:
@itemlist[
@item{@tt{webui-wire} is installed in a non-standard location;}
@item{you want to run a wrapper script around the real executable;}
@item{you are developing or testing against a different build of the
helper.}
]
The command string should be something that can be passed directly to
@racket[process], for example:
@itemlist[
@item{@racket["/opt/webui-wire/bin/webui-wire"]}
@item{@racket["flatpak run --user nl.dijkewijk.webui-wire"]}
]
The function stores the override and returns it.
Subsequent calls to @racket[ww-get-webui-wire-command] will use this
custom command as-is; the internal platform detection and default
location logic are bypassed. The version check performed by
@racket[ww-get-webui-wire-version] still applies, so your custom
binary must speak the expected protocol version.
}
@section{Internal behaviour (informative)}
The details in this section describe the typical internal behaviour of
the module. They are not part of the public API and may change
without notice, but they are useful to understand how error messages
and version checks arise.
@subsection{Platform and installation lookup}
When no custom command is installed, the module roughly follows these
steps:
@itemlist[
@item{Determine the host operating system and, if relevant, whether
it is running under Flatpak or a similar sandbox;}
@item{Construct a candidate command:
on Linux this is usually a @tt{flatpak run} incantation;
on other platforms it is the full path to the installed
@tt{webui-wire} executable;}
@item{Attempt to run @tt{<candidate> --version} to confirm that the
helper is present and working;}
@item{Compare the reported version with @racket[ww-wire-version]; if
the versions are incompatible, an error is raised.}
]
The resolved command may be cached internally so that subsequent calls
to @racket[ww-get-webui-wire-command] do not repeat all checks.
@subsection{Error reporting}
When a problem occurs (no helper, wrong version, command not
executable, etc.), errors are raised with human-readable messages that
aim to explain:
@itemlist[
@item{what went wrong (e.g. @emph{command not found},
@emph{unsupported version});}
@item{which command was attempted;}
@item{which version was expected (via @racket[ww-wire-version]).}
]
Callers of @racket[ww-get-webui-wire-command] and
@racket[ww-get-webui-wire-version] are encouraged to catch these
exceptions near the application entry point and present a friendly
message to the end user (for example: “The WebUI helper needs to be
installed or upgraded”).
}

View File

@@ -0,0 +1,259 @@
#lang scribble/manual
@(require scribble/manual
(for-label racket/base
racket/system
racket/string
"../private/webui-wire-download.rkt"
"../private/webui-wire-ipc.rkt")
)
@title{WebUI Wire IPC Bridge}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[webui-wire-ipc]
This module provides a small IPC abstraction around the external
@tt{webui-wire} executable. It is responsible for:
@itemlist[
@item{starting the @tt{webui-wire} process (via
@racket[ww-get-webui-wire-command]);}
@item{reading and decoding its stdout and stderr streams;}
@item{dispatching incoming events to a callback;}
@item{serialising access to the process so it can be shared by
multiple threads.}
]
The public API consists of a single function, @racket[webui-ipc], which
starts the helper process and returns a function for sending commands
and receiving replies.
The helper process itself implements a simple text-based protocol; this
module hides those details and presents a synchronous “send a string,
get a string back” interface.
@section{IPC entry point}
@defproc[
(webui-ipc
[event-queuer (-> string? any)]
[log-processor (-> symbol? string? any)])
(-> string? string?)
]{
Starts the @tt{webui-wire} executable and returns a function that can be
used to send commands to it.
The arguments:
@itemlist[
@item[@racket[event-queuer]]{
A callback that receives event payloads produced by the helper
process.
Whenever the process emits an @tt{EVENT} line on stderr, the
textual payload after the @tt{"EVENT:"} tag is passed to this
callback as:
@racketblock[
(event-queuer line)
]
The callback is executed in the context of a dedicated reader
thread and should return quickly (for example by placing the event
on a queue to be handled elsewhere).
}
@item[@racket[log-processor]]{
A callback used for all non-event messages and error conditions.
It is called as:
@racketblock[
(log-processor kind msg)
]
where @racket[kind] is a symbol describing the source or category
of the message (for example @racket['stderr-reader] or a tag from
the incoming line) and @racket[msg] is a human-readable string.
}
]
The return value is a @emph{command function} with the contract
@racket[(-> string? string?)]:
@itemlist[
@item{When you call it with a command string, it sends that command
to the @tt{webui-wire} process and blocks until one reply is
received.}
@item{The reply payload (decoded as UTF-8) is returned as a plain
string.}
@item{If anything goes wrong with the IPC protocol (invalid prefix,
unexpected EOF, etc.), an exception is raised.}
]
A typical usage pattern:
@racketblock[
(define (enqueue-event line)
;; Handle incoming EVENT messages from webui-wire
(printf "EVENT: ~a\n" line))
(define (log-message kind msg)
;; Central logging hook
(printf "[~a] ~a\n" kind msg))
;; Start the IPC bridge and obtain the command function:
(define send-cmd
(webui-ipc enqueue-event log-message))
;; Send a command (no newline required) and wait for the reply:
(define reply
(send-cmd "HELLO 42"))
(printf "Reply was: ~a\n" reply)
]
Notes:
@itemlist[
@item{The command function writes a line to the process stdin using
@racket[displayln]; callers do @emph{not} need to append a
newline themselves.}
@item{Calls to the command function are executed one at a time, even
when invoked from multiple threads (see
@secref["ipc-internal-concurrency"]).}
]
}
@section[#:tag "ipc-protocol"]{Protocol overview (informative)}
The @tt{webui-wire} process uses a simple length-prefixed text protocol
on both stdout and stderr. This section describes the format for
reference; the details are handled internally by this module.
@subsection{Message framing}
Each message from the helper has the following structure:
@itemlist[
@item{An 8-character decimal length prefix (ASCII digits),}
@item{followed by a single colon character @tt{":"},}
@item{followed by @emph{exactly} that many bytes of payload,}
@item{followed by a single newline character.}
]
The payload is treated as UTF-8 text by this module.
@subsection{Stdout replies}
On @bold{stdout}:
@itemlist[
@item{Each command sent via the returned command function results in
exactly one reply on stdout with the framing described above.}
@item{The payload is decoded as UTF-8 and returned to the caller as a
plain string.}
]
If the helper sends malformed output (for example, a non-numeric
prefix, incorrect length, or missing colon), the IPC code raises an
error indicating “unexpected input from webui-wire executable”.
@subsection{Stderr events and logs}
On @bold{stderr}:
@itemlist[
@item{Each payload is interpreted as a single logical line;}
@item{The line is expected to start with a symbolic tag followed by
a colon, e.g. @tt{"EVENT:..."} or @tt{"INFO:..."}.}
]
The module distinguishes:
@itemlist[
@item{@tt{EVENT} lines: the text after the @tt{"EVENT:"} prefix is
delivered to the @racket[event-queuer] callback.}
@item{All other tags (and untagged lines): turned into
@racket[(kind msg)] calls to @racket[log-processor], where
@racket[kind] is a symbol derived from the tag or from the
stderr reader, and @racket[msg] is the remaining text.}
]
If the stderr reader encounters EOF, it reports this via
@racket[log-processor] (using an appropriate @racket[kind] symbol) and
then terminates its reader thread.
@section[#:tag "ipc-internal"]{Internal behaviour (informative)}
This section summarises how @racket[webui-ipc] is implemented. The
details are not part of the public API, but they can help to interpret
log messages and errors.
@subsection{Process startup}
When you call @racket[webui-ipc], the following steps are performed:
@itemlist[
@item{Obtain the command string for @tt{webui-wire} by calling
@racket[ww-get-webui-wire-command] from
@racketmodname[webui-wire-download].}
@item{Launch the helper process using @racket[process] from
@racketmodname[racket/system].}
@item{Keep handles to the process stdin, stdout, and stderr ports.}
@item{Spawn a background thread dedicated to reading and decoding
stderr events and log lines.}
]
If the process cannot be started, an exception is raised immediately
from @racket[webui-ipc].
@subsection[#:tag "ipc-internal-concurrency"]{Concurrency and serialisation}
The command function returned by @racket[webui-ipc] may be called from
multiple threads.
Internally, a semaphore is used to ensure that only one caller at a
time:
@itemlist[
@item{writes a command line to the process stdin,}
@item{waits for a single, complete reply on stdout,}
@item{returns that reply to the caller.}
]
This guarantees that requests and replies do not interleave: the reply
string you receive always corresponds to the command you sent in that
call.
The stderr reader runs independently in its own thread and does not
block command calls.
@subsection{Error handling and shutdown}
Several error conditions can occur:
@itemlist[
@item{EOF or broken pipe on stdin/stdout/stderr;}
@item{malformed length prefix or framing errors;}
@item{helper process exiting unexpectedly.}
]
In these cases, the module:
@itemlist[
@item{raises an exception from the command function on the next call
that attempts to send a command or read a reply;}
@item{reports context via @racket[log-processor] (for example, that
the stderr reader hit EOF or that the executable exited).}
]
It is the responsibility of the caller to decide how to react:
typically by catching such exceptions at a higher level, informing the
user, and possibly restarting the application or IPC bridge.
Once the @tt{webui-wire} process has exited, the previously returned
command function is no longer usable; further calls are expected to
fail with an error.