mod data;
pub mod html;
mod icon;
pub(crate) mod node;
mod types;
use std::time::Instant;
pub use data::*;
pub use icon::Icon;
pub use node::{Node, View};
pub use types::*;
use self::node::{
actions_methods, base_methods, children_methods, event_methods, kind_type, max_method, props,
title_method,
};
pub type CardStatus = Status;
#[macro_export]
macro_rules! spans {
($($s:expr),* $(,)?) => {
vec![$($crate::ui::Span::from($s)),*]
};
}
#[macro_export]
macro_rules! nodes {
($($n:expr),* $(,)?) => {
vec![$($crate::ui::Node::from($n)),*]
};
}
pub fn span(t: impl Into<String>) -> Span {
Span::new(t)
}
pub fn node<M>(kind: impl Into<String>) -> Node<M> {
Node::new(kind)
}
macro_rules! common_kind {
($(#[$m:meta])* $name:ident) => {
kind_type!($(#[$m])* $name);
impl<M> $name<M> {
base_methods!();
title_method!();
max_method!();
actions_methods!();
event_methods!();
}
};
}
macro_rules! container_kind {
($(#[$m:meta])* $name:ident) => {
common_kind!($(#[$m])* $name);
impl<M> $name<M> {
children_methods!();
}
};
}
fn new<M, T: From<Node<M>>>(kind: &str) -> T {
T::from(Node::new(kind))
}
macro_rules! from_node {
($($name:ident),*) => {
$(impl<M> From<Node<M>> for $name<M> {
fn from(node: Node<M>) -> Self {
Self(node)
}
})*
};
}
from_node!(
Col, Row, Card, Section, Rule, Spacer, TextNode, Md, Code, Diff, Ansi, Rows, Math, Kv, Table,
Tree, Badge, Kbd, IconNode, Image, List, Item, Tabs, Picker, Spinner, Shimmer, Elapsed, Rate,
Progress, Meter, Chart, Effort, Editor, Input, StatusBar, Seg, Toast, Overlay, Tool, Agent,
Checklist, Prefs
);
container_kind!(
Col
);
impl<M> Col<M> {
props! {
gap(Gap) = "gap";
align(Align) = "align";
justify(Justify) = "justify";
wrap(bool) = "wrap";
}
}
pub fn col<M>() -> Col<M> {
new("col")
}
container_kind!(
Row
);
impl<M> Row<M> {
props! {
gap(Gap) = "gap";
align(Align) = "align";
justify(Justify) = "justify";
wrap(bool) = "wrap";
}
}
pub fn row<M>() -> Row<M> {
new("row")
}
container_kind!(
Card
);
impl<M> Card<M> {
props! {
head(Text) = "head";
status(Status) = "status";
collapsible(bool) = "collapsible";
collapsed(bool) = "collapsed";
preview(Preview) = "preview";
selected(bool) = "selected";
inset(bool) = "inset";
variant(CardVariant) = "variant";
}
}
pub fn card<M>() -> Card<M> {
new("card")
}
container_kind!(
Section
);
impl<M> Section<M> {
props! {
head(Text) = "head";
collapsible(bool) = "collapsible";
collapsed(bool) = "collapsed";
}
}
pub fn section<M>() -> Section<M> {
new("section")
}
common_kind!(
Rule
);
impl<M> Rule<M> {
props! {
label(Text) = "label";
}
}
pub fn rule<M>() -> Rule<M> {
new("rule")
}
common_kind!(
Spacer
);
impl<M> Spacer<M> {
props! {
size(Gap) = "size";
}
}
pub fn spacer<M>() -> Spacer<M> {
new("spacer")
}
common_kind!(
TextNode
);
impl<M> TextNode<M> {
props! {
wrap(Wrap) = "wrap";
truncate(Truncate) = "truncate";
lines(u32) = "lines";
measure(Measure) = "measure";
}
pub fn text(mut self, text: impl Into<Text>) -> Self {
self.0.set_text(text.into());
self
}
}
pub fn text<M>(text: impl Into<Text>) -> TextNode<M> {
let mut node = Node::new("text");
node.set_text(text.into());
TextNode(node)
}
common_kind!(
Md
);
impl<M> Md<M> {
props! {
text(String) = "text";
stream(bool) = "stream";
marks(Vec<Span>) = "marks";
}
}
pub fn md<M>(source: impl Into<String>) -> Md<M> {
new::<M, Md<M>>("md").text(source)
}
common_kind!(
Code
);
impl<M> Code<M> {
props! {
text(String) = "text";
lang(String) = "lang";
path(String) = "path";
numbers(bool) = "numbers";
start(i64) = "start";
marks(Vec<CodeMark>) = "marks";
wrap(bool) = "wrap";
}
}
pub fn code<M>(source: impl Into<String>) -> Code<M> {
new::<M, Code<M>>("code").text(source)
}
common_kind!(
Diff
);
impl<M> Diff<M> {
props! {
text(String) = "text";
hunks(Vec<Hunk>) = "hunks";
path(String) = "path";
lang(String) = "lang";
mode(DiffMode) = "mode";
}
}
pub fn diff<M>() -> Diff<M> {
new("diff")
}
common_kind!(
Ansi
);
impl<M> Ansi<M> {
props! {
text(String) = "text";
cols(u32) = "cols";
follow(bool) = "follow";
}
pub fn preview(mut self, lines: u32) -> Self {
self
.0
.set("preview", &serde_json::json!({ "lines": lines }));
self
}
}
pub fn ansi<M>(output: impl Into<String>) -> Ansi<M> {
new::<M, Ansi<M>>("ansi").text(output)
}
common_kind!(
Rows
);
impl<M> Rows<M> {
props! {
lines(Vec<String>) = "lines";
cols(u32) = "cols";
}
}
pub fn rows<M>(lines: impl Into<Vec<String>>) -> Rows<M> {
new::<M, Rows<M>>("rows").lines(lines)
}
common_kind!(
Math
);
impl<M> Math<M> {
props! {
text(String) = "text";
display(bool) = "display";
}
}
pub fn math<M>(tex: impl Into<String>) -> Math<M> {
new::<M, Math<M>>("math").text(tex)
}
common_kind!(
Kv
);
impl<M> Kv<M> {
props! {
items(Vec<KvItem>) = "items";
layout(KvLayout) = "layout";
}
pub fn item(mut self, k: impl Into<Text>, v: impl Into<Text>) -> Self {
push(&mut self.0, "items", &KvItem { k: k.into(), v: v.into() });
self
}
}
pub fn kv<M>() -> Kv<M> {
new("kv")
}
common_kind!(
Table
);
impl<M> Table<M> {
props! {
cols(Vec<Column>) = "cols";
rows(Vec<TableRow>) = "rows";
}
pub fn col(mut self, col: Column) -> Self {
push(&mut self.0, "cols", &col);
self
}
pub fn row(mut self, row: TableRow) -> Self {
push(&mut self.0, "rows", &row);
self
}
}
pub fn table<M>() -> Table<M> {
new("table")
}
common_kind!(
Tree
);
impl<M> Tree<M> {
props! {
nodes(Vec<TreeItem>) = "nodes";
}
pub fn item(mut self, item: TreeItem) -> Self {
push(&mut self.0, "nodes", &item);
self
}
}
pub fn tree<M>() -> Tree<M> {
new("tree")
}
common_kind!(
Badge
);
impl<M> Badge<M> {
props! {
text(String) = "text";
}
}
pub fn badge<M>(text: impl Into<String>) -> Badge<M> {
new::<M, Badge<M>>("badge").text(text)
}
common_kind!(
Kbd
);
impl<M> Kbd<M> {
props! {
keys(Vec<String>) = "keys";
}
}
pub fn kbd<M, K: Into<String>>(keys: impl IntoIterator<Item = K>) -> Kbd<M> {
new::<M, Kbd<M>>("kbd").keys(keys.into_iter().map(Into::into).collect::<Vec<_>>())
}
common_kind!(
IconNode
);
impl<M> IconNode<M> {
props! {
name(Icon) = "name";
}
}
pub fn icon<M>(name: impl Into<Icon>) -> IconNode<M> {
new::<M, IconNode<M>>("icon").name(name)
}
common_kind!(
Image
);
impl<M> Image<M> {
props! {
blob(String) = "blob";
builtin(String) = "builtin";
alt(String) = "alt";
w(f64) = "w";
h(f64) = "h";
path(String) = "path";
}
}
pub fn image<M>() -> Image<M> {
new("image")
}
kind_type!(
List
);
impl<M> List<M> {
base_methods!();
title_method!();
actions_methods!();
event_methods!();
children_methods!();
props! {
selected(String) = "selected";
filter(String) = "filter";
empty(Text) = "empty";
virtualize(bool) = "virtual";
}
pub fn max_lines(mut self, lines: u32) -> Self {
self.0.set("max", &serde_json::json!({ "lines": lines }));
self
}
pub fn max_fraction(mut self, fraction: f64) -> Self {
self.0.set("max", &fraction);
self
}
}
pub fn list<M>() -> List<M> {
new("list")
}
common_kind!(
Item
);
impl<M> Item<M> {
props! {
label(Text) = "label";
detail(Text) = "detail";
value(Text) = "value";
icon(Icon) = "icon";
hint(Vec<String>) = "hint";
disabled(bool) = "disabled";
}
}
pub fn item<M>(label: impl Into<Text>) -> Item<M> {
new::<M, Item<M>>("item").label(label)
}
common_kind!(
Tabs
);
impl<M> Tabs<M> {
props! {
items(Vec<Tab>) = "items";
active(String) = "active";
}
pub fn tab(mut self, id: impl Into<String>, label: impl Into<Text>) -> Self {
push(&mut self.0, "items", &Tab { id: id.into(), label: label.into() });
self
}
}
pub fn tabs<M>() -> Tabs<M> {
new("tabs")
}
kind_type!(
Picker
);
impl<M> Picker<M> {
base_methods!();
max_method!();
event_methods!();
children_methods!();
props! {
size(PickerSize) = "size";
layout(PickerLayout) = "layout";
preview(PickerPreview) = "preview";
title(Text) = "title";
subtitle(Text) = "subtitle";
icon(Icon) = "icon";
query(String) = "query";
cursor(usize) = "cursor";
placeholder(String) = "placeholder";
noun(String) = "noun";
focus(PickerFocus) = "focus";
state(PickerState) = "state";
message(Text) = "message";
empty(Text) = "empty";
total(u64) = "total";
items(Vec<PickerItem>) = "items";
items_add(Vec<PickerItem>) = "itemsAdd";
items_del(Vec<String>) = "itemsDel";
order(Vec<OrderEntry>) = "order";
selected(String) = "selected";
current(Vec<String>) = "current";
hits(std::collections::BTreeMap<String, Vec<[usize; 2]>>) = "hits";
columns(Vec<PickerColumn>) = "columns";
confirm(Confirm) = "confirm";
scopes(Vec<Scope>) = "scopes";
scope(String) = "scope";
tabs(Vec<PickerTab>) = "tabs";
tab(String) = "tab";
strip(Strip) = "strip";
actions(Vec<PickerAction>) = "actions";
}
}
pub fn picker<M>() -> Picker<M> {
new("picker")
}
common_kind!(
Spinner
);
impl<M> Spinner<M> {
props! {
style(SpinnerStyle) = "style";
label(Text) = "label";
}
}
pub fn spinner<M>() -> Spinner<M> {
new("spinner")
}
common_kind!(
Shimmer
);
impl<M> Shimmer<M> {
props! {
mode(ShimmerMode) = "mode";
palette(ShimmerPalette) = "palette";
}
pub fn text(mut self, text: impl Into<Text>) -> Self {
self.0.set_text(text.into());
self
}
}
pub fn shimmer<M>(text: impl Into<Text>) -> Shimmer<M> {
new::<M, Shimmer<M>>("shimmer").text(text)
}
common_kind!(
Elapsed
);
impl<M> Elapsed<M> {
props! {
age(i64) = "age";
stopped(u64) = "stopped";
format(ElapsedFormat) = "format";
}
pub fn since(self, start: Instant) -> Self {
let age = i64::try_from(start.elapsed().as_millis()).unwrap_or(i64::MAX);
self.age(age)
}
}
pub fn elapsed<M>() -> Elapsed<M> {
new("elapsed")
}
common_kind!(
Rate
);
impl<M> Rate<M> {
props! {
value(f64) = "value";
unit(String) = "unit";
}
}
pub fn rate<M>(value: f64) -> Rate<M> {
new::<M, Rate<M>>("rate").value(value)
}
common_kind!(
Progress
);
impl<M> Progress<M> {
props! {
value(f64) = "value";
label(Text) = "label";
}
}
pub fn progress<M>() -> Progress<M> {
new("progress")
}
common_kind!(
Meter
);
impl<M> Meter<M> {
props! {
value(f64) = "value";
parts(Vec<Part>) = "parts";
thresholds(Thresholds) = "thresholds";
style(MeterStyle) = "style";
size(Size) = "size";
steps(u32) = "steps";
marks(Vec<MeterMark>) = "marks";
label(Text) = "label";
total(Text) = "total";
}
}
pub fn meter<M>() -> Meter<M> {
new("meter")
}
common_kind!(
Chart
);
impl<M> Chart<M> {
props! {
kind(ChartKind) = "kind";
size(Size) = "size";
token(String) = "token";
summary(Text) = "summary";
series(Vec<Point>) = "series";
cells(Vec<Vec<Option<f64>>>) = "cells";
tips(Vec<Vec<Option<String>>>) = "tips";
rows(Vec<String>) = "rows";
cols(Vec<ChartCol>) = "cols";
}
}
pub fn chart<M>(kind: impl Into<ChartKind>) -> Chart<M> {
new::<M, Chart<M>>("chart").kind(kind)
}
common_kind!(
Effort
);
impl<M> Effort<M> {
props! {
level(EffortLevel) = "level";
}
}
pub fn effort<M>(level: impl Into<EffortLevel>) -> Effort<M> {
new::<M, Effort<M>>("effort").level(level)
}
common_kind!(
Editor
);
impl<M> Editor<M> {
props! {
text(String) = "text";
cursor(usize) = "cursor";
anchor(usize) = "anchor";
decor(Vec<Decor>) = "decor";
ghost(String) = "ghost";
placeholder(String) = "placeholder";
prompt(Text) = "prompt";
mode(String) = "mode";
lang(String) = "lang";
readonly(bool) = "readonly";
sendable(bool) = "sendable";
max_lines(u32) = "maxLines";
}
}
pub fn editor<M>() -> Editor<M> {
new("editor")
}
common_kind!(
Input
);
impl<M> Input<M> {
props! {
text(String) = "text";
cursor(usize) = "cursor";
anchor(usize) = "anchor";
decor(Vec<Decor>) = "decor";
ghost(String) = "ghost";
placeholder(String) = "placeholder";
prompt(Text) = "prompt";
mode(String) = "mode";
lang(String) = "lang";
readonly(bool) = "readonly";
sendable(bool) = "sendable";
}
}
pub fn input<M>() -> Input<M> {
new("input")
}
container_kind!(
StatusBar
);
impl<M> StatusBar<M> {
props! {
transparent(bool) = "transparent";
}
}
pub fn status<M>() -> StatusBar<M> {
new("status")
}
container_kind!(
Seg
);
impl<M> Seg<M> {
props! {
icon(Icon) = "icon";
side(Side) = "side";
priority(f64) = "priority";
}
pub fn text(mut self, text: impl Into<Text>) -> Self {
self.0.set_text(text.into());
self
}
}
pub fn seg<M>(text: impl Into<Text>) -> Seg<M> {
new::<M, Seg<M>>("seg").text(text)
}
common_kind!(
Toast
);
impl<M> Toast<M> {
props! {
text(String) = "text";
#[allow(clippy::should_implement_trait, reason = "the prop is named `sub`")]
sub(String) = "sub";
ttl(u64) = "ttl";
}
}
pub fn toast<M>(text: impl Into<String>) -> Toast<M> {
new::<M, Toast<M>>("toast").text(text)
}
container_kind!(
Overlay
);
impl<M> Overlay<M> {
props! {
anchor(Anchor) = "anchor";
size(OverlaySize) = "size";
modal(bool) = "modal";
head(Text) = "head";
}
}
pub fn overlay<M>() -> Overlay<M> {
new("overlay")
}
kind_type!(
Tool
);
impl<M> Tool<M> {
base_methods!();
max_method!();
actions_methods!();
event_methods!();
children_methods!();
props! {
name(String) = "name";
title(Text) = "title";
target(Text) = "target";
target_kind(TargetKind) = "targetKind";
meta(Vec<Text>) = "meta";
badges(Vec<BadgeSpec>) = "badges";
note(Text) = "note";
exit(i64) = "exit";
status(Status) = "status";
age(i64) = "age";
took(u64) = "took";
intent(String) = "intent";
frame(ToolFrame) = "frame";
collapsible(bool) = "collapsible";
collapsed(bool) = "collapsed";
preview(ToolPreview) = "preview";
tools(Vec<ToolButton>) = "tools";
}
}
pub fn tool<M>(name: impl Into<String>) -> Tool<M> {
new::<M, Tool<M>>("tool").name(name)
}
container_kind!(
Agent
);
impl<M> Agent<M> {
props! {
name(String) = "name";
agent(String) = "agent";
badges(Vec<BadgeSpec>) = "badges";
task(Text) = "task";
status(AgentStatus) = "status";
model(String) = "model";
thinking(String) = "thinking";
stats(AgentStats) = "stats";
tool(AgentTool) = "tool";
retry(Retry) = "retry";
depth(u32) = "depth";
collapsible(bool) = "collapsible";
collapsed(bool) = "collapsed";
}
}
pub fn agent<M>(name: impl Into<String>) -> Agent<M> {
new::<M, Agent<M>>("agent").name(name)
}
common_kind!(
Checklist
);
impl<M> Checklist<M> {
props! {
phases(Vec<Phase>) = "phases";
mode(ChecklistMode) = "mode";
note(Text) = "note";
}
}
pub fn checklist<M>() -> Checklist<M> {
new("checklist")
}
kind_type!(
Prefs
);
impl<M> Prefs<M> {
base_methods!();
max_method!();
actions_methods!();
event_methods!();
children_methods!();
props! {
title(String) = "title";
pages(Vec<PrefsPage>) = "pages";
page(String) = "page";
lead(String) = "lead";
sections(Vec<PrefsSection>) = "sections";
query(String) = "query";
cursor(usize) = "cursor";
focus(String) = "focus";
editing(Editing) = "editing";
}
}
pub fn prefs<M>() -> Prefs<M> {
new("prefs")
}
fn push<M>(node: &mut Node<M>, key: &str, value: &impl serde::Serialize) {
let Ok(value) = serde_json::to_value(value) else {
return;
};
match node.props.get_mut(key) {
Some(serde_json::Value::Array(list)) => list.push(value),
_ => {
node
.props
.insert(key.to_owned(), serde_json::Value::Array(vec![value]));
},
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn wire<M>(node: impl Into<Node<M>>) -> serde_json::Value {
let node = node.into();
json!({ "k": node.kind, "p": node.props })
}
#[test]
fn text_takes_a_string_or_spans() {
assert_eq!(wire::<()>(text("hi")), json!({"k": "text", "p": {"text": "hi"}}));
let styled = text::<()>(spans!["a", span("b").style(Token::Muted).style("strong")]);
assert_eq!(
wire(styled.clone()),
json!({"k": "text", "p": {"spans": [{"t": "a"}, {"t": "b", "s": "muted strong"}]}})
);
assert_eq!(wire(styled.text("c")), json!({"k": "text", "p": {"text": "c"}}));
}
#[test]
fn typed_values_and_escape_hatches_serialize() {
let c = card::<()>()
.head("Deploy")
.status(Status::Running)
.tone("brand-new")
.min(Bound::w(Extent::Ch(10.0)))
.basis(Basis::Content)
.key(7)
.prop("future", json!([1]))
.prop("head", serde_json::Value::Null);
assert_eq!(
wire(c),
json!({"k": "card", "p": {
"status": "running", "tone": "brand-new", "min": {"w": "10ch"},
"basis": "content", "key": 7, "future": [1]
}})
);
}
#[test]
fn kind_specific_props_replace_common_meanings() {
let title = spans![span("Heading").style("strong")];
assert_eq!(
wire(tool::<()>("read").title(title.clone()))["p"]["title"],
json!([{"t": "Heading", "s": "strong"}])
);
assert_eq!(
wire(picker::<()>().title(title).actions(vec![PickerAction {
id: "open".into(),
primary: Some(true),
..PickerAction::default()
}]))["p"],
json!({
"title": [{"t": "Heading", "s": "strong"}],
"actions": [{"id": "open", "primary": true}]
})
);
assert_eq!(wire(prefs::<()>().title("Settings"))["p"]["title"], "Settings");
assert_eq!(
wire(list::<()>().max_fraction(0.5).max_lines(4))["p"]["max"],
json!({"lines": 4})
);
assert_eq!(wire(list::<()>().max_lines(4).max_fraction(0.5))["p"]["max"], 0.5);
}
#[test]
fn agent_statuses_have_their_own_vocabulary() {
for (status, expected) in [
(AgentStatus::Pending, "pending"),
(AgentStatus::Running, "running"),
(AgentStatus::Done, "done"),
(AgentStatus::Failed, "failed"),
(AgentStatus::Aborted, "aborted"),
(AgentStatus::Idle, "idle"),
(AgentStatus::Parked, "parked"),
(AgentStatus::Other("future".into()), "future"),
] {
assert_eq!(wire(agent::<()>("worker").status(status))["p"]["status"], expected);
}
}
#[test]
fn table_meter_cells_keep_the_wire_wrapper() {
let node = wire(table::<()>().row(TableRow::new("a").cell("usage", MeterCell {
value: Some(0.5),
title: Some("Half".into()),
tone: Some(Tone::Success),
..MeterCell::default()
})));
assert_eq!(
node["p"]["rows"][0]["cells"]["usage"],
json!({
"meter": {"value": 0.5, "title": "Half", "tone": "success"}
})
);
assert_eq!(
serde_json::to_value(Cell::Meter(MeterCell::default())).unwrap(),
json!({"meter": {}})
);
}
#[test]
fn list_props_append() {
let k = kv::<()>().item("a", "1").item("b", span("2").style("num"));
assert_eq!(
wire(k),
json!({"k": "kv", "p": {"items": [
{"k": "a", "v": "1"}, {"k": "b", "v": [{"t": "2", "s": "num"}]}
]}})
);
}
}