Getting Started¶
This is the tutorial. You will build one app and grow it step by step: a counter, then a timer that pauses, then keys and mouse, then a pixel view, then the same program running inline in your scrollback. Along the way you will meet every piece of maya's single program shape and understand every line of it.
Work through it top to bottom with a terminal open. It takes about 20 minutes and leaves you able to read any maya example and write your own.
1. Install¶
What you need¶
- A C++26 compiler — GCC 15+ or Clang 19+. The app layer runs on jaal, which needs C++26 (structured-binding packs); an older compiler will not build it.
- CMake 3.28+.
- A Unix-like OS — Linux or macOS. maya talks to the terminal through raw
mode,
ioctl(TIOCGWINSZ), and POSIX signals. - The jaal submodule. jaal is the runtime maya draws for; it lives in
third_party/jaal. There is nothing else — no ncurses, no Boost, no package manager step.
Build the library and examples¶
git clone --recursive <repo-url> maya
cd maya
git submodule update --init third_party/jaal # if you cloned without --recursive
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
-B build puts all generated files in build/ (out-of-source, so your tree
stays clean). Every file in examples/ becomes a maya_<name> binary. Run a
few to confirm it works:
./build/maya_counter # the smallest maya app — the one you'll write below
./build/maya_mandelbrot # a pixel view, animated with a timer
./build/maya_dashboard # a multi-panel system dashboard
Press q to quit any of them.
Build options¶
| Option | Default | What it does |
|---|---|---|
MAYA_BUILD_APP |
ON |
Build maya::app (maya on jaal). Needs C++26 and the submodule. |
MAYA_BUILD_EXAMPLES |
ON |
Build the example programs (needs maya::app). |
MAYA_BUILD_TESTS |
top-level only | Build + register the test suite (ctest). |
MAYA_NATIVE_TUNING |
ON |
Bake the host CPU in (-march=native). Turn OFF for portable release builds — a native binary will SIGILL on a chip missing the build host's instructions. |
Use maya in your own project¶
Vendor maya as a subdirectory and link maya::app:
add_subdirectory(vendor/maya)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE maya::app) # maya + jaal + the C++26 flag
maya::app carries the include paths, jaal, and the language-standard flag.
(If you only ever call print() and never run a program, the plain
maya::maya target is enough.)
Or install it and find it:
2. The counter¶
Copy examples/counter.cpp into your project as main.cpp. Here it is:
#include <maya/host/run.hpp> // (1) maya + the jaal host
#include <string>
#include <variant>
using namespace maya; // (2) run, Element, KeyEvent, keymap, ...
using namespace maya::dsl; // t<>, text, v, h, Bold, border_, ...
struct Counter {
struct Model { int count = 0; }; // (3)
struct Inc {}; struct Dec {}; struct Quit {}; // (4)
using Msg = std::variant<Inc, Dec, Quit>;
using Cmd = jaal::Cmd<Msg>; // (5)
static Cmd update(Model& m, Inc) { ++m.count; return {}; } // (6)
static Cmd update(Model& m, Dec) { --m.count; return {}; }
static Cmd update(Model&, Quit) { return Cmd::quit(0); }
static Element view(const Model& m) { // (7)
return v(
text("Count: " + std::to_string(m.count)) | Bold | Fg<100, 200, 255>,
t<"[+/-] change [q] quit"> | Dim
) | border_<Round> | bcol<50, 55, 70> | pad<1>;
}
static std::optional<Msg> on(const Model&, const KeyEvent& k) { // (8)
static const keymap<Msg> keys{{'+', Inc{}}, {'-', Dec{}}, {'q', Quit{}}};
return keys(k);
}
};
int main() { return run<Counter>({.title = "counter"}); } // (9)
<maya/host/run.hpp>is the one header for apps. It includes<maya/maya.hpp>(elements, DSL, styles,print) and the jaal host.- Two namespaces.
maya::dslis the vocabulary for describing UI —t<>,text(),v()/h(), style tags, and the|modifiers.mayais everything that runs it —run,Element, input events, key helpers. Modelholds all the app's state. It must be default-constructible; that default is the starting state.- Messages are one empty (or data-carrying) struct each, collected in a
std::variant. A message is a fact: "the user pressed +". - The rows.
jaal::Cmd<Msg>is the effect typeupdatereturns; its row lists the terminal effects the program uses. Use one you didn't list and it won't compile. updatehas one overload per message. It mutates the model in place and returns aCmd—{}for "nothing to do",Cmd::quit(0)to exit with status 0.viewis a pure function of the model.t<"...">is compile-time text;text(str)is runtime text (anything built from the model).v(...)stacks vertically,h(...)horizontally;|adds style (Bold,Fg<r,g,b>,Dim) and box decoration (border_<Round>,bcol<>border colour,pad<1>). The compiler rejectsbcol<>without a border.onhandles input. One overload per event kind you care about (KeyEvent,MouseEvent,PasteEvent,FocusEvent,ResizeEvent); it reads the model as it is when the event arrives and returns at most one message.keymap<Msg>maps plain keys to messages.run<Counter>(Options)opens the terminal and hands it to jaal's loop. You can check the shape up front withstatic_assert(Program<Counter>);.
Build it, run it, press + a few times, then q.
3. Adding a timer¶
Animation in maya is a subscription: Sub::every(period, Msg) sends a message
on a clock. Let's make the counter count up by itself, and pause on space.
using namespace std::chrono_literals;
struct Counter {
struct Model { int count = 0; bool paused = false; };
struct Inc {}; struct Dec {}; struct Tick {}; struct Pause {}; struct Quit {};
using Msg = std::variant<Inc, Dec, Tick, Pause, Quit>;
using Cmd = jaal::Cmd<Msg>;
using Sub = jaal::Sub<Msg>;
static Cmd update(Model& m, Inc) { ++m.count; return {}; }
static Cmd update(Model& m, Dec) { --m.count; return {}; }
static Cmd update(Model& m, Tick) { ++m.count; return {}; }
static Cmd update(Model& m, Pause) { m.paused = !m.paused; return {}; }
static Cmd update(Model&, Quit) { return Cmd::quit(0); }
static Element view(const Model& m) {
return v(
text("Count: " + std::to_string(m.count)) | Bold | Fg<100, 200, 255>,
text(m.paused ? "paused" : "running") | Dim,
t<"[+/-] change [space] pause [q] quit"> | Dim
) | border_<Round> | bcol<50, 55, 70> | pad<1>;
}
static std::optional<Msg> on(const Model&, const KeyEvent& k) {
static const keymap<Msg> keys{{'+', Inc{}}, {'-', Dec{}}, {' ', Pause{}}, {'q', Quit{}}};
return keys(k);
}
static Sub subscribe(const Model& m) {
if (m.paused) return Sub::none(); // timer dropped
return Sub::every(250ms, Tick{});
}
static bool subs_key(const Model& m) { return m.paused; }
};
subscribe() is re-evaluated as the model changes, and jaal diffs the
result: when paused flips on, the every subscription disappears and the
timer is stopped; flip it off and a fresh one starts. A paused
screen costs nothing — no wakeups, no redraws.
subs_key is an optional hint: it returns what subscribe() depends on.
jaal only re-runs subscribe() when that value changes — here, only when
paused does, not on every tick. Return true (a constant) when
subscriptions never change.
For a one-shot delay instead of a clock, return Cmd::after(500ms, Msg{})
from update.
4. Keys and mouse¶
keymap<Msg> covers plain keys, including special ones:
{SpecialKey::Up, Up{}}, {SpecialKey::Escape, Quit{}}. For anything richer
— modifiers, data-carrying messages — write it out in the on() overload with
the predicates key_is, ctrl_is, alt_is, and add an overload for each
other kind you want:
struct Click { int x, y; };
// ... add Click to Msg, then:
static std::optional<Msg> on(const Model&, const KeyEvent& k) {
static const keymap<Msg> keys{{'+', Inc{}}, {'-', Dec{}}, {SpecialKey::Escape, Quit{}}};
if (ctrl_is(k, 'c') || key_is(k, 'q')) return Quit{};
return keys(k);
}
static std::optional<Msg> on(const Model&, const MouseEvent& e) {
if (e.button == MouseButton::Left && e.kind == MouseEventKind::Press)
return Click{e.x.value, e.y.value}; // 1-based cell coordinates
return std::nullopt;
}
Mouse reporting is off by default; turn it on when you run:
run<Counter>({.title = "counter", .mouse = true}). The other kinds work the
same way: ResizeEvent{width, height}, PasteEvent{content}, FocusEvent —
add an on() overload for each one you use; the rest are dropped.
5. A pixel view¶
There is no separate canvas loop: a graphics demo is a program whose view
returns pixels(img). An Image is an RGB buffer drawn with half blocks, so it
has two pixels per terminal row.
#include <maya/element/pixels.hpp>
struct Plasma {
struct Model { int w = 0, h = 0, t = 0; };
struct Tick {}; struct Resize { int w, h; }; struct Quit {};
using Msg = std::variant<Tick, Resize, Quit>;
using Cmd = jaal::Cmd<Msg>;
using Sub = jaal::Sub<Msg>;
static Cmd update(Model& m, Tick) { ++m.t; return {}; }
static Cmd update(Model& m, Resize r) { m.w = r.w; m.h = r.h * 2; return {}; }
static Cmd update(Model&, Quit) { return Cmd::quit(0); }
static Element view(const Model& m) {
if (m.w == 0) return text(""); // no size yet
Image img(m.w, m.h);
img.fill_rows([&](int x, int y) -> Rgb { // rows fill in parallel
auto c = [](int v) { return static_cast<std::uint8_t>(v & 0xFF); };
return {c(x * 4 + m.t), c(y * 4), c(m.t * 2)};
});
return pixels(std::move(img));
}
static std::optional<Msg> on(const Model&, const ResizeEvent& r) {
return Resize{r.width.value, r.height.value};
}
static std::optional<Msg> on(const Model&, const KeyEvent& k) {
static const keymap<Msg> keys{{'q', Quit{}}};
return keys(k);
}
static Sub subscribe(const Model&) {
return Sub::every(33ms, Tick{});
}
static bool subs_key(const Model&) { return true; }
};
img(x, y) = Rgb{r, g, b} sets a single pixel; fill_rows computes the
whole image across cores. For coloured character art rather than pixels, use
Glyphs + glyphs(grid). examples/mandelbrot.cpp is this pattern at full
size.
6. Inline mode¶
By default run takes over the screen (Mode::Fullscreen: the alternate
screen buffer, restored on exit). Pass .mode = Mode::Inline and the same
program renders in the normal terminal flow instead — scrollback above is
kept, and when the program quits its last frame stays on screen:
Use inline for flow-style tools — progress cards, prompts, chat and agent UIs;
fullscreen for dashboards, editors and games. examples/inline_progress.cpp
is a complete inline program: a Sub::every tick advances a bar, and update
returns Cmd::quit(0) when it reaches 100%.
Other Options fields: fps (0 = redraw only on change; >0 redraws
continuously), hover_motion, theme, enhanced_keyboard, backend.
7. Static output with print()¶
Not everything needs a program. To draw styled output once and exit — a CLI
report, a summary after an inline run — build an element and print it:
#include <maya/maya.hpp>
using namespace maya;
using namespace maya::dsl;
int main() {
print(v(
t<"Build finished"> | Bold | Fg<80, 220, 120>,
h(t<"warnings: "> | Dim, text(std::to_string(3)) | Bold)
) | border_<Round> | pad<1>);
}
print(element) sizes to the terminal; print(element, width) uses a fixed
width, and render_to_string(element, width) returns the rendered text as a
string instead. The output is ordinary scrollback text. This path needs no runtime,
so linking maya::maya is enough.
Coming from the old API?
run(event_fn, render_fn),live(),canvas_run(),maya::Cmd/Sub,key_map, andRunConfigare gone. Each is now the program shape above:live()isMode::InlineplusSub::every;canvas_run()is a view returningpixels(img);key_mapiskeymap<Msg>insideon(const Model&, const KeyEvent&).
8. Where to go next¶
You now know the whole program shape — model, messages, update, view,
subscribe — plus timers, input, pixels, and both modes. The rest of the
guide goes deep on each axis:
- The Compile-Time DSL — every node and pipe:
t<>,v/h,map, and how type-state validation rejects impossible UIs. - Styling — colours, attributes, themes, compile-time style tags.
- Layout — flexbox: direction, grow/shrink, gap, alignment, wrapping, borders.
- Event Handling — every input kind, the
on()overloads,keymap<Msg>, and the key predicates. - Rendering Modes — fullscreen vs inline in depth,
scrollback commits,
print(). - Widget Reference — the built-in widgets.
- Examples Walkthrough — annotated tours of the example binaries you ran in §1.
Open examples/ — every program there has the shape you just learned.