How Melker Works

A terminal app you can inspect before you run. This page uses a live earthquake dashboard to show how a single .melker file becomes a real application: UI, data fetching, charts, maps, and a permission sandbox, all in one readable document.

Melker earthquake dashboard running in a terminal
$ melker https://melker.sh/examples/showcase/earthquake-dashboard.melker
Melker runs .melker documents from local files or URLs.

1. It's just a document

No build step. A .melker file is a document you can read top to bottom.

The earthquake dashboard is a single file. It opens with a <melker> root tag, just like an HTML page opens with <html>. Inside: a title, a permission policy, a help page, some styles, a component tree, and a script. That's the entire application.

earthquake-dashboard.melker
<melker>
    <title>Earthquake Dashboard</title>
    <policy>  /* what the app is allowed to do */  </policy>
    <help>    /* built-in help page (markdown) */  </help>
    <style>   /* CSS you already know */           </style>

    <container style="display: flex; flex-direction: column; height: 100%">
        <!-- header, table, map, charts -->
    </container>

    <script type="typescript">
        // fetch data, update UI
    </script>
</melker>

No package.json, no tsconfig.json, no webpack.config.js. The file is the app. Send it to someone and they run it with one command.

Reference: .melker file format

2. Safe to run

The app declares what it needs. You can see it before you run it.

Before any code runs, the <policy> block declares the app's permissions. This dashboard needs network access to two hosts and map tile access. Nothing else.

Permission policy
<policy>
  {
    "name": "earthquake-dashboard",
    "description": "Real-time USGS earthquake monitor",
    "permissions": {
      "net": ["earthquake.usgs.gov", "raw.githubusercontent.com"],
      "map": true
    }
  }
</policy>
What this means: The app can fetch data from earthquake.usgs.gov and raw.githubusercontent.com (for tectonic plate boundary data). It can load map tiles. It cannot read files, run commands, or access any other network host. If the script tries to fetch('https://evil.com'), it fails.

The policy tells you what the app can do, and the engine enforces it. Press F12 to open Dev Tools and inspect the running app's permissions, element tree, and state.

Deep dive: The Policy System

3. Why a document format?

Terminal apps lack what browsers take for granted: view source, declared permissions, a sandbox.

You can build terminal UIs with libraries like blessed, Ink, or Textual. You can build dashboards as local web apps. Both work. But neither gives the person running the app a way to know what it does before it runs. There's no permission manifest, no sandbox, no structured inspection.

A .melker file is a document, not a program. You can read its policy, scan its markup, and understand its scope. The engine enforces the declared permissions at runtime. The file is also the distribution unit — share a URL, and the recipient runs it directly. No install, no dependency tree, no build.

The trade-off: Melker apps run inside the engine. You get sandboxing, built-in components, and zero-install distribution. You give up direct system access and the full flexibility of a general-purpose runtime. If you need unrestricted filesystem access or your own TCP server, Melker is not the right tool.

Read more: Manifesto

4. Built-in components

No dependencies to install. A sortable table and a zoomable map are each one element.

Data table showing earthquake list Tile map with earthquake markers

With a TUI library, you'd install a table package, a map package, wire up keyboard handling, and hope the pieces work together. In Melker, they're built in. A data table with sorting, selection, scrolling, tooltips, and keyboard navigation is one element:

Data table and tile map
<data-table id="quakeTable" selectable="single"
    sortColumn="0" sortDirection="desc" tooltip="auto"
    bind:selection="selectedQuake"
    onActivate="$app.showDetails(event)">
    { "columns": [
        { "header": "Mag", "width": 6, "align": "right" },
        { "header": "Location", "width": "fill" },
        { "header": "Depth km", "width": 10, "align": "right" },
        { "header": "Time (UTC)", "width": 20 }
    ]}
</data-table>

<tile-map id="worldMap" lat="20" lon="0" zoom="1"
    provider="voyager-nolabels" interactive="true"
    onOverlay="$app.drawOverlay(event)"
    onClick="$app.mapClick(event)"/>

The table handles sorting, selection, scrolling, and keyboard navigation. The map handles tile fetching, caching, Mercator projection, drag, and zoom. Over 30 components are built in: inputs, dialogs, charts, file browser, split panes, and more.

Reference: Data table · Tile map · All components

5. Data and script

The script block is TypeScript. Fetch an API, populate the UI.

The <script> block runs in a sandboxed context with access to $melker (the engine API) and standard web APIs like fetch. Here the dashboard fetches USGS earthquake data and populates the table:

Fetching data and populating the table
const res = await fetch(FEEDS[currentFeed]);
const json = await res.json();

const table = $melker.getElementById('quakeTable');
table.setValue(json.features.map(f => [
  f.properties.mag.toFixed(1),
  f.properties.place,
  f.geometry.coordinates[2].toFixed(1),
  fmtTime(f.properties.time),
  f.id,           // hidden column for selection binding
]));

setValue passes a 2D array of rows to the table. A dropdown lets users switch between eight USGS feeds (past hour, past day, past week, etc.) and the data auto-refreshes every 60 seconds.

Reference: Script context ($melker, $app)

6. CSS layout

Layout uses the CSS you already know: flexbox, borders, padding.

The dashboard uses flexbox and split panes. No new layout language to learn — if you know CSS, you know how to lay out a Melker app:

Layout structure
<!-- Outer: table+sparkline (3/4) | map+summary (1/4) -->
<split-pane sizes="3,1">

  <!-- Left: table fills space, sparkline fixed at bottom -->
  <container style="display: flex; flex-direction: column">
    <data-table style="flex: 1" .../>
    <data-bars style="height: 3; flex-shrink: 0" .../>
  </container>

  <!-- Right: map (1/4) | summary+charts (3/4) -->
  <split-pane style="direction: vertical" sizes="1,3">
    <tile-map .../>
    <container> <!-- stats, mag dist, depth dist --> </container>
  </split-pane>

</split-pane>

Dimensions are in terminal cell units (not pixels). Split panes support proportional sizing and interactive resizing by dragging the divider.

Reference: Split pane

7. Wiring it together

State binding, keyboard shortcuts, toasts, and dialogs — with minimal glue code.

The table and map need to stay in sync: select an earthquake in either one, and the other follows. A reactive state object handles this — one line to create it, one attribute to bind the table's selection to it:

Shared state and two-way binding
// Create reactive state
const state = $melker.createState({ selectedQuake: [] as string[] });

// Table binds to it (in markup: bind:selection="selectedQuake")
// Map click updates it:
export function mapClick(event) {
  const nearest = findNearestQuake(event.lat, event.lon);
  if (nearest) state.selectedQuake = [nearest.id];
}

Keyboard commands, toast notifications, and detail dialogs are similarly concise. Each is declared or called in one or two lines:

Commands, toasts, and dialogs
<!-- Markup: keyboard shortcuts -->
<command key="r" label="Refresh Data" global onExecute="$app.refresh()" />
<command key="t" label="Toggle Plates" global onExecute="$app.togglePlates()" />

// Script: toast on new earthquakes
$melker.toast.show(newCount + ' new earthquake(s) detected',
  { type: 'warning', duration: 5000 });

// Script: detail dialog on Enter
$melker.alert('M' + q.mag.toFixed(1) + ' — ' + q.place + '\n' + details);

The command palette (Ctrl+K) lists all registered shortcuts. Tooltips appear on hover over both the map and the sparkline bars.

Reference: State binding · Commands · Toast

8. Map customization

Color key filtering, canvas overlays, and SVG geo-data — all configured through CSS and callbacks.

Color-keyed map with earthquake markers and tectonic plates

The dashboard applies a color key filter to separate water from land, making markers easier to read. The filter is declared in CSS — no code required:

Map styling with color key filter
<style>
  tile-map {
    tile-key-color: #abd0e0;         /* Water color on Voyager maps */
    tile-key-threshold: 0.04;
    tile-key-match-color: #2a4a6b;   /* Water becomes muted blue */
    tile-key-other-color: #c8c8c8;   /* Land becomes light gray */
    tile-blur: 1;                    /* Smooth rendering artifacts */
  }
</style>

The onOverlay callback draws earthquake markers as colored dots, with a white ring around the selected one. Tectonic plate boundaries are loaded as GeoJSON, converted to SVG paths, and projected onto the map automatically:

Canvas overlay
export function drawOverlay(event) {
  const { canvas, geo } = event;
  for (const q of quakes) {
    const pos = geo.latLonToPixel(q.lat, q.lon);
    if (pos) canvas.fillCircleCorrectedColor(pos.x, pos.y, 1, magColor(q.mag));
  }
}

// Tectonic plates as SVG overlay (cached to disk after first fetch)
map.props.svgOverlay = platePathsString;

Reference: Tile map · SVG overlays

9. Charts and stats

Sparklines, distribution charts, and summary stats. Declare a chart, feed it data.

Magnitude sparkline

Magnitude sparkline below the table.

Distribution charts and summary stats

Summary stats and magnitude/depth distribution charts.

Each chart is a <data-bars> element. Declare it in markup, then pass it data:

Sparkline declaration and data
<!-- Markup -->
<data-bars id="magSparkline" min="0"
    showValues="false" showLabels="false"
    onTooltip="$app.sparklineTooltip(event)"
    style="orientation: vertical; height: 3; gap: 0"/>

// Script: feed it the 40 most recent earthquakes
sparkline.setValue(quakes.slice(0, 40).reverse().map(q => [q.mag]));

The same pattern applies to the distribution charts and summary stats: declare the element, pass it data. Everything updates when the feed refreshes.

Reference: Data bars

10. One file, one command

Share the file or the URL. No install beyond the runtime.

Everything on this page — the sandboxed permission policy, color-keyed map, sortable table, charts, state binding, keyboard shortcuts, toast notifications, and tectonic plate overlays — lives in one file. No dependencies, no build step.

View the full source  ·  Follow the tutorial  ·  Back to home

Installation

Deno (recommended)
deno install -g -A jsr:@melker/melker
Node.js (experimental)
npm install -g @melker/melker

Then run any .melker document from a file or URL:

# Deno
melker https://melker.sh/examples/showcase/earthquake-dashboard.melker

# Node.js (binary is melker-node)
melker-node https://melker.sh/examples/showcase/earthquake-dashboard.melker

See README for all installation methods.

11. AI Assistant

Optional AI assistant, fully opt-in. Nothing is sent anywhere unless you configure it.

Press F8 in any running app to open the assistant. It requires an API key you provide and only connects when you explicitly invoke it. It works with any LLM provider (Anthropic, OpenAI, Ollama, etc.) and supports voice input.

This is possible because Melker apps are inspectable by design. The same structured element tree and declared actions that let you read a .melker file before running it also let an AI understand and operate the running UI. The assistant receives the visible screen content, the element tree with IDs and props, and available keyboard actions.

Learn more about the AI assistant