Builders for the two things an extension shows the operator: the typed pause
it WAITS on — com.blockether.vis.core/request-human-input! — and the live
view it does not, com.blockether.vis.core/with-live-view!.
A request is plain data, and it stays plain data: every builder here returns
the very map you could have typed by hand. What it adds is that the two
things a hand-typed map gets wrong cannot happen. The node TYPE is the
function you called, so :type "plaintxt" is a compile-time unresolved
symbol instead of a refused request at run time; and the node is VALIDATED
the moment it is built, by the engine's own com.blockether.vis.internal.view.core/normalize-node
seam, so a bad :default, an unknown key or a :select with no options
throws at the line that built it rather than in front of the human.
(require '[com.blockether.vis.core :as vis]
'[com.blockether.vis.view :as view])
(vis/request-human-input!
(view/form {:title "Deploy" :description "Where this build lands."}
(view/heading "Target")
(view/paragraph "Staging pages nobody.")
(view/row (view/select "env" ["staging" "prod"] {:label "Environment"
:is-required true})
(view/slider "canary" {:label "Canary %" :min 0 :max 100 :step 5}))
(view/password "token" {:label "Deploy token" :is-required true})))
Three node contracts, exactly as the engine sees them: a FIELD holds one
answer and is keyed by its name, a GROUP (row / column) only arranges
the nodes below it, and a DECORATION (heading / paragraph) is ink —
no name, never focusable, never in the answer map.
Every optional key is the one the engine documents, in either spelling
(:is-required or "is_required"): builders pass options through
untouched instead of keeping a second copy of the vocabulary.
A LIVE VIEW is the same discipline one :kind further: view and the node
builders under it declare a picture the human WATCHES while the work runs. It
asks nothing and parks no thread; it is patched by node id and ends in a
verdict carrying the markdown the model reads.
(vis/with-live-view!
(view/view {:title "CI"}
(view/status "now" "Polling GitHub…" {:tone "running"})
(view/table "jobs" [(view/table-column "job" "Job")
(view/table-column "took" "Took" {:align "right"})]))
(fn [view-id]
(vis/patch-live-view!
view-id
[{:op "set" :node-id "now" :text "18 jobs" :tone "ok"}
{:op "append" :node-id "jobs"
:rows [(view/table-row "build" ["tests / ubuntu" "13m0s"] {:tone "ok"})]}])))
Python extensions get the SAME names on the vis module —
vis.select('env', ['staging', 'prod'], label='Environment') — built by
the same engine seam across the JSON boundary.
Builders for the two things an extension shows the operator: the typed pause
it WAITS on — `com.blockether.vis.core/request-human-input!` — and the live
view it does not, `com.blockether.vis.core/with-live-view!`.
A request is plain data, and it stays plain data: every builder here returns
the very map you could have typed by hand. What it adds is that the two
things a hand-typed map gets wrong cannot happen. The node TYPE is the
function you called, so `:type "plaintxt"` is a compile-time unresolved
symbol instead of a refused request at run time; and the node is VALIDATED
the moment it is built, by the engine's own [[com.blockether.vis.internal.view.core/normalize-node]]
seam, so a bad `:default`, an unknown key or a `:select` with no options
throws at the line that built it rather than in front of the human.
(require '[com.blockether.vis.core :as vis]
'[com.blockether.vis.view :as view])
(vis/request-human-input!
(view/form {:title "Deploy" :description "Where this build lands."}
(view/heading "Target")
(view/paragraph "Staging pages nobody.")
(view/row (view/select "env" ["staging" "prod"] {:label "Environment"
:is-required true})
(view/slider "canary" {:label "Canary %" :min 0 :max 100 :step 5}))
(view/password "token" {:label "Deploy token" :is-required true})))
Three node contracts, exactly as the engine sees them: a FIELD holds one
answer and is keyed by its name, a GROUP ([[row]] / [[column]]) only arranges
the nodes below it, and a DECORATION ([[heading]] / [[paragraph]]) is ink —
no name, never focusable, never in the answer map.
Every optional key is the one the engine documents, in either spelling
(`:is-required` or `"is_required"`): builders pass options through
untouched instead of keeping a second copy of the vocabulary.
A LIVE VIEW is the same discipline one `:kind` further: [[view]] and the node
builders under it declare a picture the human WATCHES while the work runs. It
asks nothing and parks no thread; it is patched by node id and ends in a
verdict carrying the markdown the model reads.
(vis/with-live-view!
(view/view {:title "CI"}
(view/status "now" "Polling GitHub…" {:tone "running"})
(view/table "jobs" [(view/table-column "job" "Job")
(view/table-column "took" "Took" {:align "right"})]))
(fn [view-id]
(vis/patch-live-view!
view-id
[{:op "set" :node-id "now" :text "18 jobs" :tone "ok"}
{:op "append" :node-id "jobs"
:rows [(view/table-row "build" ["tests / ubuntu" "13m0s"] {:tone "ok"})]}])))
Python extensions get the SAME names on the `vis` module —
`vis.select('env', ['staging', 'prod'], label='Environment')` — built by
the same engine seam across the JSON boundary.(add-node node)(add-node node after)Add a whole node to a RUNNING view — a second table, a per-device log.
after names the node it lands behind; without it, it goes last.
Add a whole `node` to a RUNNING view — a second table, a per-device log. `after` names the node it lands behind; without it, it goes last.
(button id label)(button id label opts)Live operator action; accepted presses increment :clicks in shared state.
Live operator action; accepted presses increment :clicks in shared state.
(checkbox field-name)(checkbox field-name opts)One box, answered as a boolean. :is-required means it must end up TICKED,
which is how a consent line is expressed.
One box, answered as a boolean. `:is-required` means it must end up TICKED, which is how a consent line is expressed.
(code id text)(code id text opts)Literal live code; :language is optional and whitespace is preserved.
Literal live code; :language is optional and whitespace is preserved.
(column & nodes)Stack nodes one under the next — the default arrangement, worth saying
explicitly inside a row. Takes a leading id in a live view, exactly like
row.
Stack `nodes` one under the next — the default arrangement, worth saying explicitly inside a [[row]]. Takes a leading id in a live view, exactly like [[row]].
(disclosure id label nodes & [opts])A collapsible live column. :default-expanded applies only to its initial active frame.
A collapsible live column. :default-expanded applies only to its initial active frame.
(form opts & nodes)The request map com.blockether.vis.core/request-human-input! takes, built
from opts and the nodes that follow it — and refused right here if it is
not one.
opts needs at least a :title, and may carry :description,
:submit-label, :cancel-label, :is-cancellable, :timeout-ms (0 waits
indefinitely) and :channel-ids. At least one node is required, and the
answerable ones must have distinct names.
The request map `com.blockether.vis.core/request-human-input!` takes, built from `opts` and the `nodes` that follow it — and refused right here if it is not one. `opts` needs at least a `:title`, and may carry `:description`, `:submit-label`, `:cancel-label`, `:is-cancellable`, `:timeout-ms` (0 waits indefinitely) and `:channel-ids`. At least one node is required, and the answerable ones must have distinct names.
(heading text)(heading id text)(heading id text opts)A form title, or live heading addressed by id with :level 1–6.
A form title, or live heading addressed by id with :level 1–6.
(link id links)(link id links opts)Labeled pointers the human OPENS: each of links is
{:id … :label … :target … :target-kind …} — an attachment, a path or a url.
Labeled pointers the human OPENS: each of `links` is
`{:id … :label … :target … :target-kind …}` — an attachment, a path or a url.(log id)(log id opts)Append-only lines — the scrollback. :lines seeds it and :window-lines says
how many a surface holds hot; the view's record on disk keeps every line
either way, so a window is a paint budget, never a loss. Optional :line-tones
align with seeded lines (nil is plain); append ops accept one :tone for their
lines. Redact before publishing. Terminal controls display literally.
Append-only lines — the scrollback. `:lines` seeds it and `:window-lines` says how many a surface holds hot; the view's record on disk keeps every line either way, so a window is a paint budget, never a loss. Optional `:line-tones` align with seeded lines (nil is plain); append ops accept one `:tone` for their lines. Redact before publishing. Terminal controls display literally.
(multiline field-name)(multiline field-name opts)A multi-line text box, answered as a string with its newlines and its
leading whitespace intact. Takes the same opts as plaintext.
A multi-line text box, answered as a string with its newlines and its leading whitespace intact. Takes the same `opts` as [[plaintext]].
(multiselect field-name options)(multiselect field-name options opts)Choose ANY of options, answered as a vector of the chosen values (empty
when nothing is ticked). Same options shape as select.
Choose ANY of `options`, answered as a vector of the chosen values (empty when nothing is ticked). Same `options` shape as [[select]].
(option value)(option value label)One entry for a select / multiselect: the value that is answered
and, optionally, the label shown instead of it.
An option is not a node, so it is checked by the field that offers it.
One entry for a [[select]] / [[multiselect]]: the `value` that is answered and, optionally, the `label` shown instead of it. An option is not a node, so it is checked by the field that offers it.
(otp field-name)(otp field-name opts)A one-time code in digit boxes, answered as an opaque vis-secret: handle —
a code opens the account once, so it is a secret exactly like a password.
:min-length / :max-length say how many digits (6 by default, 12 at most).
A one-time code in digit boxes, answered as an opaque `vis-secret:` handle — a code opens the account once, so it is a secret exactly like a password. `:min-length` / `:max-length` say how many digits (6 by default, 12 at most).
(paragraph text)(paragraph id text)(paragraph id text opts)Form prose, or an addressed live paragraph with inline Markdown.
Form prose, or an addressed live paragraph with inline Markdown.
(password field-name)(password field-name opts)A typed line whose characters are masked, answered as an opaque
vis-secret: HANDLE — never the plaintext. Read it with
com.blockether.vis.core/reveal-human-input-secret on the trusted side.
Takes the same opts as plaintext.
A typed line whose characters are masked, answered as an opaque `vis-secret:` HANDLE — never the plaintext. Read it with `com.blockether.vis.core/reveal-human-input-secret` on the trusted side. Takes the same `opts` as [[plaintext]].
(plaintext field-name)(plaintext field-name opts)One typed line, answered as a string.
opts may carry :label, :description, :placeholder, :default,
:is-required, :min-length, :max-length and :validate.
One typed line, answered as a string. `opts` may carry `:label`, `:description`, `:placeholder`, `:default`, `:is-required`, `:min-length`, `:max-length` and `:validate`.
(progress id)(progress id opts)How far the work has come: :done of :total, or a :value between 0 and 1.
Neither means INDETERMINATE, which is the honest picture while a job queues.
How far the work has come: `:done` of `:total`, or a `:value` between 0 and 1. Neither means INDETERMINATE, which is the honest picture while a job queues.
(remove-node node-id)Drop node node-id from a running view, its items with it.
Drop node `node-id` from a running view, its items with it.
(row & nodes)Lay nodes out side by side. A group holds no value and never appears in
the answer map; groups nest freely.
A LIVE view arranges its work with the same two builders, and there a group
is addressed like every other node, so it opens with its id:
(hi/row "reading" (hi/table "hosts" …) (hi/status "why" …)) —
add-node :after names it, remove-node takes it with its children, and no
patch ever moves it.
Lay `nodes` out side by side. A group holds no value and never appears in the answer map; groups nest freely. A LIVE view arranges its work with the same two builders, and there a group is addressed like every other node, so it opens with its id: `(hi/row "reading" (hi/table "hosts" …) (hi/status "why" …))` — `add-node :after` names it, `remove-node` takes it with its children, and no patch ever moves it.
(select field-name options)(select field-name options opts)Choose exactly ONE of options, answered as that option's value.
options is a vector of plain values or option maps. A :default must
be one of the values offered.
Choose exactly ONE of `options`, answered as that option's value. `options` is a vector of plain values or [[option]] maps. A `:default` must be one of the values offered.
(slider field-name)(slider field-name opts)A number on a track, answered as a NUMBER: :min / :max / :step
(0 / 100 / 1 when unsaid), :default inside its own track.
The wire type is range; the builder is spelled slider so it never
shadows clojure.core/range — and so the Python mirror never shadows the
range builtin either.
A number on a track, answered as a NUMBER: `:min` / `:max` / `:step` (0 / 100 / 1 when unsaid), `:default` inside its own track. The wire type is `range`; the builder is spelled `slider` so it never shadows `clojure.core/range` — and so the Python mirror never shadows the `range` builtin either.
(spinner id text)(spinner id text opts)Live activity indicator; :variant is braille, dots, line or pulse.
Live activity indicator; :variant is braille, dots, line or pulse.
(stat id stats)(stat id stats opts)A strip of counters upserted by id — the score. Each entry of stats is
{:id … :label … :value-text … :tone …}.
A strip of counters upserted by id — the score. Each entry of `stats` is
`{:id … :label … :value-text … :tone …}`.(status id text)(status id text opts)One line saying what is happening RIGHT NOW — replaced in place, never
appended, so the top of the view never scrolls. opts may carry :label,
:detail and a :tone.
One line saying what is happening RIGHT NOW — replaced in place, never appended, so the top of the view never scrolls. `opts` may carry `:label`, `:detail` and a `:tone`.
(steps id items)(steps id items opts)An ORDERED checklist: the shape of a pipeline, in the order it runs. Each of
items is {:id … :label … :tone … :detail … :value …} and carries its own
tone, because a step is where a run goes wrong.
An ORDERED checklist: the shape of a pipeline, in the order it runs. Each of
`items` is `{:id … :label … :tone … :detail … :value …}` and carries its own
tone, because a step is where a run goes wrong.(table id columns)(table id columns opts)Rows upserted and removed by row id, painted in the :order the view
DECLARES — columns are table-columns, :rows seeds it and :max-rows
bounds it by refusal. :is-selectable true turns rows into controls;
:selected-ids is the shared current selection extensions read from state.
Rows upserted and removed by row id, painted in the `:order` the view DECLARES — `columns` are [[table-column]]s, `:rows` seeds it and `:max-rows` bounds it by refusal. `:is-selectable true` turns rows into controls; `:selected-ids` is the shared current selection extensions read from state.
(table-column id label)(table-column id label opts)One column of a table: the id a cell is addressed by and the label
over it, optionally :align "right" for numbers.
A column is not a node, so it is checked by the table that declares it.
One column of a [[table]]: the `id` a cell is addressed by and the `label` over it, optionally `:align` `"right"` for numbers. A column is not a node, so it is checked by the table that declares it.
(table-row id cells)(table-row id cells opts)One row of a table, keyed by id: cells in column order, optionally
:toned or placed below a collapsible :branch label shared by sibling rows.
Built rather than typed because it is the one POSITIONAL thing here — a cell
means whatever column stands over it.
A row is not a node, so it is checked by the table, or the patch, carrying it.
One row of a [[table]], keyed by `id`: `cells` in column order, optionally `:tone`d or placed below a collapsible `:branch` label shared by sibling rows. Built rather than typed because it is the one POSITIONAL thing here — a cell means whatever column stands over it. A row is not a node, so it is checked by the table, or the patch, carrying it.
(view opts & nodes)The live view com.blockether.vis.core/open-live-view! mounts, built from
opts and the nodes that follow it — and refused right here if it is not
one.
opts needs at least a :title, and may carry :description, :source,
:channel-ids and :timeout-ms. There is no cancellable flag: a human can
stop watching ANY view. At least one node is required, and their ids must be
distinct: every patch names the node it speaks to.
The live view `com.blockether.vis.core/open-live-view!` mounts, built from `opts` and the `nodes` that follow it — and refused right here if it is not one. `opts` needs at least a `:title`, and may carry `:description`, `:source`, `:channel-ids` and `:timeout-ms`. There is no cancellable flag: a human can stop watching ANY view. At least one node is required, and their ids must be distinct: every patch names the node it speaks to.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |