feat: added a twist

This commit is contained in:
2026-04-01 00:53:55 +02:00
parent 0b7deee59e
commit b40c6436cd
76 changed files with 15121 additions and 64 deletions

883
docs/dynamic-templates.md Normal file
View File

@@ -0,0 +1,883 @@
# µFrame Dynamic Templates — Making Rich UIs Live on NomadNet
## Addendum to the µFrame Design Document (v3)
---
## 1. The NomadNet Dynamic Page Model
Understanding how NomadNet serves dynamic pages is essential to
understanding how µFrame templates become live applications.
### How it works
NomadNet has a simple but powerful execution model, analogous to
CGI on the early web:
```
Client Node Server
────── ───────────
1. Browse to /page/dashboard.mu
───────────▶
2. Is dashboard.mu executable?
YES → run it
NO → send contents as-is
3. Execute: #!/usr/bin/env python3
Script prints Micron to stdout
4. Capture stdout → send to client
◀───────────
5. Render Micron in terminal
```
Key mechanics:
- A `.mu` file without the execute bit is **static** — NomadNet
sends its contents directly to the browsing client
- A `.mu` file with the execute bit set is **dynamic** — NomadNet
runs it as a subprocess and serves whatever it prints to stdout
- The shebang line (`#!/usr/bin/env python3`) determines the
interpreter — Python, Bash, Lua, Rust, anything that runs
- The script must **terminate** — it cannot wait for input or
run indefinitely
- Cache behavior is controlled via a header line: `#!c=0` means
never cache (always re-execute), `#!c=300` means cache for 5 min
### Form data flow
Micron form fields (`\`<field\`placeholder>`, `\`<^|group|val\`label>`,
etc.) collect user input. When the user clicks a link on a page
that contains form fields, the field data is submitted along with
the link request:
```
Page A (has form fields + a submit link)
┌───────────────────────────────────────────────┐
│ Name: `<24|name`Enter name...> │
│ Role: `<^|role|admin`Admin> `<^|role|user`User> │
│ │
│ `[Submit`:/page/handle.mu] │
└───────────────────────────────────────────────┘
User fills in "Alice", selects "Admin", clicks Submit
Page B (handle.mu) — receives field data via environment
variables, generates a response page with Micron output
```
The submitted field data is passed to the executable script
through environment variables in the format:
```
FIELD_name=Alice
FIELD_role=admin
```
Or via stdin as a URL-encoded or structured data payload
(implementation varies by NomadNet version). The executable
script reads these values and uses them to generate its output.
---
## 2. µFrame's Dynamic Compilation Model
Here is the key insight: **µFrame doesn't just emit static
Micron text — it can compile `.uf` templates into executable
Python scripts that generate Micron at request time.**
This gives us three output modes:
```
.uf source ──▶ Parser ──▶ IR ──┬──▶ Plain ASCII (static preview)
├──▶ Static .mu (static page)
└──▶ Dynamic .mu (executable script)
#!/usr/bin/env python3
# Auto-generated by µFrame
# from: dashboard.uf
import os, sys, json, subprocess
...
print(rendered_micron)
```
The dynamic output is a self-contained Python script with:
- The µFrame rendering engine embedded (or imported)
- Data-fetching hooks that run at request time
- Form field processing from environment variables
- Conditional rendering based on submitted data
- The full ASCII art + Micron generation pipeline
### The three modes compared
```
┌──────────────────────────────────────────────────────────────┐
│ µFrame Output Modes │
├────────────────┬───────────────┬─────────────────────────────┤
│ Plain ASCII │ Static .mu │ Dynamic .mu │
├────────────────┼───────────────┼─────────────────────────────┤
│ Box-drawing │ Box-drawing │ Box-drawing │
│ Block chars │ Block chars │ Block chars │
│ Braille │ Braille │ Braille │
│ │ + Color │ + Color │
│ │ + Bold/italic │ + Bold/italic │
│ │ + Links │ + Links │
│ │ + Form fields │ + LIVE form fields │
│ │ │ + Server-side data binding │
│ │ │ + Conditional rendering │
│ │ │ + Form submission handling │
│ │ │ + System data at render time │
│ │ │ + State persistence │
├────────────────┼───────────────┼─────────────────────────────┤
│ Local terminal │ NomadNet page │ NomadNet live application │
│ preview │ (cached) │ (re-executed per request) │
└────────────────┴───────────────┴─────────────────────────────┘
```
---
## 3. DSL Extensions for Dynamic Behavior
### 3.1 Data Sources — `source` blocks
A `source` block declares where live data comes from. At render
time, the generated script executes the source and binds the
result to a variable.
```
# Shell command — output captured as string
source cpu_pct : shell "grep 'cpu ' /proc/stat | awk '{print int(($2+$4)*100/($2+$4+$5))}'"
source mem_pct : shell "free | awk '/Mem/{print int($3/$2*100)}'"
source uptime : shell "uptime -p"
source peers : shell "rnstatus -j | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d.get(\"peers\",[]))); '"
# File read — contents loaded as string or parsed as JSON
source motd : file "/etc/motd"
source config : json "/home/node/.nomadnetwork/config.json"
# Python expression — evaluated inline
source timestamp : python "datetime.now().strftime('%Y-%m-%d %H:%M')"
source rand_hex : python "secrets.token_hex(4)"
# RNS/Reticulum API — direct integration
source peer_list : rns "peers"
source node_info : rns "identity"
```
Sources are resolved **at page render time** — every time a
client requests the page, the commands run fresh.
Usage in templates:
```
gauge "CPU" $cpu_pct 100 28 warn=75 crit=90
gauge "MEM" $mem_pct 100 28 warn=80 crit=95
label "Uptime" "$uptime"
label "Peers" "$peers active"
text "Last updated: $timestamp"
```
### 3.2 Form Handling — `on_submit` blocks
An `on_submit` block defines what happens when a form is
submitted. It receives field values and controls what the
page renders in response.
```
page "Search" 64
form "search"
field "query" 30 "Enter search term..."
radio "scope" "Local" | "Network" | "All"
button "Search" "/page/search.mu"
on_submit "search"
# $query and $scope are now populated from submitted form data
source results : shell "search_index.py '$query' --scope '$scope'"
heading 2 "Results for: $query"
if $results
text "$results"
else
text "No results found."
```
The generated Python script handles this as:
```python
#!/usr/bin/env python3
#!c=0
import os, subprocess
# Read submitted form data
query = os.environ.get("FIELD_query", "")
scope = os.environ.get("FIELD_scope", "Local")
if query:
# Form was submitted — render results
results = subprocess.check_output(
["search_index.py", query, "--scope", scope]
).decode().strip()
# ... render results template with Micron ...
else:
# No submission — render the form
# ... render form template with Micron ...
```
### 3.3 Conditional Rendering — `if` / `else` / `elif`
```
source disk_pct : shell "df / | awk 'NR==2{print int($5)}'"
if $disk_pct > 90
box heavy "DISK CRITICAL"
color f00
gauge "Disk" $disk_pct 100 40 crit=90
text "@bold{@color{f00}{Immediate action required!}}"
elif $disk_pct > 75
box light "Disk Warning"
color ff0
gauge "Disk" $disk_pct 100 40 warn=75
else
gauge "Disk" $disk_pct 100 40
```
### 3.4 Iteration — `for` loops
```
source peer_json : shell "rnstatus --json-peers"
heading 2 "Active Peers"
for peer in $peer_json
row 2
col 30
text "$peer.name"
col 10
status "$peer.name" $peer.state
col 10
text "$peer.latency"
```
The `for` construct works with JSON arrays or newline-delimited
text from shell commands.
### 3.5 State Persistence — `state` blocks
Since each page request is a fresh script execution, state must
be stored externally. µFrame provides a simple key-value store
backed by a JSON file on the node:
```
state "counter" "/tmp/uframe_counter.json"
# Read
let visits = $counter.visits || 0
# Write (increments on each page load)
set counter.visits = $visits + 1
text "This page has been viewed $counter.visits times."
```
For form-driven state (e.g., a guestbook):
```
state "guestbook" "/var/nomadnet/guestbook.json"
form "sign"
field "name" 20 "Your name..."
field "message" 40 "Your message..."
button "Sign" "/page/guestbook.mu"
on_submit "sign"
append guestbook.entries { name: $name, message: $message, time: $timestamp }
text "@color{0f0}{Thanks, $name! Your message has been saved.}"
heading 2 "Guestbook ($guestbook.entries.length entries)"
for entry in $guestbook.entries
box light
text "@bold{$entry.name} — @italic{$entry.time}"
text "$entry.message"
spacer
```
### 3.6 Page Navigation with Data — `link` with parameters
Links can pass data to the target page via query-style encoding:
```
# Simple navigation
link "Home" "/page/index.mu"
# Navigation with parameters
link "View Peer $peer.name" "/page/peer_detail.mu?hash=$peer.hash"
# In the target page, access with:
source peer_hash : param "hash"
```
### 3.7 Cache Control
```
page "Dashboard" 64
cache 0 # never cache — always re-execute
# cache 60 # cache for 60 seconds
# cache none # alias for 0
source cpu : shell "..."
...
```
Translates to the Micron header `#!c=0` in the first line of output.
---
## 4. Compilation Pipeline — .uf to Executable .mu
### 4.1 What the compiler generates
A `.uf` file with dynamic features compiles into a Python script
that:
1. Sets the shebang and cache header
2. Imports required modules
3. Reads environment variables (form data)
4. Executes source commands (shell, file, python, rns)
5. Evaluates conditionals and loops
6. Renders the IR tree into a CharGrid
7. Emits the CharGrid as Micron with style tags
8. Prints to stdout
```python
#!/usr/bin/env python3
#!c=0
# Auto-generated by µFrame from dashboard.uf
# Do not edit — regenerate with: uframe compile dashboard.uf
import os, sys, json, subprocess, datetime, secrets
# ─── µFrame Runtime (embedded) ───────────────────────────────
class CharGrid:
"""2D character grid with style annotations."""
def __init__(self, width, height):
self.w = width
self.h = height
self.chars = [[' ']*width for _ in range(height)]
self.styles = [[None]*width for _ in range(height)]
def put(self, x, y, ch, style=None):
if 0 <= x < self.w and 0 <= y < self.h:
self.chars[y][x] = ch
self.styles[y][x] = style
def box(self, x, y, w, h, weight='light', title=None, title_style=None):
"""Draw a box with automatic border characters."""
# ... border drawing logic ...
def gauge(self, x, y, w, value, max_val, label=None,
warn=None, crit=None):
"""Render a horizontal gauge bar with threshold colors."""
pct = min(value / max_val, 1.0)
filled = int(w * pct)
for i in range(w):
ch = '█' if i < filled else '░'
fg = None
if crit and value >= crit: fg = 'f00'
elif warn and value >= warn: fg = 'ff0'
elif i < filled: fg = '0f0'
else: fg = '555'
self.put(x + i, y, ch, {'fg': fg})
# ... label and percentage ...
def sparkline(self, x, y, w, values):
"""Render braille sparkline from value array."""
# ... braille pattern generation ...
def emit_micron(self):
"""Scan grid and emit Micron with style tags."""
lines = []
for row_idx in range(self.h):
line = []
cur_style = None
for col_idx in range(self.w):
ch = self.chars[row_idx][col_idx]
st = self.styles[row_idx][col_idx]
if st != cur_style:
# Close previous style tags
if cur_style:
if cur_style.get('fg'): line.append('`f')
if cur_style.get('bold'): line.append('`!')
# Open new style tags
if st:
if st.get('bold'): line.append('`!')
if st.get('fg'): line.append(f'`F{st["fg"]}')
cur_style = st
line.append(ch)
# Close final style
if cur_style:
if cur_style.get('fg'): line.append('`f')
if cur_style.get('bold'): line.append('`!')
lines.append(''.join(line).rstrip())
return '\n'.join(lines)
# ─── Form Data ───────────────────────────────────────────────
def get_field(name, default=''):
"""Read submitted form field from environment."""
return os.environ.get(f'FIELD_{name}', default)
def get_param(name, default=''):
"""Read URL parameter."""
return os.environ.get(f'PARAM_{name}',
os.environ.get(f'var_{name}', default))
# ─── Data Sources ────────────────────────────────────────────
def shell(cmd):
"""Execute shell command, return stdout."""
try:
return subprocess.check_output(
cmd, shell=True, timeout=5
).decode().strip()
except Exception:
return ''
# ─── Resolve Sources ─────────────────────────────────────────
cpu_pct = int(shell(
"grep 'cpu ' /proc/stat | awk '{print int(($2+$4)*100/($2+$4+$5))}'"
) or 0)
mem_pct = int(shell(
"free | awk '/Mem/{print int($3/$2*100)}'"
) or 0)
uptime_str = shell("uptime -p")
peer_count = shell("rnstatus -j 2>/dev/null | python3 -c "
"'import sys,json; print(len(json.load(sys.stdin).get(\"peers\",[])))'")
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
# ─── Build Grid & Render ────────────────────────────────────
grid = CharGrid(66, 40)
# ... all the box(), gauge(), sparkline(), text() calls
# ... exactly as the layout engine would produce them ...
# ─── Output ──────────────────────────────────────────────────
print('#!c=0') # cache header: never cache
print(grid.emit_micron())
```
### 4.2 CLI usage
```bash
# Compile to dynamic executable .mu
uframe compile dashboard.uf --out dashboard.mu
chmod +x dashboard.mu
# Compile and deploy in one step
uframe deploy dashboard.uf
# → renders, sets +x, copies to ~/.nomadnetwork/storage/pages/
# Compile with embedded vs. imported runtime
uframe compile dashboard.uf --embed # single self-contained file
uframe compile dashboard.uf --import # requires uframe_runtime.py on node
```
### 4.3 Compilation modes
```
┌────────────────────────────────────────────────────────────────┐
│ µFrame Compilation Modes │
├────────────────┬───────────────┬───────────────────────────────┤
│ uframe render │ uframe render │ uframe compile │
│ --ascii │ --micron │ │
├────────────────┼───────────────┼───────────────────────────────┤
│ Static ASCII │ Static .mu │ Executable .mu (Python) │
│ to stdout │ file │ file with +x │
├────────────────┼───────────────┼───────────────────────────────┤
│ All values │ All values │ source{} values fetched │
│ resolved at │ resolved at │ at request time │
│ render time │ render time │ │
│ │ │ Form fields become live │
│ │ │ on_submit{} blocks active │
│ │ │ if/for evaluated per request │
│ │ │ State persists across visits │
└────────────────┴───────────────┴───────────────────────────────┘
```
---
## 5. Complete Dynamic Example
### 5.1 Source — `search_node.uf`
```
page "Node Search" 64
cache 0
source timestamp : python "datetime.now().strftime('%H:%M:%S')"
source peer_count : shell "rnstatus 2>/dev/null | grep -c 'Peer'"
state "history" "/var/nomadnet/search_history.json"
box double "Node Search"
align center
text "Find peers and pages on the Reticulum mesh"
text "@italic{$peer_count peers reachable · updated $timestamp}"
spacer
form "search"
field "query" 30 "Search term..."
radio "type" "Nodes" | "Pages" | "Files"
checkbox "cache" "Include cached results"
button "Search" "/page/search_node.mu"
on_submit "search"
# Log the search
append history.queries { q: $query, type: $type, time: $timestamp }
source results : shell "mesh_search.py '$query' --type '$type'"
source result_count : python "len('''$results'''.strip().splitlines())"
divider light
heading 2 "Results for \"$query\" ($result_count found)"
if $result_count > 0
for line in $results
source parts : python "'''$line'''.split('|')"
row 1
col 28
link "$parts.0" "/page/detail.mu?hash=$parts.1"
col 8
text "$parts.2"
col 10
status "$parts.0" $parts.3
else
spacer
text "@center{@color{ff0}{No results found for \"$query\"}}"
spacer
divider light
heading 3 "Recent Searches"
for entry in $history.queries[-5:]
text " $entry.time $entry.q ($entry.type)"
divider heavy
text "@center{@italic{Relay Alpha-7 · $timestamp}}"
```
### 5.2 What the client sees
**Before submission** (form is empty):
```
╔══ Node Search ═══════════════════════════════════════════════╗
║ Find peers and pages on the Reticulum mesh ║
║ 7 peers reachable · updated 14:32:07 ║
╚══════════════════════════════════════════════════════════════╝
Search: [ Search term...___________________ ]
Type: (•) Nodes ( ) Pages ( ) Files
[ ] Include cached results
`[`!Search`!`:/page/search_node.mu]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Relay Alpha-7 · 14:32:07
```
**After submitting "relay"** (script re-executes with FIELD_query=relay):
```
╔══ Node Search ═══════════════════════════════════════════════╗
║ Find peers and pages on the Reticulum mesh ║
║ 7 peers reachable · updated 14:32:15 ║
╚══════════════════════════════════════════════════════════════╝
Search: [ relay__________________________ ]
Type: (•) Nodes ( ) Pages ( ) Files
[ ] Include cached results
`[`!Search`!`:/page/search_node.mu]
──────────────────────────────────────────────────────────────
>> Results for "relay" (3 found)
`F0cfRelay-East`f 2 hops `F0f0●`f online
`F0cfRelay-South`f 4 hops `F0f0●`f online
`F0cfRelay-Backup`f 6 hops `Fff0◐`f degraded
──────────────────────────────────────────────────────────────
>>> Recent Searches
14:32:15 relay (Nodes)
14:28:44 bridge (Pages)
14:25:01 firmware (Files)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Relay Alpha-7 · 14:32:15
```
The form fields retain submitted values, results appear below,
and the search history persists across visits via the state file.
Every piece of box-drawing, every colored indicator, every braille
sparkline renders identically in both ASCII preview and live Micron.
---
## 6. Dynamic Patterns — A Cookbook
### 6.1 Live Dashboard (auto-refresh via cache=0)
```
page "Status" 64
cache 0
source cpu : shell "cat /proc/loadavg | awk '{print int($1*100/$(nproc))}'"
source mem : shell "free | awk '/Mem/{print int($3/$2*100)}'"
source net_in : shell "net_traffic.sh in"
source net_out : shell "net_traffic.sh out"
source net_history_in : shell "net_spark.sh in 20"
source net_history_out : shell "net_spark.sh out 20"
box heavy "System Status"
row 2
gauge "CPU" $cpu 100 28 warn=75 crit=90
gauge "MEM" $mem 100 28 warn=80 crit=95
spacer
label "IN" "$net_in KB/s"
sparkline "IN" $net_history_in 28
label "OUT" "$net_out KB/s"
sparkline "OUT" $net_history_out 28
text "@center{@italic{Press Ctrl+R to refresh}}"
```
Client hits the page → script runs → reads `/proc` → renders
gauges and sparklines with real data → client sees it.
Ctrl+R re-requests → fresh execution → updated values.
### 6.2 Guestbook with Persistent State
```
page "Guestbook" 64
cache 0
state "gb" "/var/nomadnet/guestbook.json"
source timestamp : python "datetime.now().strftime('%Y-%m-%d %H:%M')"
heading 1 "Guestbook"
form "sign"
field "name" 20 "Your name"
field "msg" 40 "Leave a message..."
button "Sign" "/page/guestbook.mu"
on_submit "sign"
if $name && $msg
prepend gb.entries { name: $name, msg: $msg, time: $timestamp }
text "@color{0f0}{✓ Thanks, $name!}"
divider light
for entry in $gb.entries[:20]
box rounded
text "@bold{$entry.name} @italic{@color{888}{$entry.time}}"
text "$entry.msg"
spacer
```
### 6.3 Multi-Page Wizard with Navigation
```
# Page 1: setup.mu
page "Setup Wizard — Step 1" 64
cache 0
heading 1 "Network Configuration"
form "net"
field "interface" 20 "eth0"
radio "mode" "Auto" | "Manual" | "Mesh Only"
button "Next →" "/page/setup_2.mu"
# Page 2: setup_2.mu
page "Setup Wizard — Step 2" 64
cache 0
source iface : param "interface" # or field from previous page
source mode : param "mode"
heading 1 "Confirm Settings"
label "Interface" "$iface"
label "Mode" "$mode"
form "confirm"
checkbox "apply_now" "Apply immediately"
button "← Back" "/page/setup.mu"
button "Finish ✓" "/page/setup_done.mu?interface=$iface&mode=$mode"
```
### 6.4 Chat Room (community pattern)
```
page "Chat" 64
cache 0
state "chat" "/var/nomadnet/chatlog.json"
source timestamp : python "datetime.now().strftime('%H:%M')"
heading 1 "Node Chat"
# Display last 15 messages
for msg in $chat.messages[-15:]
text "@bold{@color{$msg.color}{$msg.nick}} @color{888}{$msg.time}"
text " $msg.text"
divider light
form "send"
field "nick" 12 "Nickname"
field "text" 40 "Type message..."
button "Send" "/page/chat.mu"
on_submit "send"
if $nick && $text
source color : python "format(hash('$nick')%4095,'03x')"
append chat.messages { nick: $nick, text: $text, time: $timestamp, color: $color }
text "@center{@italic{@color{888}{Ctrl+R to refresh · $chat.messages.length messages}}}"
```
### 6.5 Interactive Data Explorer
```
page "Peer Explorer" 64
cache 0
source peers_json : shell "rnstatus --json 2>/dev/null"
source selected : param "hash"
heading 1 "Peer Explorer"
table "Peers"
columns "Name" 24 | "Hops" 6 | "RTT" 8 | "Status" 10
for peer in $peers_json.peers
row "$peer.name" | "$peer.hops" | "$peer.rtt" | "$peer.status"
if $selected
divider heavy
source detail : shell "rnstatus --peer $selected --json 2>/dev/null"
box double "Peer Detail: $detail.name"
label "Hash" "$detail.hash"
label "Address" "$detail.address"
label "Hops" "$detail.hops"
label "Latency" "$detail.rtt"
label "Last Seen" "$detail.last_seen"
label "Transport" "$detail.transport"
sparkline "Latency (24h)" $detail.latency_history 40
row 2
link "Ping" "/page/action.mu?cmd=ping&hash=$detail.hash"
link "Trace" "/page/action.mu?cmd=trace&hash=$detail.hash"
link "Browse" "$detail.hash:/page/index.mu"
```
---
## 7. Security Considerations
Dynamic pages execute code on the node server. µFrame enforces:
- **Shell command sanitization**: all `$variable` values interpolated
into shell commands are escaped with `shlex.quote()` to prevent
injection
- **State file isolation**: state files are restricted to a
configurable directory (default: `/var/nomadnet/uframe/`)
- **Execution timeout**: all shell sources have a default 5-second
timeout, configurable per source
- **No network egress by default**: source commands run in the
node's local context — they can read local system data but
µFrame does not add network capabilities beyond what the
scripts themselves invoke
- **Input validation**: field values are length-limited and
sanitized before use in sources or state operations
```
# In the DSL, explicit sanitization:
source result : shell "search.py" --arg $query --sanitize
timeout 10
max_length 256
```
---
## 8. Architecture Summary
```
┌──────────────────────────────┐
│ .uf Source │
│ (layout + sources + forms │
│ + conditionals + state) │
└──────────────┬───────────────┘
┌────▼────┐
│ Parse │
└────┬────┘
┌────▼────┐
│ IR │
│ Tree │
└──┬───┬──┘
│ │
┌──────────────┘ └────────────────┐
│ │
┌──────▼──────┐ ┌───────▼───────┐
│ render mode │ │ compile mode │
│ (immediate) │ │ (codegen) │
└──┬───────┬──┘ └───────┬───────┘
│ │ │
┌──────▼┐ ┌───▼─────┐ ┌───────▼────────┐
│ ASCII │ │ Static │ │ Executable .mu │
│ stdout│ │ .mu file│ │ Python script │
└───────┘ └─────────┘ │ with embedded │
│ runtime + │
│ data sources + │
│ form handling │
└───────┬────────┘
chmod +x
deploy to
┌─────────▼─────────┐
│ NomadNet Node │
│ ~/.nomadnetwork/ │
│ storage/pages/ │
│ │
│ Client request → │
│ Execute script → │
│ Stdout = Micron │
│ with live data, │
│ rich ASCII art, │
│ color + forms │
└───────────────────┘
```
The dynamic model turns µFrame from a static template engine into a
**full application framework for NomadNet** — where the same DSL that
defines the visual layout also defines the data flow, user interaction,
and server-side logic. The ASCII art isn't decoration — it's the UI
of a live, interactive, decentralized application running over
encrypted mesh networks.

889
docs/framework-design-v3.md Normal file
View File

@@ -0,0 +1,889 @@
# µFrame — A DSL for Rich Terminal UIs rendered as ASCII and Micron
## Overview
**µFrame** is a declarative DSL that compiles to rich terminal
interfaces built from Unicode box-drawing, block elements, braille
patterns, and careful spatial layout. It produces two outputs from
the same source:
1. **Plain ASCII** — the raw visual layout, viewable in any terminal
2. **Micron `.mu`** — the same visual layout enhanced with Micron's
color, styling, links, and interactive form fields
Both outputs share the same rich character art. Micron doesn't
degrade the visuals — it *elevates* them. The ASCII art passes
through verbatim into the `.mu` file, and Micron tags wrap it
with color, emphasis, alignment, and interactivity that plain
text cannot express.
```
┌──────────────────────────────────┐
┌───▶│ Plain ASCII │
│ │ Box drawing, braille, blocks │
│ │ No color, no links, no forms │
.uf ──▶ Parser ──▶ IR ─┤ └──────────────────────────────────┘
│ ┌──────────────────────────────────┐
└───▶│ Micron .mu │
│ Same visual base │
│ + `Fhex color`f │
│ + `!bold`! `*italic`* │
│ + `[links`/dest] │
│ + `<form fields`> │
│ + `c alignment`a │
└──────────────────────────────────┘
```
The relationship is additive:
```
Plain ASCII = layout + structure + data viz
Micron = layout + structure + data viz + color + style + interaction
```
---
## 1. Design Philosophy
### The terminal is the canvas — in both modes
A NomadNet node browser is a terminal. A local shell is a terminal.
The character grid is the shared substrate. Unicode box-drawing,
block elements, and braille dots render identically in both
contexts. µFrame exploits this fully:
- **Box drawing** (`┌─┐│└┘`) creates bordered panels, tables,
nested layouts — same characters in ASCII and Micron
- **Block elements** (`█▉▊▋▌▍▎▏░▒▓`) build bar charts, gauges,
heatmaps — passed through as literal text in Micron
- **Braille** (```⣿`, 256 patterns) gives 2×4 sub-cell resolution
for sparklines and dot plots — just text, works everywhere
- **Micron then paints on top**: colored bars, highlighted thresholds,
bold headers, clickable links, interactive form fields
### What Micron adds beyond ASCII
Micron's tag system maps perfectly onto the styling layer that
plain ASCII lacks:
| Capability | Plain ASCII | Micron |
|--------------------|---------------------|---------------------------------------|
| Borders & boxes | ✓ box-drawing chars | ✓ same chars + colored with `Fhex` |
| Bar charts | ✓ block elements | ✓ same blocks + colored thresholds |
| Sparklines | ✓ braille dots | ✓ same braille + colored |
| Status indicators | ✓ ● ○ ◐ chars | ✓ same chars + `F0f0` green/red |
| Table data | ✓ monospace align | ✓ same alignment + bold headers |
| Emphasis | ✗ (no mechanism) | ✓ `!bold`! `*italic`* `_underline`_ |
| Color | ✗ (no mechanism)* | ✓ `Fhex text`f / `Bhex text`b |
| Alignment | manual spacing | ✓ `c center`a / `r right`a |
| Links | ✗ display only | ✓ `[click here`/page.mu] |
| Text input | ✗ display only | ✓ `<name`placeholder> |
| Radio / checkbox | ✗ visual only | ✓ `<^|group|val`label> `<?|..`label> |
| Headings | manual styling | ✓ `>` / `>>` / `>>>` with styling |
*ASCII mode can optionally emit ANSI escape codes with `--ansi`,
but the default is pure text.
---
## 2. The Rendering Model
Both renderers share a common **CharGrid** — a 2D matrix of
characters that represents the visual layout. They diverge only
in the final emission step.
```
┌──────────┐
.uf ──▶ Parse ──▶│ IR Tree │
└────┬─────┘
┌────▼─────┐
│ Layout │ ◀── width resolution, row splits,
│ Engine │ border merging, chart rendering
└────┬─────┘
┌────▼─────┐
│ CharGrid │ ◀── 2D array of (char, style) pairs
│ + Styles │ style = {fg, bg, bold, italic,
└──┬────┬──┘ underline, link, field_meta}
│ │
┌───────▼┐ ┌▼─────────┐
│ ASCII │ │ Micron │
│ Emitter │ │ Emitter │
└─────────┘ └──────────┘
chars only chars + tags
```
### The CharGrid
Every cell in the grid stores:
```
Cell:
char : string # the visible character (e.g. "█", "┌", "⣿")
fg : string|null # foreground color, 3-digit hex
bg : string|null # background color, 3-digit hex
bold : bool
italic : bool
underline : bool
link : string|null # destination path for clickable cells
field : FieldMeta|null # form field metadata for interactive cells
```
### ASCII Emitter
Reads only `cell.char` from each cell. Produces a plain text file.
With `--ansi`, reads `fg`, `bg`, `bold`, `italic`, `underline`
and emits ANSI escape codes.
### Micron Emitter
Reads every cell property. Scans each line left-to-right, tracks
style state, and opens/closes Micron tags at style transitions:
```
Line scan: ┌── gauge "CPU" ──────────┐
Chars: C P U █ █ █ ░ ░ 6 2 %
Styles: bold fg:0f0 fg:f00
↓ ↓
Micron: `!CPU`! `F0f0███`f`Ff00░░`f `Ff0062%`f
```
This means the exact same box-drawing layout appears in both
outputs. The Micron version simply has color and emphasis tags
woven between the same characters.
---
## 3. Visual Primitives — The Shared Toolkit
Everything below renders identically in both ASCII and Micron.
The Micron output adds color/style annotations on top.
### 3.1 Box Drawing
Four border weights:
```
Light Heavy Double Rounded
┌──────┐ ┏━━━━━━┓ ╔══════╗ ╭──────╮
│ │ ┃ ┃ ║ ║ │ │
└──────┘ ┗━━━━━━┛ ╚══════╝ ╰──────╯
```
Nested boxes with automatic junction merging:
```
┌─────────────────┬──────────┐
│ Left Panel │ Right │
│ │ │
├─────────────────┴──────────┤
│ Footer spans full width │
└────────────────────────────┘
```
Titled boxes (title inlined in top border):
```
┌─ System Health ─────────────────────────┐
│ │
└─────────────────────────────────────────┘
┏━ ALERT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
In Micron, the border characters are plain text. The title
can be wrapped in `!bold`! and `Fhex color`f tags.
### 3.2 Data Visualization
**Horizontal bars** — block elements with optional color thresholds:
```
ASCII: CPU ████████████████████░░░░░░░░░░ 62%
Micron: CPU `F0f0████████████████████`f`F333░░░░░░░░░░`f 62%
↑ green fill ↑ dim empty
```
**Gauges with thresholds** — color shifts at warn/crit boundaries:
```
ASCII: MEM █████████████████████████████░ 93% ⚠
Micron: MEM `Ff00█████████████████████████████`f░ `Ff00 93% ⚠`f
↑ red because value > crit threshold
```
**Vertical bar charts**:
```
ASCII: Micron adds color per bar:
█ `F08f█`f
█ █ `F08f█`f `F0f0█`f
█ █ █ █ `F08f█`f `Fff0█`f `F0f0█`f `Fff0█`f
█ █ █ █ █ █ `F08f█`f `Fff0█`f █ `F0f0█`f █ `Fff0█`f
█ █ █ █ █ █ █ █ ...
─────────────────
0 3 6 9 12 15
```
**Sparklines** — braille characters for inline time series:
```
ASCII: NET ⣀⣤⣶⣿⣿⣷⣶⣤⣀⣀⣤⣶⣿⣷⣤⣀ avg 31%
Micron: NET `F0ff⣀⣤⣶⣿⣿⣷⣶⣤⣀⣀⣤⣶⣿⣷⣤⣀`f avg 31%
↑ cyan sparkline
```
**Heatmap** — using shade blocks with per-cell color:
```
Mon `F0f0░`f`F0f0░`f`F4f0▒`f`F8f0▓`f`Fff0█`f`F8f0▓`f`F4f0▒`f
Tue `F4f0▒`f`F8f0▓`f`Fff0█`f`Fff0█`f`Fff0█`f`F8f0▓`f`F4f0▒`f
Wed `F0f0░`f`F0f0░`f`F0f0░`f`F4f0▒`f`F4f0▒`f`F0f0░`f`F0f0░`f
0 4 8 12 16 20 24
```
In plain ASCII, same characters, no color — the shade density
still communicates intensity.
**Status indicators** — colored in Micron, shape-coded in ASCII:
```
ASCII: ● Online ○ Offline ◐ Degraded ◌ Unknown
Micron: `F0f0●`f Online `Ff00○`f Offline `Fff0◐`f Degraded
```
Both modes are readable — color adds clarity but shape carries
the information alone.
### 3.3 Tables
Tables use box-drawing for structure. Identical in both outputs.
Micron adds bold headers and colored status cells:
```
ASCII:
┌──────────────────────┬──────┬─────────┬──────────┐
│ Destination │ Hops │ Latency │ Status │
├──────────────────────┼──────┼─────────┼──────────┤
│ a7f2::relay-east │ 2 │ 34ms │ ● alive │
│ c4e1::bridge-south │ 4 │ 112ms │ ● alive │
│ 01ab::node-gamma │ 7 │ 580ms │ ○ stale │
└──────────────────────┴──────┴─────────┴──────────┘
Micron:
┌──────────────────────┬──────┬─────────┬──────────┐
│ `!Destination`! │`!Hops`!│`!Latency`!│`!Status`!│
├──────────────────────┼──────┼─────────┼──────────┤
│ a7f2::relay-east │ 2 │ 34ms │ `F0f0●`f alive │
│ c4e1::bridge-south │ 4 │ 112ms │ `F0f0●`f alive │
│ 01ab::node-gamma │ 7 │ 580ms │ `Ff00○`f stale │
└──────────────────────┴──────┴─────────┴──────────┘
```
### 3.4 Form Elements
In ASCII, forms are visual representations. In Micron, the same
characters appear but the input areas become live interactive
fields.
```
ASCII (visual only):
┌─ Search ──────────────────────────────┐
│ │
│ Query: [ _________________________ ] │
│ │
│ Scope: (•) Local ( ) Network │
│ [ ] Include offline nodes │
│ │
│ ┌──────────┐ │
│ │ Search │ │
│ └──────────┘ │
└───────────────────────────────────────┘
Micron (interactive):
┌─ `!Search`! ──────────────────────────┐
│ │
│ Query: `<32|query`Enter search...> │
│ │
│ Scope: `<^|scope|local|*`Local> `<^|scope|net`Network>
│ `<?|offline|yes`Include offline nodes>
│ │
│ `[`!Search`!`:/action/search]
│ │
└───────────────────────────────────────┘
```
The border characters are identical. Micron replaces the
placeholder bracket notation with live form tags and turns
the button into a clickable link.
---
## 4. The DSL — `.uf` Files
### 4.1 Syntax
- **Indentation** defines nesting (2-space)
- **Keywords** lead each line
- **Strings** in double quotes
- **Pipe `|`** separates inline list items
- **`$`** references variables
- **`#`** starts comments
### 4.2 Layout Primitives
```
page "Title" [width]
# Root container. Default width: 64.
box [weight] "Title"
# Bordered region. weight: light|heavy|double|rounded
# Title is inset in the top border.
# In Micron: title gets `!bold`!, border chars are literal.
row [gap]
# Horizontal layout. Children split available width.
# gap: chars between children (default: 1)
col [width]
# Explicit column in a row. Width in chars or percentage.
spacer [lines]
# Vertical whitespace. Default: 1
pad [top] [right] [bottom] [left]
# Inner margin for a container.
```
### 4.3 Content Primitives
```
heading [1|2|3] "Text"
# Rendered with underline/box in ASCII.
# In Micron: > / >> / >>> plus `!bold`!
text "Content with @bold{inline} @color{0f0}{modifiers}"
# @bold{...} → Micron `!...`!
# @italic{...} → Micron `*...*`
# @under{...} → Micron `_..._`
# @color{hex}{...} → Micron `Fhex...`f
# @bg{hex}{...} → Micron `Bhex...`b
# In ASCII: modifiers stripped (or ANSI with --ansi)
label "Key" "Value"
# Aligned key-value pair.
list [bullet|number|dash|arrow]
item "First"
item "Second"
link "Display text" "/destination.mu"
# ASCII: [Display text]
# Micron: `[Display text`/destination.mu]
divider [light|heavy|double|dash|dot]
# Full-width horizontal rule using appropriate chars.
```
### 4.4 Data Visualization Primitives
```
gauge "Label" [value] [max] [width]
# Horizontal progress bar.
# Thresholds: warn=[n] crit=[n]
# ASCII: Label ████████████░░░░ 62%
# Micron: same, with color shifts at thresholds
meter "Label" [value] [max]
# Compact inline gauge (no border, just bar + %)
bar_h "Label" [value] [max] [width]
# Single horizontal bar in a chart context.
bar_v [height]
# Vertical bar chart container.
bar "Label" [value]
bar "Label" [value]
# Uses ▁▂▃▄▅▆▇█ stacked vertically.
sparkline "Label" [values] [width]
# Braille-dot inline chart.
# values: comma-separated or $variable
heatmap [rows] [cols]
# Grid of colored shade blocks.
# Uses ░▒▓█ for intensity.
# Micron adds per-cell `Fhex` color.
status "Label" [online|offline|degraded|unknown|alert]
# Shape-coded indicator + label.
# Micron adds color to the indicator.
table "Title"
columns "Name" [width] | "Name" [width] | ...
row "val" | "val" | ...
# Box-drawn table with header separator.
# Micron: bold headers, colored cells via @modifiers in values.
```
### 4.5 Form Primitives
```
form "name"
field "name" [width] "placeholder"
password "name" [width] "placeholder"
radio "group" "Opt A" | "Opt B" | "Opt C"
checkbox "name" "Label"
toggle "name" "Label" [on|off]
dropdown "name" "Opt A" | "Opt B" | "Opt C"
button "Label" ["/action/path"]
# ASCII: visual placeholders (brackets, radio dots, etc.)
# Micron: live interactive fields using native form tags
```
### 4.6 Style Modifiers
Applied as indented children of any node:
```
align [left|center|right]
color [3-digit hex]
bg [3-digit hex]
border [light|heavy|double|rounded|none]
bold
italic
underline
```
### 4.7 Variables & Components
```
# Variables
let name = "Relay Alpha-7"
let cpu_data = 42, 67, 55, 78, 91, 63, 48
# Component definition
component stat(label, value, max, trend)
box light "$label"
gauge "$label" $value $max 20
text "@italic{$trend}"
# Component usage
row 2
stat "CPU" 62 100 "▲ +5%"
stat "MEM" 84 100 "▼ -2%"
```
---
## 5. Complete Example
### 5.1 Source
```
let node = "Relay Alpha-7"
let uptime = "14d 3h 22m"
page "$node" 66
box double "$node"
align center
text "Reticulum Network Node"
text "Online $uptime"
spacer
heading 1 "Resources"
row 2
col
gauge "CPU" 62 100 28 warn=75 crit=90
gauge "GPU" 21 100 28
col
gauge "MEM" 84 100 28 warn=80 crit=95
gauge "SWP" 3 100 28
spacer
heading 1 "Network"
row 2
col 40
text "Traffic (60s)"
sparkline "IN" 1,3,5,8,7,5,3,2,1,3,6,8,7,4 20
sparkline "OUT" 2,2,3,5,8,7,5,3,2,1,1,3,5,8 20
col
label "Peers" "7 / 12"
status "East Relay" online
status "South Bridge" online
status "Node Gamma" degraded
spacer
heading 1 "Routing"
table "Routes"
columns "Destination" 22 | "Hops" 6 | "RTT" 8 | "State" 10
row "a7f2::relay-east" | "2" | "34ms" | "@color{0f0}{● alive}"
row "c4e1::bridge-south" | "4" | "112ms" | "@color{0f0}{● alive}"
row "01ab::node-gamma" | "7" | "580ms" | "@color{f00}{○ stale}"
row "f390::hub-north" | "1" | "8ms" | "@color{0f0}{● alive}"
spacer
heading 1 "Actions"
box rounded "Quick Command"
form "cmd"
field "target" 30 "Destination hash..."
radio "mode" "Ping" | "Trace" | "Page"
checkbox "verbose" "Verbose output"
button "Execute" "/action/exec"
divider heavy
text "@center{© 2026 $node · Reticulum Network}"
```
### 5.2 ASCII Output
```
╔══ Relay Alpha-7 ═════════════════════════════════════════════════╗
║ Reticulum Network Node ║
║ Online 14d 3h 22m ║
╚══════════════════════════════════════════════════════════════════╝
── Resources ─────────────────────────────────────────────────────
CPU ████████████████████░░░░░░░░ 62% MEM █████████████████████████░░ 84% ⚠
GPU ██████░░░░░░░░░░░░░░░░░░░░░ 21% SWP █░░░░░░░░░░░░░░░░░░░░░░░░░ 3%
── Network ───────────────────────────────────────────────────────
Traffic (60s) Peers: 7 / 12
IN ⣀⣤⣶⣿⣷⣶⣤⣀⣀⣤⣶⣿⣷⣤ 4.2 KB/s East Relay ● online
OUT ⣀⣀⣠⣤⣶⣿⣷⣶⣤⣀⣀⣠⣤⣶⣿ 2.1 KB/s South Bridge ● online
Node Gamma ◐ degraded
── Routing ───────────────────────────────────────────────────────
┌────────────────────────┬────────┬──────────┬────────────┐
│ Destination │ Hops │ RTT │ State │
├────────────────────────┼────────┼──────────┼────────────┤
│ a7f2::relay-east │ 2 │ 34ms │ ● alive │
│ c4e1::bridge-south │ 4 │ 112ms │ ● alive │
│ 01ab::node-gamma │ 7 │ 580ms │ ○ stale │
│ f390::hub-north │ 1 │ 8ms │ ● alive │
└────────────────────────┴────────┴──────────┴────────────┘
── Actions ───────────────────────────────────────────────────────
╭─ Quick Command ──────────────────────────────────────────────╮
│ │
│ target: [ Destination hash...________________ ] │
│ mode: (•) Ping ( ) Trace ( ) Page │
│ [ ] Verbose output │
│ ┌───────────┐ │
│ │ Execute │ │
│ └───────────┘ │
╰──────────────────────────────────────────────────────────────╯
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
© 2026 Relay Alpha-7 · Reticulum Network
```
### 5.3 Micron Output
The **same characters** — every box corner, every bar segment, every
braille dot — with Micron tags added for color and interactivity:
```
╔══ `!Relay Alpha-7`! ═════════════════════════════════════════════╗
║`c Reticulum Network Node`a ║
║`c Online 14d 3h 22m`a ║
╚══════════════════════════════════════════════════════════════════╝
>Resources
`!CPU`! `F0f0████████████████████`f`F555░░░░░░░░`f 62% `!MEM`! `Ff80█████████████████████████`f`F555░░`f `Ff8084%`f ⚠
`!GPU`! `F0f0██████`f`F555░░░░░░░░░░░░░░░░░░░░░`f 21% `!SWP`! `F0f0█`f`F555░░░░░░░░░░░░░░░░░░░░░░░░░`f 3%
>Network
`!Traffic (60s)`! `!Peers:`! 7 / 12
IN `F0cf⣀⣤⣶⣿⣷⣶⣤⣀⣀⣤⣶⣿⣷⣤`f 4.2 KB/s East Relay `F0f0●`f online
OUT `F0cf⣀⣀⣠⣤⣶⣿⣷⣶⣤⣀⣀⣠⣤⣶⣿`f 2.1 KB/s South Bridge `F0f0●`f online
Node Gamma `Fff0◐`f degraded
>Routing
┌────────────────────────┬────────┬──────────┬────────────┐
│ `!Destination`! │ `!Hops`! │ `!RTT`! │ `!State`! │
├────────────────────────┼────────┼──────────┼────────────┤
│ a7f2::relay-east │ 2 │ 34ms │ `F0f0●`f alive │
│ c4e1::bridge-south │ 4 │ 112ms │ `F0f0●`f alive │
│ 01ab::node-gamma │ 7 │ 580ms │ `Ff00○`f stale │
│ f390::hub-north │ 1 │ 8ms │ `F0f0●`f alive │
└────────────────────────┴────────┴──────────┴────────────┘
>Actions
╭─ `!Quick Command`! ──────────────────────────────────────────╮
│ │
│ target: `<30|target`Destination hash...> │
│ mode: `<^|mode|ping|*`Ping> `<^|mode|trace`Trace> `<^|mode|page`Page>
│ `<?|verbose|yes`Verbose output> │
│ `[`!Execute`!`:/action/exec]
│ │
╰──────────────────────────────────────────────────────────────╯
-━
`c© 2026 Relay Alpha-7 · Reticulum Network`a
```
Note how the table borders, box corners, and gauge characters
are **byte-for-byte identical** in both outputs. Micron simply
interleaves its backtick tags around the characters that need
color or emphasis.
---
## 6. Intermediate Representation
### IR Node
```
IRNode:
type : NodeType
label : string | null
children : IRNode[]
styles : {
fg : string | null # 3-digit hex
bg : string | null
bold : bool
italic : bool
underline: bool
align : left | center | right
border : light | heavy | double | rounded | none
}
layout : {
width : int | pct | null
height : int | null
gap : int
pad : [top, right, bottom, left]
}
data : { # type-specific
value : number | null
max : number | null
warn : number | null
crit : number | null
values : number[] | null # sparkline, bar_v
state : enum | null # status indicator
options : string[] | null # radio, dropdown
columns : Column[] | null # table
rows : Row[] | null # table
link : string | null # destination
field : FieldMeta | null # form metadata
}
inline : InlineSpan[] # parsed @modifiers
```
### The CharGrid
```
CharGrid:
width : int
height : int
cells : Cell[height][width]
Cell:
char : char # visible character
style : CellStyle # for Micron emission
field : FieldMeta? # if this cell is part of a form field
link : string? # if this cell is clickable
CellStyle:
fg : string? # 3-digit hex
bg : string?
bold : bool
italic : bool
underline : bool
```
The layout engine fills the CharGrid. Both emitters read it.
The ASCII emitter ignores the style layer. The Micron emitter
scans for style transitions and inserts tags.
---
## 7. Rendering Pipeline
```
Phase 1: Parse
.uf source → token stream → IR tree
Variables resolved, components expanded.
Phase 2: Measure
Bottom-up pass: compute min/preferred width and height
for each node. Leaf nodes (text, gauge, field) report
their intrinsic sizes. Containers aggregate children.
Phase 3: Layout
Top-down pass: assign (x, y, w, h) to every node.
Row nodes divide width among columns.
Box nodes reserve border characters (1 char each side).
Phase 4: Paint
Depth-first traversal. Each node writes characters into
the CharGrid at its assigned position:
- Box: draw border chars, set title style
- Gauge: compute bar length, write █ and ░, set fg color
based on thresholds
- Sparkline: convert values to braille patterns
- Table: draw grid, write cell content, set header bold
- Form: write visual placeholders, attach FieldMeta
- Status: write indicator char, set color by state
Phase 5: Merge Borders
Post-pass: scan for adjacent border characters and replace
with correct junction characters (┬ ┴ ├ ┤ ┼ etc.).
Weight priority: double > heavy > light > rounded.
Phase 6: Emit
ASCII: read cell.char for every cell, join into lines.
Micron: scan each line, diff style between adjacent cells,
open/close Micron tags at transitions.
```
### Border Merging Detail
```
Before: After:
┌────┐┌────┐ ┌────┬────┐
│ ││ │ ──▶ │ │ │
└────┘└────┘ └────┴────┘
┌────────┐ ┌────────┐
│┌──────┐│ ├──────┐ │ (nested box shares
││ ││ ──▶ │ │ │ parent left edge)
│└──────┘│ ├──────┘ │
└────────┘ └────────┘
```
The merging pass checks each cell against its 4 neighbors
and selects from a lookup table of ~40 junction characters.
---
## 8. Character Reference
### Boxes
```
Light: ┌ ─ ┐ │ └ ┘ ├ ┤ ┬ ┴ ┼
Heavy: ┏ ━ ┓ ┃ ┗ ┛ ┣ ┫ ┳ ┻ ╋
Double: ╔ ═ ╗ ║ ╚ ╝ ╠ ╣ ╦ ╩ ╬
Rounded: ╭ ╮ ╰ ╯
Mixed: ╒ ╓ ╕ ╖ ╘ ╙ ╛ ╜ (light+double junctions)
```
### Blocks
```
Horizontal fill: █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ (full → 1/8)
Vertical fill: ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ (1/8 → full)
Shade: ░ ▒ ▓ █ (25% → 100%)
Quadrants: ▖ ▗ ▘ ▝ ▞ ▟ ▙ ▛ ▜ ▚
```
### Braille (sparklines, dot plots)
```
Range: U+2800U+28FF (256 patterns)
Each char = 2×4 dot matrix (2 cols × 4 rows)
Smooth curves: ⠀⣀⣠⣤⣴⣶⣾⣿⣷⣶⣤⣀⠀
```
### Indicators
```
Status: ● ○ ◐ ◑ ◒ ◓ ◌ ◉
Arrows: ▲ ▼ ◀ ▶ ← → ↑ ↓ ↗ ↘
Marks: ✓ ✗ ◆ ◇ ★ ☆ ⚠ ⚡
```
---
## 9. CLI
```bash
uframe render <file.uf> # emit ASCII (stdout) + .mu (file)
--ascii # ASCII only
--micron # .mu only
--ansi # ANSI colors in ASCII output
--width <int> # override page width
--out <dir> # directory for .mu output
uframe preview <file.uf> # live terminal preview
--watch # re-render on file change
uframe check <file.uf> # validate / lint
uframe deploy <file.uf> [dest] # render + copy to NomadNet pages dir
# default dest: ~/.nomadnetwork/storage/pages/
```
---
## 10. Standard Component Library
```
use std/dashboard
use std/filebrowser
use std/board
# Pre-built patterns:
dashboard.banner node_name uptime
dashboard.resources cpu mem gpu swap
dashboard.peers peer_list
dashboard.traffic in_data out_data
filebrowser.tree root_path
filebrowser.listing dir_path
board.recent count
board.compose action_path
nav.tabs items active
nav.breadcrumb path
chart.timeseries label values width
chart.compare items
chart.histogram bins
```
---
## 11. Future Directions
- **Responsive reflow** — breakpoints that stack `row` children
vertically at narrow widths
- **Themes** — `.uf-theme` files defining color palettes and
border preferences shared across pages
- **Animation** — frame-by-frame for live dashboards using
terminal cursor repositioning
- **Bidirectional** — parse existing `.mu` into `.uf` for editing
- **HTML export** — for web-based Reticulum browsers (rBrowser)
- **Shebang mode** — `#!/usr/bin/env uframe --micron` for
executable dynamic NomadNet pages
- **LSP** — editor support: highlighting, completion, live preview