These libraries are experimental. APIs may change without notice. Generated from source with koruc 0.1.7 on 8/19/2026.
Vaxis
@korulang/vaxis@0.0.1Terminal UI library for Koru wrapping libvaxis with phantom type obligations
vaxis/index.kz · 31 tors
Koru Vaxis Library — Reactive TUI wrapper, effect-branch shape · 24 more lines
Koru Vaxis Library — Reactive TUI wrapper, effect-branch shape
`run` is a single long-running proc that owns the terminal for its
entire body: init → loop emitting effects → deinit. Consumers handle
terminal events as effect branches; they don't write an outer loop.
USAGE:
~import koru/vaxis
~koru/vaxis:run(title: "Hello")
! ready |> koru/vaxis:write-at(x: 2, y: 2, text: "Hello! Press q to quit.") |> koru/vaxis:render()
! key k when k.ch == 'q' |> koru/vaxis:quit()
! resize _ |> koru/vaxis:render()
| done _ |> _
| err e |> _
MODULE-SCOPE REACH-BACK ($mod.):
`run` is an effect-branch event, so its |zig body is SPLICED into the
consumer's frame (cut-1 inlining) — where this module's bare names don't
exist. Every reference to this module's own state (the vars/consts below)
from inside a proc body uses the sanctioned `$mod.` spelling so the
emitter rewrites it to the right namespace on the splice path and strips
it to bare names on the standalone-handler path. `std` is exempt (it
auto-rewrites to @import("std")). See koru pin 400_155 for the contract.
run
index.kz:156~pub tor run { title: string }
! ?draw koru/vaxis:Window
! ?key koru/vaxis:KeyData
! ?resize koru/vaxis:SizeData
! ?focus-in
! ?focus-out
! ?tick i64
| ?done
| err stringquit
index.kz:332// CONSUMER-FACING UTILITIES
//
// Request quit. Sets the flag the `run` loop checks each iteration.
~pub tor quit {}render
index.kz:339// Render the current frame to the terminal.
~pub tor render {}clear
index.kz:348// Clear the screen.
~pub tor clear {}write-at
index.kz:361// Write text at position.
~pub tor write-at { x: u16, y: u16, text: string }style
index.kz:380// Construct a reusable Style value. Bind it once (`: style`) and it puns into
// every downstream write-styled's `style:` param; or pass it explicitly. No
// param defaults yet, so all attrs are named here — partial specs ride the
// struct defaults via an inline `{ … }` record literal instead.
~pub tor style {
bold: bool,
dim: bool,
italic: bool,
strikethrough: bool
} -> koru/vaxis:Stylewrite-styled
index.kz:389// Write text at position WITH a style. `style` is a reusable Style value —
// bind it once, apply to many writes. The koru Style maps onto libvaxis
// Cell.Style (bools + fg/bg RGB; 0 color = .default).
~pub tor write-styled { x: u16, y: u16, text: string, style: koru/vaxis:Style }box
index.kz:411// Open a child window (a positioned, sized region) of the root and return it as
// a value. Bind it (`: win`) and downstream write-in calls pun `win` to paint
// INTO it with box-relative coords. `border` draws a frame around the region.
~pub tor box { x: u16, y: u16, w: u16, h: u16, border: bool } -> koru/vaxis:Windowsub
index.kz:430// Slice a sub-window out of a GIVEN parent window (not the root) — the primitive
// a layout container uses to carve its own region into child slots. Offsets are
// parent-relative and cumulative (a sub of a sub composes), so moving the parent
// moves the whole subtree. `w`/`h` of 0 mean FILL to the parent's edge (libvaxis
// takes ?u16, null = fill) — a StackPanel row is `sub(parent, x:0, y:i, w:0, h:1)`:
// full width, one row tall, at row i.
~pub tor sub {
parent: koru/vaxis:Window,
x: u16,
y: u16,
w: u16,
h: u16
} -> koru/vaxis:Windowsub-from-bottom
index.kz:447// Dock a child of height `h` at the BOTTOM edge of the parent, `up` rows up from
// that edge (up = this child's height plus everything already docked below it).
// The parent's RUNTIME height is read here — bottom-pinning is the one thing that
// cannot be comptime-folded, because the terminal height changes on resize. This
// is precisely the primitive StackPanel never needed: fixed top rows never touch
// the far edge, so they stay pure arithmetic; the bottom edge does not.
~pub tor sub-from-bottom {
parent: koru/vaxis:Window,
up: u16,
h: u16
} -> koru/vaxis:Windowsub-fill
index.kz:464// The FILL slot: the middle band between the top-docked rows (`top`) and the
// bottom-docked rows (`bottom`). Height = parent.height - top - bottom, resolved
// at runtime so the fill grows and shrinks with the terminal — classic Dock's
// last-child-fill, expressed as a koru Window value.
~pub tor sub-fill {
parent: koru/vaxis:Window,
top: u16,
bottom: u16
} -> koru/vaxis:Windowstack
index.kz:484~pub tor stack { parent: koru/vaxis:Window } -> koru/vaxis:Stackstack-row
index.kz:493// Next 1-line full-width slice of the stack's parent. Advances the frame cursor.
// Layout stays the container's job — the child paints into the returned window.
~pub tor stack-row { stack: koru/vaxis:Stack } -> koru/vaxis:Windowwrite-in
index.kz:508// Write text INTO a window, at coords relative to that window's top-left. Moving
// the window moves everything written into it — the retained-layout payoff.
~pub tor write-in { win: koru/vaxis:Window, x: u16, y: u16, text: string }progress-bar
index.kz:524// Charm-class progress meter — fill math + per-cell color blend live HERE
// (COMPONENT wall 3). Defaults match bubbles/progress: ▌ filled (half-block
// dual-color blend), ░ empty, purple→pink (#5A56E0→#EE6FF8), empty #606060,
// trailing " N%". Width = win.width (stack/dock rows hand a slice). Component
// markup tags `<progress-bar …/>` resolve to this event (builtin path), so the
// widget boundary owns the meter.
~pub tor progress-bar { win: koru/vaxis:Window, value: i64, max: i64 }spinner
index.kz:610// Charm-class spinner — glyph cycling + Style fg + MiniDot FPS live HERE
// (COMPONENT spinner). Defaults match bubbles/spinner MiniDot + Charm purple
// #5A56E0. `ms` is monotonic tick payload; widget owns frame index + period.
// Markup tag `<spinner …/>` resolves via builtin wa_qual path.
~pub tor spinner { win: koru/vaxis:Window, ms: i64 }pulse
index.kz:646// FRAGMENT SHADER (rung one) — pulse
//
// Terminal fragment = one cell. `pulse` is the first fill: for each cell in
// `win`, uv = (col/w, row/h), time from `t` (monotonic ms), out = glyph + bg.
// Same thesis as std/kernel (relationships over elements; compiler/host owns
// the loop) — here the domain is the window's cell grid, not particles.
//
// Under UI by layout: dock `<pulse dock="fill" t={{ t:d }}/>` under a topbar —
// separate Window slots, chrome never fights the field. Stack later = paint
// order. Prefer `koru/vaxis:shader(name) { glyph=/fg=/bg= }` (C1) for authored fills;
// `pulse` stays the hard-coded builtin twin.
//
// Markup `<pulse …/>` resolves via the builtin wa_qual path.
~pub tor pulse { win: koru/vaxis:Window, t: i64 }timer
index.kz:696// Charm-class timer — a COUNTDOWN clock (COMPONENT timer). Ports bubbles/timer:
// the widget paints the remaining duration string (Go time.Duration.String() —
// "1m2s" / "500ms" / "0s" taste) and OWNS all chrome; the host store owns the
// remaining ms + running flag and decrements on `! tick` (same host-owned model
// as spinner). Purple #5A56E0 running, dim #606060 paused; when remaining ≤ 0 the
// timer is TIMED OUT → "0s" painted in Charm pink #EE6FF8. `running` is 0/1 (the
// store rail is i64, same as paginator/list). Markup `<timer …/>` via wa_qual.
~pub tor timer { win: koru/vaxis:Window, remaining_ms: i64, running: i64 }stopwatch
index.kz:761// Charm-class stopwatch — a COUNT-UP clock (COMPONENT stopwatch). Ports
// bubbles/stopwatch: the widget paints the elapsed duration string (Go
// time.Duration.String() taste) and OWNS its chrome; the host store owns the
// elapsed ms + running flag and increments on `! tick`, reset zeroes it. Purple
// #5A56E0 running, dim #606060 paused. `running` is 0/1 (i64 store rail). Markup
// `<stopwatch …/>` resolves via the builtin wa_qual path.
~pub tor stopwatch { win: koru/vaxis:Window, elapsed_ms: i64, running: i64 }text-input
index.kz:821// Charm-class textinput — prompt + value/placeholder + blink block cursor +
// horizontal scroll live HERE (COMPONENT textinput). Defaults match Bubbles:
// Prompt "> ", placeholder when empty, focused purple prompt, dim placeholder,
// blink ~530ms from `ms` tick. Cursor always at end (append-char/pop-char
// path — no mid-buffer caret yet). Markup `<text-input …/>` via wa_qual.
~pub tor text-input { win: koru/vaxis:Window, value: string, ms: i64 }list
index.kz:949// Charm-class simple list — title + paginated items + selection cursor live
// HERE (COMPONENT list). Matches bubbles/list simple delegate: bold purple
// title (marginLeft 2), each row "<N>. <title>" with paddingLeft 4 (selected
// swaps to paddingLeft 2 + "> " prefix, so the title column stays at x=4 on
// every row). Selected row in Charm pink + bold; unselected in soft contrast.
// Pagination footer at bottom: one dot per page (Charm paginator), current
// page filled ● in pink, others dim ○. Page-aligned scroll keeps the selection
// on the visible page. State (items newlstring + selected index) lives in a
// host store and is fed as props — the widget OWNS paint/chrome, NOT the key
// pipeline. Markup tag `<list …/>` resolves via builtin wa_qual path.
~pub tor list {
win: koru/vaxis:Window,
title: string,
items: string,
selected: i64
}viewport
index.kz:1125// Charm-class viewport — a scrollable content window (COMPONENT viewport).
// Matches bubbles/viewport's job: content taller than the window, paint ONLY
// the visible slice starting at `offset` (YOffset). Content is a newline-
// delimited string (same payload path as list/text-input); each visible line
// paints into its own 1-row child window so long lines hard-clip at the right
// edge instead of wrapping (bubbles' no-soft-wrap default). The widget is
// chromeless like bubbles' — content renders in the terminal default style;
// scroll position lives in the HOST store and arrives as a prop. Offset is
// clamped to [0, count-1]: the last line can scroll to the top (less/vim
// max-scroll). Bubbles clamps at count-height instead, but the host cannot
// see the window height to mirror that guard — clamping the paint at
// count-height here would leave a key-press dead zone where the host offset
// moves and nothing changes on screen. Floated, not routed around.
// Markup tag `<viewport …/>` resolves via builtin wa_qual path.
~pub tor viewport { win: koru/vaxis:Window, content: string, offset: i64 }paginator
index.kz:1185// Charm-class paginator — pagination STATUS CHROME (COMPONENT paginator).
// Charm's own note: this widget does NOT render page content; it paints the
// page indicator and does the page math. Two display modes matching bubbles/
// paginator's Type:
// Dots (default) — one ● per page, the current page's dot pink #EE6FF8 +
// bold (Charm ActiveDot), the rest dim ○ (InactiveDot).
// Same taste as list's embedded footer; spaced "● ○ ○".
// Arabic ("kind=arabic") — "N/M" (1-based current / total), current number
// pink + bold, "/M" dim (Charm defaults to "%d/%d").
// Both modes center on the widget's row. Page state (current page + total
// pages) lives in the HOST store and arrives as props — the widget OWNS the
// paint, NOT the `! key` pipeline (host keys mutate `page`). The row is
// usually 1 tall (a status row); it paints at y=0 of whatever window it is
// handed. Markup tag `<paginator …/>` resolves via builtin wa_qual path.
~pub tor paginator { win: koru/vaxis:Window, page: i64, total: i64, kind: string }help
index.kz:1265// Charm-class help — keybinding hints (COMPONENT help). Matches bubbles/help:
// SHORT mode (mode=0, default) — a single line "key desc • key desc • …",
// muted keys (#626262), softer descs (#4A4A4A), dim " • " separators
// (#3C3C3C); truncated with a trailing "…" when the content is wider than
// the window (bubbles' width-aware ShortHelpView + ansi.Truncate).
// FULL mode (mode!=0) — one "key desc" row per binding, keys right-padded
// into an aligned column (bubbles' single-column FullHelpView).
// Both anchor to the BOTTOM of the given window (help sits at the screen bottom
// in a real app). Bindings arrive as a host-owned "key:desc\nkey:desc" payload
// (the list/viewport string path — a real structured []Binding with separate
// short/full lists is the deferred hole); `mode` is a host store int the app's
// `?` key toggles — the widget OWNS the paint, NOT the `! key` pipeline.
// Markup tag `<help …/>` resolves via builtin wa_qual path.
~pub tor help { win: koru/vaxis:Window, bindings: string, mode: i64 }table
index.kz:1450// Charm-class table — columns, header row, selectable rows (COMPONENT table).
// Matches bubbles/table: a header row (bold) over data rows; the SELECTED row
// paints in Charm pink #EE6FF8 + bold (bubbles DefaultStyles Selected = bold +
// lipgloss "212"); a dim border line under the header (bubbles' Header
// BorderBottom). Columns carry fixed widths; cell text wider than its column
// truncates with a trailing "…" (bubbles' ansi.Truncate). Cells pad 1 col on
// each side (bubbles' Cell/Header Padding(0,1)).
//
// Payload (the list/viewport/help string path — real typed []Column/[]Row is
// the deferred hole):
// headers — "Rank|City|Country|Population" — `|`-separated column titles
// widths — "4|16|16|10" — `|`-separated column widths
// rows — one row per line, `|`-separated fields (bubbles FromValues)
// Selection state (cursor index) lives in the HOST store and arrives as a
// prop; host keys (j/k/g/G) mutate it — the widget OWNS the paint, NOT the
// `! key` pipeline. Scroll: page-aligned visible window keeps the selected
// row on screen (same stateless shape as list — bubbles' offset-tracking
// viewport needs widget-owned scroll state the prop rail can't hold yet;
// floated, not routed around).
// Markup tag `<table …/>` resolves via builtin wa_qual path.
~pub tor table {
win: koru/vaxis:Window,
headers: string,
widths: string,
rows: string,
selected: i64
}filepicker
index.kz:1649// Charm-class filepicker — a directory browser (COMPONENT filepicker). Matches
// bubbles/filepicker's job: a current-path header over a listing of entries, a
// ">" cursor (Charm pink #EE6FF8) on the selected row, DIRECTORIES in Charm
// purple #5A56E0 (bubbles Directory style, lipgloss "99"), files in a soft
// contrast. The selected row wins: bold pink cursor + name (bubbles Cursor +
// Selected). Directories carry a trailing "/" (ls -F taste) so kind reads even
// where color is unavailable. Page-aligned scroll keeps the cursor visible
// (same stateless shape as list/table — bubbles' offset viewport needs
// widget-owned scroll state the prop rail can't hold yet; floated, not routed
// around).
//
// Payload (the list/table string path — real typed []DirEntry is the deferred
// hole):
// path — current directory label, painted muted+bold in the header
// entries — one entry per line, "<kind>\t<name>" (bubbles ReadDir output):
// kind D = directory, F = file (tab-separated; missing tab = file)
// selected — cursor index; lives in the HOST store and arrives as a prop.
// Host keys (j/k move, g/G ends, l/enter open, h back) mutate the store — the
// widget OWNS the paint, NOT the `! key` pipeline. For navigation the host can
// keep N fixed entry strings and flip an i64 dir index (a valid Bridge — real
// mutable-string ReadDir is a stretch hole).
// Markup tag `<filepicker …/>` resolves via builtin wa_qual path.
~pub tor filepicker {
win: koru/vaxis:Window,
path: string,
entries: string,
selected: i64
}textarea
index.kz:1766// Charm-class textarea — multi-line text input (COMPONENT textarea). Matches
// bubbles/textarea's job: the value fills a TALL window, one line per row,
// behind a "┃ " prompt gutter painted down the full input height (bubbles'
// default Prompt on every row). Placeholder when empty (muted, blink cursor
// over the first char — text-input's placeholderView shape). Blink block
// cursor (~530ms from `ms`) sits at the END of the value (append-char/
// pop-char path — no mid-buffer caret yet, text-input's hole). Vertical
// follow: when the value has more lines than the window, the visible slice
// bottom-anchors so the cursor line stays on screen (the widget CAN see its
// own height here — no page-aligned compromise needed). Long lines hard-clip
// at the right edge (viewport's honest no-soft-wrap), except the CURSOR line
// which horizontally scrolls like text-input so the cursor stays visible.
// The value splits on '\n' OR '\r': Enter reaches the host as raw ch 13 and
// is appended verbatim through the proven append-char path — both bytes mean
// a hard line break. Host store owns the buffer; the widget OWNS paint/
// chrome, NOT the `! key` pipeline.
// Markup tag `<textarea …/>` resolves via builtin wa_qual path.
~pub tor textarea { win: koru/vaxis:Window, value: string, ms: i64 }// VIEW — markup → flow (Svelte-for-terminal). N sibling <text> elements.
//
// `view { <text>hi</text> <text>bye</text> }` lowers AT COMPILE TIME to a CHAIN
// of real koru write-at invocations — it COMPOSES OUT OF the existing `write-at`
// event, it does not reimplement vaxis internals. Every other koru transform
// (fmt.blk, ln, sqlite3:query) generates an inline Zig body; `view` instead
// generates real event invocations and hands them back for normal codegen.
// That's the koru-native path that lets markup lower to composable flow.
//
// Each `<text>…</text>` becomes `write-at(x: 0, y: <row>, text: "…")`, stacked
// one per row, wired as a linear `|>` pipeline (a nested continuation chain —
// the exact shape the parser builds for `a |> b |> c`). The site's own trailing
// continuations (a `|> render()`, branch handlers) ride on the LAST link.
// Nesting, {{ }} interpolation, and <box> come next — each an increment on this.
~[comptime|transform] pub tor view {
source: Source,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
allocator: std.mem.Allocator
} -> SiteResult~[keyword|comptime|transform|pre] pub tor default {
expr: Expression,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
allocator: std.mem.Allocator
} -> SiteResult~[comptime|transform|pre] pub tor shader {
expr: Expression,
source: Source,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
allocator: std.mem.Allocator
} -> SiteResult// COMPONENT — a named, reusable view that SYNTHESIZES a koru event.
//
// `component(greet) { <text>{{ name:s }}</text> }` is a DECLARATION. It doesn't
// rewrite a flow — it synthesizes a whole koru event, exactly the way `store`
// synthesizes its insert/query family (store.kz): the `{{ name:s }}` placeholders
// BECOME the event's typed input shape, and the markup becomes an implementing
// flow that paints it. The declaration is consumed; a new `greet { name: string }`
// event + its impl are appended.
//
// The payoff: `greet` is then a NORMAL koru event. You call it, branch on it with
// koru `if`, loop it with `for`, nest it — composition is koru flow, not template
// syntax. This proof does string props (`:s`, zero-alloc passthrough into write-at);
// typed/formatted props (`:d`, `:b`) ride the fmt rail next.
~[comptime|transform|pre] pub tor component {
expr: Expression,
source: Source,
invocation: *const Invocation,
item: *const Item,
program: *const Program,
allocator: std.mem.Allocator
} -> SiteResult