Examples
Complete, copy-pasteable modules. Each is a folder with a manifest.json + index.html + index.js; the
index.html is always the same one-liner from the overview, so only the
manifest and JS are shown.
Now-playing widget
A pinned widget that shows the current track and updates live. Needs only data:nowPlaying.
{
"id": "com.you.nowplaying",
"name": "Now Playing",
"version": "1.0.0",
"permissions": ["data:nowPlaying"],
"entry": "index.html",
"surfaces": [{ "kind": "widget", "id": "np", "gated": false }]
}
import { activate } from '@readout/sdk'
activate((ps) => {
ps.registerWidget({
id: 'np',
gated: false,
render(root) {
root.style.padding = '8px 12px'
root.style.font = '600 13px var(--ps-font)'
return ps.host.nowPlaying.subscribe((t) => {
root.textContent = t && t.status !== 'stopped' ? `♪ ${t.title} — ${t.artist}` : 'Nothing playing'
})
},
})
})
Weather widget (host.fetch)
Fetches from an allow-listed API through the host proxy. Needs net:fetch + allowedDomains.
{
"id": "com.you.weather",
"name": "Weather",
"version": "1.0.0",
"permissions": ["net:fetch"],
"allowedDomains": ["api.open-meteo.com"],
"entry": "index.html",
"surfaces": [{ "kind": "widget", "id": "wx", "gated": false }]
}
import { activate } from '@readout/sdk'
const URL =
'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=temperature_2m'
activate((ps) => {
ps.registerWidget({
id: 'wx',
gated: false,
render(root) {
root.style.padding = '8px 12px'
root.textContent = 'Loading...'
ps.host
.fetch(URL)
.then((res) => {
const data = JSON.parse(res.body)
root.textContent = `${Math.round(data.current.temperature_2m)}°C`
})
.catch(() => (root.textContent = 'Weather unavailable'))
},
})
})
Settings + widget sharing state (storage + reload)
A settings section writes a preference; the widget reads it. ps.reload() re-mounts surfaces so the widget
picks up the change. Storage needs no permission.
{
"id": "com.you.clock",
"name": "Clock",
"version": "1.0.0",
"permissions": [],
"entry": "index.html",
"surfaces": [
{ "kind": "widget", "id": "clock", "gated": false },
{ "kind": "settings", "id": "prefs", "label": "Clock", "icon": "gauge", "order": 220 }
]
}
import { activate } from '@readout/sdk'
activate((ps) => {
// Widget: read the saved preference inside render(), then tick.
ps.registerWidget({
id: 'clock',
gated: false,
async render(root) {
const h24 = (await ps.host.storage.get('h24')) ?? false
const tick = () => {
root.textContent = new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: !h24,
})
}
tick()
const id = setInterval(tick, 1000)
return () => clearInterval(id) // cleanup on unmount
},
})
// Settings: write the preference, then reload so the widget re-reads it.
ps.registerSettings({
id: 'prefs',
title: 'Clock',
async render(root) {
const h24 = (await ps.host.storage.get('h24')) ?? false
const label = document.createElement('label')
const cb = document.createElement('input')
cb.type = 'checkbox'
cb.checked = h24
cb.addEventListener('change', async () => {
await ps.host.storage.set('h24', cb.checked)
ps.reload() // re-mount all surfaces with the new setting
})
label.append(cb, document.createTextNode(' 24-hour time'))
root.append(label)
},
})
})
A window with a dock button
{
"id": "com.you.notes",
"name": "Quick Notes",
"version": "1.0.0",
"permissions": [],
"entry": "index.html",
"surfaces": [{ "kind": "window", "id": "main", "label": "Notes", "icon": "notes", "order": 200 }]
}
import { activate } from '@readout/sdk'
activate((ps) => {
ps.registerWindow({
kind: 'main', // must equal the surface id
title: 'Quick Notes',
initial: { w: 320, h: 240 },
launcher: { icon: 'notes', label: 'Notes', order: 200 },
async render(root) {
const ta = document.createElement('textarea')
ta.style.cssText = 'width:100%;height:100%;background:transparent;color:var(--ps-foreground);border:0;resize:none;outline:none;font:13px var(--ps-font)'
ta.value = (await ps.host.storage.get('text')) ?? ''
ta.addEventListener('input', () => ps.host.storage.set('text', ta.value))
root.append(ta)
},
})
})