README.md

cat README.md
/
# erza

`erza` is a terminal-native UI language and runtime for docs, tools, and small
product surfaces.

If you are an AI agent, do not use this file as your primary guide. Read
`SKILLS.md` instead:

`https://erza.ryangerardwilson.com/skills`

The hosted docs shell also includes a worked example at:

`https://erza.ryangerardwilson.com/example`

The repo also contains that same guide directly at [SKILLS.md](./SKILLS.md).

## What erza Is

`erza` moves small interactive surfaces out of the browser and into the
terminal.

The project currently includes:

- the `.erza` language surface
- the Python template/parser/runtime prototype for local in-process apps
- a language-agnostic remote protocol that backends like Node.js can implement
- local and remote app support
- a reusable Python chat TUI surface for conversation lists, boxed messages,
  file modals, and persistent composers
- example apps
- `koinonia`, a larger social-app prototype built in `erza`

The long-term direction is `erzanet`: apps and documents that can be opened as
`erza example.com` instead of “open a browser tab and hunt around.”

## Why erza

Use `erza` when the browser is the wrong container.

- docs should open without tab sprawl, cookie banners, or popover junk
- a tool should feel local, keyboard-first, and terminal-native
- a workflow should survive slow links, large monitors, and minimal machines
- a remote product surface should be reachable as `erza example.com`

## Current Product Model

The current design direction is intentionally opinionated.

- A typical app should be a single `index.erza` file.
- Top-level `<Section>` blocks act like tabs.
- Selecting a tab changes the active page within the same screen.
- Tabs can be conditional, so login state can change which tabs exist.
- Tabs can declare `tab-order` and `default-tab`.
- Forms are modal-only.
- A modal is either:
  - a single-form modal
  - a view modal whose actions may only open form-only modals
- `ButtonRow` is the standard action surface inside pages and forms.
- Direct-action tabs are allowed for flows like `Logout`.
- Splash screens and splash animations are first-class.
- Chat-style apps can use the `erza.chat` runtime API while declarative chat
  syntax settles.

In other words: `erza` apps are moving closer to a terminal-native React-like
single-surface model than to a folder of loosely connected pages.

## Install

```bash
curl -fsSL https://raw.githubusercontent.com/ryangerardwilson/erza/main/app/install.sh | bash
```

If `~/.local/bin` is not already on your `PATH`, add it once and reload:

```bash
export PATH="$HOME/.local/bin:$PATH"
source ~/.bashrc
```

## Quick Start

Open the hosted docs:

```bash
erza run erza.ryangerardwilson.com
```

Open local examples from a checkout:

```bash
python app/main.py run app/examples/docs
python app/main.py run app/examples/forms
python app/main.py run app/examples/animation
python app/main.py run app/examples/tasks/app.erza
python app/main.py run app/examples/greetings
```

Open the `koinonia` prototype locally:

```bash
python app/main.py run koinonia
```

Canonical CLI surface:

```bash
python app/main.py -h
python app/main.py -v
python app/main.py run <source> [--backend <path>] [-u <username> -p <password>]
```

`source` may be:

- a single `.erza` file
- a directory, in which case `erza` resolves `index.erza`
- an `http(s)` URL
- a bare domain like `erza.ryangerardwilson.com`
- omitted entirely, in which case `erza run` uses the current directory

`erza` automatically loads `backend.py` from the same directory as the entry
file unless `--backend` is provided explicitly. That local auto-loading is
Python-only today. Non-Python backends, including Node.js, fit through the
remote protocol instead of a local `backend.py` hook.

For remote apps that implement standardized auth, you can sign in before the
first render:

```bash
python app/main.py run koinonia-singapore.onrender.com -u ryan -p secret
```

## A Minimal App

```erza
<Screen title="Town Square">
  <? status = backend("ui.status") ?>

  <Section title="Feed" tab-order="1" default-tab="true">
    <Header>Town Square</Header>
    <Text><?= status ?></Text>
    <ButtonRow align="right">
      <Action on:press="ui.open_modal" modal:id="new-post">New post</Action>
    </ButtonRow>
  </Section>

  <Section title="Profile" tab-order="0">
    <Header>@ryan</Header>
    <Text>No description set yet.</Text>
  </Section>

  <Modal id="new-post" title="New Post">
    <Form action="/posts">
      <Input name="body" type="text" label="Post" required="mandatory" />
      <ButtonRow align="right">
        <Submit>Publish</Submit>
      </ButtonRow>
    </Form>
  </Modal>
</Screen>
```

Matching local `backend.py` in Python:

```python
from erza.backend import handler, redirect, route, session


@handler("ui.status")
def ui_status() -> str:
    return session().get("status", "Welcome to erza.")


@route("/posts")
def create_post(body: str = ""):
    session()["status"] = f"Posted: {body.strip()}"
    return redirect("index.erza")
```

Equivalent remote backend example in Node.js using the protocol:

```js
import express from "express";

const app = express();
app.use(express.json());

let status = "Welcome to erza.";

app.get("/.well-known/erza", (req, res) => {
  res.type("application/erza").send(`
<Screen title="Town Square">
  <Section title="Feed" default-tab="true">
    <Header>Town Square</Header>
    <Text>${status}</Text>
    <ButtonRow align="right">
      <Action on:press="ui.open_modal" modal:id="new-post">New post</Action>
    </ButtonRow>
  </Section>

  <Modal id="new-post" title="New Post">
    <Form action="/posts">
      <Input name="body" type="text" label="Post" required="mandatory" />
      <ButtonRow align="right">
        <Submit>Publish</Submit>
      </ButtonRow>
    </Form>
  </Modal>
</Screen>`);
});

app.post("/posts", (req, res) => {
  status = `Posted: ${String(req.body.body || "").trim()}`;
  res.json({ type: "redirect", href: "index.erza" });
});

app.post("/.well-known/erza/action", (req, res) => {
  res.json({ type: "refresh" });
});

app.listen(3000);
```

## Local and Remote Model

Locally, `erza` can run a file or directory with an optional Python backend.
Remotely, `erza` can open a host directly by domain or URL, which is where
Node.js and other backend languages fit cleanly today.

The current remote protocol is:

- `GET /.well-known/erza?path=/requested/path`
- `POST /.well-known/erza/action?path=/requested/path`
- `POST /.well-known/erza/auth`

Remote form submits post JSON directly to the form `action` URL.

The key design point is that `erza` is frontend-first, but not frontend-only:
there is a small backend contract for reads, writes, auth, and action dispatch.

## Language Surface

Supported root-level structure today:

- `<Screen title="...">`
- zero or one `<Splash duration-ms="...">`
- top-level `<Section>` tabs
- top-level `<Modal>` overlays

Common components:

- `<Section title="...">`
- `<Header>`
- `<Text>`
- `<AsciiArt>`
- `<Link href="...">`
- `<Action on:press="handler.name">`
- `<Button on:press="handler.name">`
- `<ButtonRow align="left|center|right">`
- `<Modal id="..." title="...">`
- `<Form action="/path">`
- `<Input name="field" type="text|password|ascii-art|hidden">`
- `<Submit>`
- `<AsciiAnimation fps="...">`
- `<Splash duration-ms="...">`
- `<SplashAnimation fps="...">`
- `<Column gap="...">`
- `<Row gap="...">`
- HTML comments `<!-- like this -->` are valid source comments and are ignored by the compiler/runtime.

## Chat UI Runtime

Apps that already own their data or API layer can use `erza.chat` directly as a
UI/UX abstraction layer without rewriting their command surface as a `.erza`
file first.

```python
from erza.chat import ChatCallbacks, ChatConversation, ChatMessage, run_chat_app


callbacks = ChatCallbacks(
    load_conversations=lambda: [
        ChatConversation("D1", "maanas", "2026-04-27 10:00", kind="dm"),
    ],
    load_messages=lambda conversation: [
        ChatMessage("D1:1", "Maanas", "2026-04-27 10:00", "hello"),
    ],
    send_message=lambda conversation, text: None,
    mark_read=lambda conversation, messages: None,
    mark_all_read=lambda conversations: None,
    open_file=lambda conversation, message, file: "/tmp/file.txt",
)

run_chat_app(callbacks, title="slack tui")
```

The chat runtime owns the terminal interaction: conversation list, boxed message
transcript, default normal mode, `i`-to-insert composition, Ctrl-N/Ctrl-P
message movement, `g`/`gg`/`G` jumps, `,mra` mark-all-read from the
conversation list or an open conversation, fixed-height file picker, shortcuts modal,
the Erza matrix loading overlay for slow callbacks, and file opening. PDFs
default to `zathura`, images default to `swayimg`, and unknown/text files fall
back through `$VISUAL`, `$EDITOR`, then `vim`. The app owns API calls, tokens,
download paths, and message data.

See [`CHAT_SURFACES_SPEC.md`](./CHAT_SURFACES_SPEC.md) for the detailed chat
contract and Slack adapter direction.

## Runtime Controls

Global movement:

- `h` / left: previous tab or previous button in a row
- `l` / right: next tab or next button in a row
- `j` / down: move down inside a page
- `k` / up: move up inside a page
- arrow keys work as alternatives to `hjkl`
- `Enter`: activate current target or enter the active page
- `Esc`: leave page/edit mode or close modal focus back toward the page
- `Backspace`: go back
- `gg`: jump to the first top-level section
- `G`: jump to the last top-level section
- `Ctrl+D` / `Ctrl+U`: half-page movement
- `?`: shortcuts/help

Text inputs and chat composers share the Erza input editor:

- `Ctrl+A` / `Ctrl+E`: start / end of the input
- `Ctrl+B` / `Ctrl+F`: backward / forward one character
- `Alt+B` / `Alt+F`: backward / forward one word
- `Ctrl+W` / `Ctrl+H`: delete previous word / character
- `Ctrl+D` / `Ctrl+K` / `Ctrl+U`: delete next character / to end / full input

Chat surfaces add:

- conversation list `j`/`k` and `l` to open
- normal mode by default in conversations
- normal-mode `i` to enter insert mode
- insert-mode Enter to send and Esc to return to normal mode
- insert mode uses the same Erza input editor as form fields
- normal-mode Ctrl-N/Ctrl-P for message movement
- normal-mode `,mra` to mark all loaded conversations read from the conversation list or an open conversation when the app provides that callback
- normal-mode `l` on `<<<X Files>>>` to open the fixed-height file picker

## Where to Go Next

Humans should continue from:

- [SKILLS.md](./SKILLS.md) if you want the agent-style operating manual
- `app/examples/` for small runnable examples
- `koinonia/` for a larger end-to-end app
- `app/tests/` if you want to see what the runtime actually guarantees

## Repo Layout

- `README.md`: human-facing introduction and overview
- `SKILLS.md`: AI-agent guide to building with `erza`
- `AGENTS.md`: repo guardrails for coding agents
- `PRODUCT_SPEC.md`: current product direction
- `FORMS_SPEC.md`: older form-focused notes
- `CHAT_SURFACES_SPEC.md`: chat runtime and Slack adapter direction
- `app/main.py`: canonical CLI entrypoint
- `app/install.sh`: installer and upgrade path
- `app/src/erza/template.py`: template engine
- `app/src/erza/parser.py`: markup-to-component compiler
- `app/src/erza/runtime.py`: curses runtime and renderer
- `app/src/erza/chat.py`: reusable conversation/chat TUI runtime
- `app/src/erza/backend.py`: backend bridge and route/session primitives
- `app/src/erza/remote.py`: remote fetch and remote app client
- `app/examples/`: runnable examples
- `app/tests/`: unit tests
- `koinonia/`: larger end-to-end example app
- `docs_website/`: browser shell that serves `README.md`, `SKILLS.md`, and `EXAMPLE.md`

## Development

Run the app test suite:

```bash
cd app/tests
python -m unittest
```

Run the Koinonia tests:

```bash
cd koinonia
python -m unittest
```

Run the browser docs locally:

```bash
cd docs_website
npm install
npm run dev
```

## Status

This is still an intentionally small, opinionated prototype.

What the current repo already proves:

- `.erza` can serve as a readable TUI authoring language
- one-file app surfaces are practical
- tabbed section navigation works well in the terminal
- modal-only forms keep write flows cleaner
- backend reads and writes can share the same runtime surface
- remote apps can be opened directly by domain
- animated splash screens and ASCII motion can be first-class terminal UI
- chat-style apps can reuse the Erza conversation/message/composer runtime

What is still fluid:

- the exact long-term language surface
- the remote transport and capability model for `erzanet`
- how much browser fallback should exist beside true terminal-native hosts
- how much more structure should be added to the app/layout vocabulary