Build your own client
Edit on GitHubAn extension never draws: it composes catalog components. When the catalog lacks something (a Gantt chart, a domain view), you build your own client: one of Orrery's terminal clients as a library, plus your components, built into one program. Two libraries: the ratatui client in Rust (--from ratatui), and the Ink client in TypeScript and React (--from ink). It is installed like any client and draws everything the built-in TUI draws.
Scaffold one
orrery client new --from ratatui acme-client
cd acme-client
cargo build --release
cp target/release/acme-client . # acme-client.exe on Windows
orrery install .
orrery --ui acme-clientorrery client new writes four files into ./acme-client and fails, naming another name to use, if the folder exists:
| File | What |
|---|---|
Cargo.toml | A project on orrery-client-ratatui, at the version this orrery ships. |
src/main.rs | Client::builder() with one example component, acme-client.card. |
orrery.toml | [[provides.clients]]: the executable, and draws listing the library's elements plus your kind. |
README.md | How to build, install and add a component. |
For Ink, see Ink: scaffold one.
Add a component
A component draws one element id from that element's props, as ratatui lines:
use orrery_client_ratatui::{Client, Component};
use ratatui::text::Line;
use serde_json::Value;
struct Gantt;
impl Component for Gantt {
fn lines(&self, props: &Value, width: u16) -> Vec<Line<'static>> {
// one line per task, at most `width` columns
vec![Line::from(format!("{} tasks", props["tasks"].as_array().map_or(0, Vec::len)))]
}
}
fn main() -> std::process::ExitCode {
Client::builder().component("acme.gantt", Gantt).run()
}A closure |props: &Value, width: u16| -> Vec<Line<'static>> works too. Add the id to draws in orrery.toml.
| Call | Does |
|---|---|
Client::builder() | The whole client, with no components of yours: what orrery-ratatui is. |
.component(id, c) | Draw custom nodes and surfaces of kind id with c. id is <ext>.<name>. |
.build() | Register the components. Fails with the fix on an id that is not <ext>.<name> or one given twice. |
.run() | Build, then run as the host's client process (and answer the install-time fixture probe). Exits 2 on a build error. |
components::draw(id, props, width) | What your component draws, for a test. |
Ink: scaffold one
orrery client new --from ink acme-ink
cd acme-ink
pnpm add link:<orrery-checkout>/harness/clients/ink # until @getorrery/client-ink is on npm
pnpm install
pnpm run build
orrery install .
orrery --ui acme-inkIt writes five files into ./acme-ink, with the same refusals:
| File | What |
|---|---|
package.json | A pnpm project on @getorrery/client-ink at the version this orrery ships, ink and react at the versions it uses, and a build script bundling src/main.tsx into dist/main.js. |
tsconfig.json | TypeScript with React's JSX. |
src/main.tsx | createClient() with one example component, acme-ink.card. |
orrery.toml | runtime = "node", [[provides.clients]] with entry = "dist/main.js", and draws listing the Ink library's elements plus your kind. |
README.md | How to build, install and add a component. |
Ink: add a component
A component is an Ink/React component handed { props, width, kind }:
import { createClient, type ComponentProps } from "@getorrery/client-ink";
import { Text } from "ink";
function Gantt({ props, width }: ComponentProps) {
const tasks = Array.isArray(props["tasks"]) ? props["tasks"] : [];
return <Text>{tasks.length} tasks in {width} columns</Text>;
}
createClient({ components: { "acme.gantt": Gantt } }).run();| Call | Does |
|---|---|
createClient() | The whole Ink client, with no components of yours: what orrery --ui ink runs. |
createClient({ components }) | Draw custom nodes and surfaces of each key's kind with its component. A key is <ext>.<name>. Throws Misconfigured with the fix on a key that is not, or one already registered to another component. |
.run() | Run as the host's client process (or against --endpoint), and answer the install-time fixture probe under ORRERY_CONFORMANCE=1. Exits 2 when misconfigured. |
componentLines(id, props, width) | What your component draws in the full screen, as rows, for a test. |
The element is declared as below, exactly as for ratatui. The worked example is harness/extensions/examples/acme-client-ink: the same Gantt chart, schema, fallback and fixture, in Ink, declared as acme-ink.gantt so it installs beside acme-client.
Declare the element
The extension that sends the element declares it, so every other client still draws something:
[[provides.elements]]
id = "acme.gantt"
props = "gantt.schema.json" # a JSON schema for its props
fallback = "table" # a catalog element every client draws
fixture = "fixtures/acme.gantt.json"It sends a custom node with the fallback beside the props:
{ "type": "custom", "kind": "acme.gantt",
"props": { "tasks": [{ "name": "build", "start": 2, "end": 9 }] },
"fallback": { "type": "table", "columns": ["task", "days"], "rows": [["build", "2-9"]] } }Your client draws the Gantt chart. Every other client draws the table; one without the catalog draws the props as text.
With fixtures = "fixtures" in your client's entry, orrery install hands it fixtures/acme.gantt.json in probe mode and your component answers it. A client missing a required element is refused, as any client is; the library draws all of them, so yours passes from the start.
The worked example is harness/extensions/examples/acme-client: a Gantt chart, its schema, its fallback and its fixture.
Typing in a component
While a placed component has the keys (F6) and is typing into a textarea, a form field or a free question: characters, Backspace and Esc are its own, Enter sends (the prompt's submit chord), and Shift+Enter or Alt+Enter is a newline. The first Esc leaves typing; the next one dismisses the surface.
Limits
- A rebuild per upgrade. A Rust client needs a Rust toolchain, an Ink one Node 20.19 and pnpm, and either a rebuild on each Orrery upgrade you take: the library is compiled (or bundled) in.
- Your component draws only in your client. A Gantt chart in ratatui does not draw in the ADE, Cloud web or Ink, nor one in Ink in ratatui; they draw the fallback.
- Composed when built, never loaded. No component is loaded at runtime; adding one means rebuilding.
- Full screen maps styles. In the full screen a named colour is drawn as the theme's (red, green, yellow, blue, magenta, cyan, gray), an RGB colour as given, with bold, dim, italic and underline (Ink adds strikethrough and inverse and keeps 256 colours as RGB); mono drops the colours. An Ink component is rendered there once per kind, width and props by a throwaway Ink instance, and is on the first frame; a width change (a resize, a pane opening) draws the fallback for one frame, then the component.
- Ink redraws on props. An Ink component reading anything but its props and width (a clock, a file) is not redrawn in the full screen until its props change.
- Nested width. In the full screen a component inside a placed structure is drawn at its pane's width (or the thread's); inside a
boxor arowit is narrower than that, so its lines wrap. Draw it at the top of the structure, or keep lines short. - One set per process. Components are registered once, for the client's life.
- Library version.
orrery-client-ratatuifollows semver from 0.1.0.Client,Builder,Componentandcomponents::draware its stable API; the rest of the crate is public fororreryitself and may change. - Ink library version.
@getorrery/client-inkfollows semver from 0.1.0.createClient,ComponentProps,ClientandClientOptionsare its stable API; the rest of the package may change. - Not on crates.io or npm yet. The ratatui scaffold takes the library from the Orrery repository by version; point it at a checkout with
path = ...to build offline. The Ink scaffold asks for@getorrery/client-inkby version, whichpnpm installcannot find until it is published: link a checkout (pnpm add link:<checkout>/harness/clients/ink), built once withpnpm installat its root. - Ink's bundle has no picture addon. The scaffold's build leaves
@resvg/resvg-jsout: the mark draws as pixels, not a picture. - One id, one declaration. Two installed packages cannot declare the same element id:
acme-clientdeclaresacme.gantt,acme-client-inkacme-ink.gantt, so both install. A sender picks the client by the id it sends; the other draws the fallback.