segfault labs

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&current=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)
    },
  })
})