# µFrame (Micronomicon)

A self-hosted web IDE and CLI for building rich terminal UIs using a declarative DSL. Compiles `.uf` source files into both plain ASCII art and styled Micron `.mu` pages for NomadNet — a decentralized communication platform running on the Reticulum mesh network.

## What It Does

Write this:
```
page "Node Status" 60
  box double "Relay Alpha-7"
    align center
    text "Reticulum Network Node"
  gauge "CPU" 62 100 28 warn=75 crit=90
  status "East Relay" online
  table "Routes"
    columns "Dest" 20 | "Hops" 6 | "Status" 10
    row "relay-east" | "2" | "@color{0f0}{alive}"
```

Get this (ASCII):
```
╔═ Relay Alpha-7 ════════════════════════════════════════════╗
║               Reticulum Network Node                       ║
╚════════════════════════════════════════════════════════════╝
CPU  ████████████████████░░░░░░░░  62%
● East Relay
┌────────────────────┬──────┬──────────┐
│Dest                │Hops  │Status    │
├────────────────────┼──────┼──────────┤
│relay-east          │2     │● alive   │
└────────────────────┴──────┴──────────┘
```

And the same content as Micron `.mu` with color tags, bold, links, and interactive form fields — ready to serve on NomadNet.

## Quick Start

### Web IDE

```bash
# Backend
cd backend
source .venv/bin/activate
PAGES_DIR=~/.nomadnetwork/storage/pages \
SOURCES_DIR=~/.micron-editor/sources \
uvicorn main:app --reload --port 8080

# Frontend (separate terminal)
cd frontend
npm run dev
```

Open http://localhost:5173 → click **New Page** → click **Examples** → pick a template.

### CLI

```bash
cd backend
source .venv/bin/activate

# Render to terminal
python -m uframe render page.uf

# Render Micron only
python -m uframe render page.uf --micron

# Compile to .mu file
python -m uframe compile page.uf --out page.mu

# Validate without output
python -m uframe check page.uf

# Compile and deploy to NomadNet
python -m uframe deploy page.uf
```

## Tech Stack

| Layer      | Technology                                     |
|------------|-------------------------------------------------|
| Backend    | Python 3.13 + FastAPI + uvicorn                |
| µFrame     | Pure Python (zero deps): parser → IR → CharGrid → emitters |
| Frontend   | React 19 + Vite + TypeScript                   |
| UI         | shadcn/ui + Tailwind CSS v4                    |
| Editor     | CodeMirror 6 (custom µFrame syntax mode)       |
| Graph      | React Flow (@xyflow/react) + dagre             |
| State      | Zustand                                        |
| Container  | Docker + Compose                               |

## Project Structure

```
micronomicon/
├── backend/
│   ├── main.py              # FastAPI app + static file serving
│   ├── converter.py          # POST /api/compile endpoint
│   ├── pages.py              # CRUD /api/pages (.uf sources + .mu publish)
│   ├── graph.py              # GET /api/graph (link parser)
│   ├── docker_utils.py       # POST /api/restart (NomadNet container)
│   └── uframe/               # µFrame engine (16 modules, ~3800 LOC)
│       ├── __init__.py        #   compile(source, width) → CompileResult
│       ├── parser.py          #   .uf DSL → IR tree (indentation-based)
│       ├── ir.py              #   30+ IR node dataclasses
│       ├── grid.py            #   CharGrid — 2D char + style buffer
│       ├── chars.py           #   Unicode tables (box-drawing, braille)
│       ├── measure.py         #   Bottom-up size computation
│       ├── layout.py          #   Top-down position assignment
│       ├── paint.py           #   IR nodes → CharGrid rendering
│       ├── borders.py         #   Junction merging post-pass
│       ├── emit_ascii.py      #   CharGrid → plain text
│       ├── emit_micron.py     #   CharGrid → Micron with style tags
│       ├── codegen.py         #   Dynamic page → executable Python script
│       ├── cli.py             #   CLI: render / compile / check / deploy
│       ├── errors.py          #   ParseError, LayoutError, CompileWarning
│       └── tests/             #   42 tests (compile, dynamic, components)
├── frontend/src/
│   ├── routes/                # DashboardView, EditorView, GraphView
│   ├── components/editor/
│   │   ├── EditorPane.tsx     #   CodeMirror 6 host
│   │   ├── PreviewPane.tsx    #   ASCII / Micron / Raw / Script tabs
│   │   ├── ToolBar.tsx        #   Save, Publish, Examples, Backlinks
│   │   ├── uframeHighlight.ts #   µFrame syntax highlighting
│   │   ├── uframeCommands.ts  #   "/" slash command palette
│   │   ├── micronRenderer.ts  #   Micron → HTML preview renderer
│   │   └── examples.ts        #   9 built-in example templates
│   ├── hooks/
│   │   ├── useCompile.ts      #   Debounced POST /api/compile
│   │   └── useUnsavedGuard.ts #   Prevent accidental navigation
│   └── stores/
│       ├── editorStore.ts     #   Zustand: source, compiled output, preview mode
│       └── pagesStore.ts      #   Zustand: page list, delete, unpublish
└── docs/
    ├── framework-design-v3.md #   Full DSL spec + rendering model
    └── dynamic-templates.md   #   Dynamic page addendum
```

## API Endpoints

| Method | Path                | Description                                          |
|--------|---------------------|------------------------------------------------------|
| GET    | /api/health         | Health check                                         |
| POST   | /api/compile        | Compile `.uf` → `{ascii, micron, script, is_dynamic}` |
| GET    | /api/pages          | List all pages with metadata                         |
| GET    | /api/pages/{name}   | Read page source                                     |
| POST   | /api/pages/{name}   | Save page — `{ source, publish }`                    |
| DELETE | /api/pages/{name}   | Delete source and/or .mu file                        |
| GET    | /api/graph          | Page link graph (nodes + edges)                      |
| POST   | /api/restart        | Restart NomadNet Docker container                    |

## Storage

```
~/.micron-editor/sources/       ← .uf source files (drafts + published)
~/.nomadnetwork/storage/pages/  ← Compiled .mu files served by NomadNet
```

- **Save Draft**: writes `.uf` to sources dir only
- **Publish (static)**: compiles `.uf` → `.mu`, writes to pages dir (chmod 644)
- **Publish (dynamic)**: compiles `.uf` → executable Python script, writes to pages dir (chmod 755)

NomadNet auto-detects the execute bit: static pages are served as-is, dynamic pages are executed and their stdout is served.

## µFrame DSL Reference

### Layout
```
page "Title" [width]                         # root (default width 64)
  box [light|heavy|double|rounded] "Title"   # bordered panel
  row [gap]                                  # horizontal layout
    col [width]                              # column in a row
  spacer [lines]                             # vertical whitespace
  pad [t] [r] [b] [l]                       # inner margin
```

### Content
```
heading [1|2|3] "Text"                       # styled heading
text "Content with @bold{inline} @color{hex}{modifiers}"
label "Key" "Value"                          # aligned key-value pair
list [bullet|dash]
  item "Entry"
link "Display text" "/dest.mu"               # clickable in Micron
divider [light|heavy|double|dash|dot]        # horizontal rule
# comment                                   # ignored in output
```

### Data Visualization
```
gauge "Label" value max width [warn=N crit=N]  # ████░░░░ bar with thresholds
sparkline "Label" "1,3,5,8,7,5" width          # ⣀⣤⣶⣿⣷⣤ braille chart
status "Label" [online|offline|degraded]        # ●○◐ colored indicators
table "Title"
  columns "Name" 20 | "Hops" 6 | "Status" 10
  row "relay" | "2" | "@color{0f0}{● alive}"
```

### Forms
```
form "name"
  field "name" [width] "placeholder"          # text input
  password "name" [width] "placeholder"       # masked input
  radio "group" "Opt A" | "Opt B" | "Opt C"  # radio buttons
  checkbox "name" "Label"                     # checkbox
  button "Label" "/action/path"               # submit link
```

### Dynamic Features

Any page using `source`, `if`, `for`, `on_submit`, or `state` becomes **dynamic**: it compiles to an executable Python script instead of static Micron. NomadNet runs the script on each request and serves its stdout.

#### Variables
```
let name = "Relay Alpha"                      # string assignment
let threshold = 75                            # numeric
let tags = "alpha","beta","gamma"             # comma-separated → list
```
Variables are substituted with `$name` in text, labels, and other content. They work in both static and dynamic pages.

#### Data Sources
```
source var_name : type "command" [timeout=N]
```
Sources fetch data **at render time** and bind results to variables:

| Type     | Description                          | Example                                                  |
|----------|--------------------------------------|----------------------------------------------------------|
| `shell`  | Run shell command, capture stdout    | `source cpu : shell "cat /proc/loadavg"`                 |
| `file`   | Read file contents as string         | `source motd : file "/etc/motd"`                         |
| `json`   | Read + parse JSON file → dict/list   | `source config : json "/etc/config.json"`                |
| `python` | Evaluate Python expression           | `source ts : python "datetime.now().strftime('%H:%M')"` |
| `http`   | HTTP request, auto-parses JSON       | `source data : http "https://api.example.com/data"`      |
| `sqlite` | SQLite query → list of dicts         | `source users : sqlite "/path/db" "SELECT * FROM users"` |
| `env`    | Read environment variable            | `source key : env "API_KEY"`                             |
| `param`  | Read URL parameter from link         | `source hash : param "hash"`                             |
| `rns`    | Query Reticulum via `rnstatus`       | `source peers : rns "peers"`                             |

**Shell** commands have a default 5-second timeout (override with `timeout=N`).

**Python** expressions have access to: `datetime` (the class, so `datetime.now()` works), `timedelta`, `secrets`, `os`, `json`. Expressions are evaluated via `eval()` — single expressions only, not statements.

```
# Python source examples
source timestamp : python "datetime.now().strftime('%H:%M:%S')"
source rand_id : python "secrets.token_hex(4)"
source cpu_sim : python "secrets.randbelow(60) + 20"
source uptime : python "str(timedelta(seconds=12345))"
source hostname : python "os.uname().nodename"
```

**HTTP** requests return parsed JSON (dict/list) or raw string. Default timeout 10s.

```
# GET request — JSON auto-parsed into dict
source todo : http "https://api.example.com/todos/1"
text "Title: $todo.title"

# POST with JSON body
source result : http "https://api.example.com/search" method=POST body='{"q":"relay"}'

# Custom headers (semicolon-separated)
source data : http "https://api.example.com/data" headers='Authorization: Bearer tok123'

# Use $var references in URL, headers, and body — resolved at runtime
source token : env "API_TOKEN"
source data : http "https://api.example.com/data" headers='Authorization: Bearer $token'
```

**Env** reads server-side environment variables. Use this for secrets — tokens never appear in `.uf` source or compiled scripts.

```
source api_key : env "API_KEY"
source db_pass : env "DB_PASSWORD"
```

**SQLite** queries return a list of dicts (or a single dict for one row). Uses Python stdlib `sqlite3`.

```
# Query returns list of dicts with column names as keys
source nodes : sqlite "/data/network.db" "SELECT name, status, hops FROM nodes"

# Iterate results
for node in $nodes
  label "$node.name" "$node.status ($node.hops hops)"

# Single row queries return a dict directly
source config : sqlite "/data/app.db" "SELECT value FROM config WHERE key='theme'"
text "Theme: $config.value"
```

#### Conditionals
```
if $cpu > 90
  text "ALERT: CPU critical"
elif $cpu > 75
  text "Warning: elevated"
else
  text "All clear"
```
Conditions are Python expressions. `$var` references resolve to the variable's value. Supports `>`, `<`, `>=`, `<=`, `==`, `!=`, `&&` (and), `||` (or).

#### Loops
```
for peer in $peers
  status "$peer.name" $peer.state
```
Iterates over lists (from JSON sources), dicts (wrapped as single-item list), or newline-delimited strings (from shell output). Access nested fields with `$item.field`.

#### Cache Control
```
cache 0                                       # never cache (re-execute every request)
cache 300                                     # cache for 5 minutes
```
Emits the `#!c=N` header that NomadNet uses to control page caching.

#### Form Submission Handling
```
on_submit "form_name"
  # Runs when the named form is submitted
  # Form field values are available as $field_name
  source results : shell "search.py '$query'"
  text "Found: $results"
```
Field values are read from `FIELD_*` environment variables set by NomadNet.

#### Persistent State
```
state "counter" "/tmp/counter.json"           # load JSON into $counter
```
Loads a JSON file into a variable. Use `_save_state(path, data)` in the generated script to persist changes.

#### Using Variables in Content
```
text "Hello, $name"                           # inline substitution
label "CPU" "$cpu_pct%"                       # in labels
gauge "CPU" $cpu_pct 100 28 warn=75 crit=90   # as gauge values
status "$peer" $state                         # in status indicators
link "View $name" "/page/detail.mu"           # in links
```

#### Generated Script Runtime

The compiled script includes these helpers, available in `on_submit` and source blocks:

| Helper                              | Description                                   |
|-------------------------------------|-----------------------------------------------|
| `_shell(cmd, timeout=5)`            | Execute shell command, return stdout          |
| `_read_file(path)`                  | Read file contents                            |
| `_read_json(path)`                  | Read + parse JSON file                        |
| `_http(url, method, body, headers)` | HTTP request, auto-parse JSON response        |
| `_sqlite(db_path, query)`           | SQLite query → list of dicts (or single dict) |
| `_get_field(name, default)`         | Read submitted form field                     |
| `_get_param(name, default)`         | Read URL parameter                            |
| `_load_state(path)`                 | Load state from JSON file                     |
| `_save_state(path, data)`           | Save state to JSON file                       |
| `_iter(val)`                        | Make a value iterable (list/dict/string)      |

### Components
```
# Define a reusable component
component stat(label, value, max)
  gauge "$label" $value $max 20

# Use it
stat "CPU" 62 100
stat "MEM" 84 100

# Import standard library
use std/dashboard
banner "My Node" "Mesh Network"
resources 62 84
```

### Standard Libraries
| Library | Components |
|---------|-----------|
| `std/dashboard` | `banner(title, subtitle)`, `resources(cpu, mem)`, `peer_status(name, state)` |
| `std/status-bar` | `status_bar(label, value, max)`, `status_item(name, state)` |
| `std/nav` | `nav_link(label, dest)`, `nav_divider()` |

## Rendering Pipeline

```
.uf source
    │
    ▼
  Parse ──→ IR Tree (30+ node types)
    │
    ▼
  Measure (bottom-up: compute sizes)
    │
    ▼
  Layout (top-down: assign positions)
    │
    ▼
  Paint (depth-first: write chars into CharGrid)
    │
    ▼
  Merge Borders (fix junction characters)
    │
    ├──→ ASCII emitter → plain text
    ├──→ Micron emitter → styled .mu (with colors, links, form tags)
    └──→ Codegen (if dynamic) → executable Python script
```

## Web IDE Features

- **Split-pane editor**: µFrame DSL source (left) / live preview (right)
- **Syntax highlighting**: keywords, strings, variables, comments in distinct colors
- **`/` command palette**: type `/` to insert layout, content, data viz, form, and style primitives
- **4 preview tabs**: ASCII | Micron (rendered) | Raw (Micron source) | Script (dynamic pages only)
- **`⚡ dynamic` badge**: auto-detected when source contains `source`, `if`, `for`, etc.
- **Examples dropdown**: 9 built-in templates (Hello World → Full Node Page → Dynamic Dashboard)
- **Pages dashboard**: table view with Published/Draft/Orphan status badges
- **Page graph**: React Flow visualization of inter-page links
- **Keyboard shortcuts**: `Ctrl+S` save draft, `Ctrl+P` publish
- **Unsaved changes guard**: warns before navigating away
- **Backlink indicator**: shows which pages link to the current page

## Running Tests

```bash
cd backend
source .venv/bin/activate
python -m pytest uframe/tests/ -v
```

42 tests covering:
- Static compilation (boxes, headings, text, gauges, tables, links, lists, spacers, dividers)
- Form elements (field, radio, checkbox, button)
- Dynamic pages (source, if/for, let, state, on_submit, codegen)
- Components (inline definitions, standard library, parameter substitution)

## Conventions

- µFrame engine is **pure Python stdlib** — zero external dependencies
- Backend is FastAPI; no ORM, flat file storage
- Frontend uses shadcn/ui components in `frontend/src/components/ui/`
- Feature components grouped by domain: `dashboard/`, `editor/`, `shared/`
- State management via Zustand stores
- All `.uf` sources stored in `SOURCES_DIR`, compiled `.mu` in `PAGES_DIR`

## References

- µFrame design spec: `docs/framework-design-v3.md`
- Dynamic templates spec: `docs/dynamic-templates.md`
- NomadNet: https://github.com/markqvist/NomadNet
- Micron syntax: https://github.com/fr33n0w/micron-composer
- Reticulum: https://github.com/markqvist/Reticulum
