Writing Your First Melker App

Build a QR Code Generator while learning core concepts step by step.

QR Code Generator Demo

What you'll learn:

1. Basic Structure

Every Melker app starts with a <melker> root element.

qr-code.melker
<melker>
  <title>QR Code Generator</title>

  <container>
    <text>Hello, Melker!</text>
  </container>
</melker>

Save this as qr-code.melker and run it:

melker qr-code.melker

Concepts introduced

  • <melker> - Root element (required)
  • <title> - Sets the terminal window title
  • <container> - Layout container (like a <div>)
  • <text> - Display text content

Reference: .melker file format

2. Layout with Flexbox

Melker uses flexbox for layout, just like CSS.

<melker>
  <title>QR Code Generator</title>

  <container style="width: 100%; height: 100%; padding: 2; display: flex; flex-direction: column; gap: 1; border: thin">
    <text style="font-weight: bold;">QR Code Generator</text>

    <container style="flex-direction: row; gap: 1; align-items: center;">
      <text>Text:</text>
      <text>[ input will go here ]</text>
    </container>

    <container style="flex: 1; display: flex; justify-content: center; align-items: center;">
      <text>[ QR code will go here ]</text>
    </container>

    <text>Type in the input field to generate a QR code</text>
  </container>
</melker>

Concepts introduced

  • style attribute with CSS-like properties
  • width: 100%; height: 100% - Fill available space
  • flex-direction: column - Stack children vertically
  • flex-direction: row - Arrange children horizontally
  • flex: 1 - Expand to fill remaining space
  • gap: 1 - Space between children (terminal rows/columns)
  • border: thin - Add a border

3. Adding an Input Field

Replace the placeholder with an actual input component.

<container style="flex-direction: row; gap: 1; align-items: center;">
  <text>Text:</text>
  <input
    id="text-input"
    placeholder="Enter text to encode..."
    style="flex: 1;"
  />
</container>

Concepts introduced

  • <input> - Single-line text input
  • id - Unique identifier for accessing from scripts
  • placeholder - Hint text shown when empty
  • style="flex: 1" - Input expands to fill available width

4. Handling Events

Add an event handler to respond to input changes.

<input
  id="text-input"
  placeholder="Enter text to encode..."
  style="flex: 1;"
  onChange="$melker.alert('You typed: ' + event.value)"
/>

Type something and press Enter - you'll see an alert dialog!

Concepts introduced

  • onChange - Event handler (fires when input value changes)
  • event.value - The current input value
  • $melker.alert() - Show a modal alert dialog

5. Adding a Script

For more complex logic, use a TypeScript script block.

<melker>
  <title>QR Code Generator</title>

  <script type="typescript">
    let currentText = 'Hello Melker!';

    export function updateText(text: string) {
      currentText = text;
      $melker.alert('Current text: ' + currentText);
    }
  </script>

  <container style="width: 100%; height: 100%; padding: 2; ...">
    <!-- ... -->
    <input
      id="text-input"
      placeholder="Enter text to encode..."
      style="flex: 1;"
      onChange="$app.updateText(event.value)"
    />
    <!-- ... -->
  </container>
</melker>

Concepts introduced

  • <script type="typescript"> - TypeScript code block
  • export function - Functions must be exported to be callable
  • $app.functionName() - Call exported functions from handlers
  • Script variables persist across function calls

Reference: Script context ($melker, $app)

6. Accessing Elements from Scripts

Use $melker.getElementById() to access and manipulate elements.

<script type="typescript">
  let currentText = 'Hello Melker!';

  export function updateText(text: string) {
    if (!text) text = ' ';
    currentText = text;

    // Update a text element to show the current value
    const display = $melker.getElementById('display');
    if (display) {
      display.setValue('Current: ' + currentText);
    }
  }
</script>

<!-- In your UI -->
<text id="display">Current: Hello Melker!</text>

Concepts introduced

  • $melker.getElementById(id) - Get an element by its ID
  • element.setValue(value) - Set the element's display value
  • Null checking with if (element) - Element may not exist

7. Async Initialization

Use an async script to run setup after the UI is ready.

<script type="typescript">
  let currentText = 'Hello Melker!';

  export async function init() {
    // Set initial input value
    const input = $melker.getElementById('text-input');
    if (input) {
      input.setValue(currentText);
    }
    $melker.render();
  }

  export function updateText(text: string) {
    currentText = text;
  }
</script>

<script type="typescript" async="ready">
  await $app.init();
</script>

Concepts introduced

  • async="ready" - Script runs after first render (UI is ready)
  • $melker.render() - Manually trigger a re-render
  • Separating initialization from function definitions

8. Displaying Images

Add an <img> element and load images dynamically.

<container style="flex: 1; display: flex; justify-content: center; align-items: center;">
  <img
    id="qr-img"
    width="fill"
    height="fill"
    style="object-fit: contain;"
    dither="none"
  />
</container>

Load images from scripts using setSrc():

export async function updateQR(text: string) {
  const img = $melker.getElementById('qr-img');
  await img?.setSrc('path/to/image.png');
  // Or use a data URL:
  // await img?.setSrc('data:image/gif;base64,...');
}

Concepts introduced

  • <img> - Image display component
  • width="fill", height="fill" - Fill available space
  • style="object-fit: contain;" - Maintain aspect ratio
  • element.setSrc(url) - Load an image (async, last call wins)

Reference: Component reference

9. Using npm Packages

Import npm packages directly with the npm: prefix.

import encodeQR from 'npm:qr';

export async function updateQR(text: string) {
  const gifBytes = encodeQR(text, 'gif');
  const base64 = btoa(String.fromCharCode(...gifBytes));
  const dataUrl = `data:image/gif;base64,${base64}`;

  const img = $melker.getElementById('qr-img');
  await img?.setSrc(dataUrl);
}

Concepts introduced

  • import ... from 'npm:package' - Import npm packages
  • Generating data URLs for dynamic images

10. Adding Permissions with Policy

Declare what your app needs so users can review it before running.

<melker>
  <title>QR Code Generator</title>

  <policy>
  {
    "name": "QR Code Generator",
    "description": "Generate QR codes from text input",
    "permissions": {
      "net": ["registry.npmjs.org", "cdn.jsdelivr.net"]
    }
  }
  </policy>

  <!-- rest of app -->
</melker>

Concepts introduced

  • <policy> - JSON permission declaration
  • permissions.net - Network access to specific hosts
  • Users approve permissions on first run

Deep dive: The Policy System

Complete App

Here's the final QR Code Generator with all pieces together.

qr-code.melker
<melker>
  <title>QR Code Generator</title>

  <policy>
  {
    "name": "QR Code Generator",
    "description": "Generate QR codes from text input",
    "permissions": {
      "net": ["registry.npmjs.org", "cdn.jsdelivr.net"]
    }
  }
  </policy>

  <script type="typescript">
    import encodeQR from 'npm:qr';

    let currentText = 'Hello Melker!';

    export async function updateQR(text: string) {
      if (!text) {
        text = ' '; // QR needs at least something
      }
      currentText = text;

      try {
        const gifBytes = encodeQR(text, 'gif');
        const base64 = btoa(String.fromCharCode(...gifBytes));
        const dataUrl = `data:image/gif;base64,${base64}`;

        const img = $melker.getElementById('qr-img');
        await img?.setSrc(dataUrl);
      } catch (err) {
        $melker.logger?.error('QR generation failed', err);
      }
    }

    export async function init() {
      const input = $melker.getElementById('text-input');
      if (input) {
        input.setValue(currentText);
      }
      await updateQR(currentText);
      $melker.render();
    }
  </script>

  <script type="typescript" async="ready">
    await $app.init();
  </script>

  <container style="width: 100%; height: 100%; padding: 2; display: flex; flex-direction: column; gap: 1; border: thin">
    <text style="font-weight: bold;">QR Code Generator</text>

    <container style="flex-direction: row; gap: 1; align-items: center;">
      <text>Text:</text>
      <input
        id="text-input"
        placeholder="Enter text to encode..."
        style="flex: 1;"
        onChange="$app.updateQR(event.value)"
      />
    </container>

    <container style="flex: 1; display: flex; justify-content: center; align-items: center;">
      <img
        id="qr-img"
        width="fill"
        height="fill"
        style="object-fit: contain;"
        dither="none"
      />
    </container>

    <text>Type in the input field to generate a QR code</text>
  </container>
</melker>

Key Takeaways

Concept Example
Root element<melker>...</melker>
Window title<title>My App</title>
Layout<container style="flex-direction: column">
Stylingstyle="border: thin; padding: 1; gap: 1"
Text input<input id="myInput" onChange="..." />
Event handlersonChange="$app.myFunction(event.value)"
Scripts<script type="typescript">export function ...
Async init<script async="ready">await $app.init()</script>
Get elements$melker.getElementById('id')
Set valueselement.setValue('text')
Load imagesawait img.setSrc('url')
Manual render$melker.render()
Logging$melker.logger?.error('message', err)
Permissions<policy>{"permissions": {"net": [...]}}</policy>
npm packagesimport pkg from 'npm:package'

Next Steps