-
This commit is contained in:
10
info.rkt
10
info.rkt
@@ -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.
@@ -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)
|
||||
|
||||
108
scribblings/web-racket-version.scrbl
Normal file
108
scribblings/web-racket-version.scrbl
Normal 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"].
|
||||
}
|
||||
641
scribblings/web-racket.scrbl
Normal file
641
scribblings/web-racket.scrbl
Normal 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 module’s 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 isn’t 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 element’s existing style.}
|
||||
|
||||
@defmethod[(set-style! [st css-style?]) void?]{
|
||||
Replace the element’s 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 input’s 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 element’s 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
|
||||
Racket’s @tt{set!} naming style.
|
||||
}
|
||||
}
|
||||
965
scribblings/web-wire.scrbl
Normal file
965
scribblings/web-wire.scrbl
Normal 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 helper’s 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)))))
|
||||
)
|
||||
]
|
||||
}
|
||||
187
scribblings/webui-wire-download.scrbl
Normal file
187
scribblings/webui-wire-download.scrbl
Normal 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”).
|
||||
}
|
||||
259
scribblings/webui-wire-ipc.scrbl
Normal file
259
scribblings/webui-wire-ipc.scrbl
Normal 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.
|
||||
Reference in New Issue
Block a user