Build a QR Code Generator while learning core concepts step by step.
What you'll learn:
Every Melker app starts with a <melker> root element.
<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
<melker> - Root element (required)<title> - Sets the terminal window title<container> - Layout container (like a <div>)<text> - Display text contentReference: .melker file format
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>
style attribute with CSS-like propertieswidth: 100%; height: 100% - Fill available spaceflex-direction: column - Stack children verticallyflex-direction: row - Arrange children horizontallyflex: 1 - Expand to fill remaining spacegap: 1 - Space between children (terminal rows/columns)border: thin - Add a borderReplace 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>
<input> - Single-line text inputid - Unique identifier for accessing from scriptsplaceholder - Hint text shown when emptystyle="flex: 1" - Input expands to fill available widthAdd 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!
onChange - Event handler (fires when input value changes)event.value - The current input value$melker.alert() - Show a modal alert dialogFor 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>
<script type="typescript"> - TypeScript code blockexport function - Functions must be exported to be callable$app.functionName() - Call exported functions from handlersReference: Script context ($melker, $app)
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>
$melker.getElementById(id) - Get an element by its IDelement.setValue(value) - Set the element's display valueif (element) - Element may not existUse 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>
async="ready" - Script runs after first render (UI is ready)$melker.render() - Manually trigger a re-renderAdd 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,...'); }
<img> - Image display componentwidth="fill", height="fill" - Fill available spacestyle="object-fit: contain;" - Maintain aspect ratioelement.setSrc(url) - Load an image (async, last call wins)Reference: Component reference
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); }
import ... from 'npm:package' - Import npm packagesDeclare 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>
<policy> - JSON permission declarationpermissions.net - Network access to specific hostsDeep dive: The Policy System
Here's the final QR Code Generator with all pieces together.
<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>
| Concept | Example |
|---|---|
| Root element | <melker>...</melker> |
| Window title | <title>My App</title> |
| Layout | <container style="flex-direction: column"> |
| Styling | style="border: thin; padding: 1; gap: 1" |
| Text input | <input id="myInput" onChange="..." /> |
| Event handlers | onChange="$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 values | element.setValue('text') |
| Load images | await img.setSrc('url') |
| Manual render | $melker.render() |
| Logging | $melker.logger?.error('message', err) |
| Permissions | <policy>{"permissions": {"net": [...]}}</policy> |
| npm packages | import pkg from 'npm:package' |
<button>, <checkbox>, <select>, <dialog>, <tabs>