Complete developer and user documentation for
halpradio's sandboxed WebAssembly (Wasm) plugin engine and official curated registry.
halpradio is designed to be lightweight and blazingly fast in your terminal. Rather than bloating the core binary with every third-party service, halpradio provides a military-grade WebAssembly Plugin Architecture that lets you extend the player with custom integrations:
- 📡 Webhooks & Chat Bots: Broadcast now-playing tracks to Discord, Slack, or Telegram in real-time.
- 🏠 Home Automation: Sync Philips Hue or Home Assistant smart lights to radio state changes.
- 📊 Scrobblers & Stats: Track listening habits, top stations, and listening time locally or cloud-synced.
- 🎧 Hardware Controllers: Hook into stream changes for Stream Decks, Raspberry Pi LCD screens, or LED strips.
- 🎨 Visualizers & Lyrics: Fetch live song lyrics or create custom ASCII animations.
All plugins execute inside an isolated WebAssembly Virtual Machine with zero unauthorized access to your filesystem, network, or OS processes.
halpradio uses tetratelabs/wazero, a high-performance, pure Go WebAssembly runtime with zero CGo dependencies.
┌─────────────────────────────────────────────────────────────┐
│ halpradio Core TUI │
│ (Bubble Tea Event Loop • Audio Player • Stations) │
└──────────────────────────────┬──────────────────────────────┘
│ Asynchronous Event Dispatch
▼
┌─────────────────────────────────────────────────────────────┐
│ Wazero Sandboxed WebAssembly Engine │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Guest Plugin VM: "webhook-broadcaster" │ │
│ │ • Isolated Memory (max 16 MB) │ │
│ │ • Non-blocking execution (timeout 2s) │ │
│ └───────────────────────────┬───────────────────────────┘ │
│ │ Capability Checked API Calls │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Host Security Gatekeeper (manifest.yaml enforcement) │ │
│ │ • Network: Check domain against whitelist │ │
│ │ • Storage: Sandboxed directory only │ │
│ │ • User Permission Approval Check │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Plugins run in a strict sandbox:
- No filesystem access outside the plugin's dedicated data directory.
- No network access unless explicit domains or CIDRs are declared in
manifest.yamland approved by the user. - No environment variable or process execution capabilities.
When a plugin is installed, halpradio presents an explicit permission prompt showing requested capabilities (e.g. 🌐 Network: api.discord.com, 💾 Storage: Local Sandbox). The plugin remains disabled until you approve it.
Press P (or <space>p) anywhere in halpradio to open the Plugin & Extension Manager:
[1] 📦 InstalledTab:Space/Enter: Toggle plugin enable/disable.p: View requested permissions & toggle approval status.dorx: Uninstall plugin and clean up data.u: Check and apply updates from the official registry.
[2] 🌐 Official RegistryTab:Tab/1/2/h/l: Switch between Installed and Registry views.i/Enter: Download and install a plugin from the official registry.Esc/q: Close the modal.
You can also manage plugins directly from your command line:
# List all installed plugins and available registry plugins
halpradio plugin list
# Install a plugin from the official registry
halpradio plugin install webhook-broadcaster
halpradio plugin install scrobble-logger
# Enable or disable a plugin
halpradio plugin enable webhook-broadcaster
halpradio plugin disable webhook-broadcaster
# Update installed plugins to the latest release
halpradio plugin update --all
# Remove a plugin
halpradio plugin remove webhook-broadcaster| Resource | Path | Description |
|---|---|---|
| Installed Plugins | ~/.config/halpradio/plugins/<plugin-id>/ |
Contains manifest.yaml and plugin.wasm. |
| Plugin Data Storage | ~/.config/halpradio/plugins_data/<plugin-id>/ |
Sandboxed persistent data (storage.json). |
| Plugin State Registry | ~/.config/halpradio/plugins.json |
Stores enabled states & permission approvals. |
Plugins can be authored in any language that compiles to WebAssembly (.wasm), including Go, Rust, TypeScript/AssemblyScript, Zig, and C.
Create a directory my-awesome-plugin with a manifest.yaml:
id: "my-awesome-plugin"
name: "My Awesome Plugin"
version: "1.0.0"
author: "your_name"
description: "Broadcasts song changes or logs listening history."
homepage: "https://github.com/yourname/my-awesome-plugin"
wasm_file: "plugin.wasm"
permissions:
network:
- "*.discord.com" # Domain whitelist (or "*" for all)
storage:
- "local" # Sandboxed storage permission
events:
- "on_track_change" # Lifecycle events to listen for
- "on_playback_change"Using Go 1.21+ and the official halpradiosdk:
package main
import (
"fmt"
"unsafe"
"github.com/halpworld/halpradio-plugins/sdk/go/halpradiosdk"
)
// Required entry point for wasip1
func main() {}
var memoryBuffer []byte
//export alloc
func alloc(size uint32) uint32 {
if size == 0 {
return 0
}
memoryBuffer = make([]byte, size)
return uint32(uintptr(unsafe.Pointer(&memoryBuffer[0])))
}
//export free
func free(ptr uint32, size uint32) {
memoryBuffer = nil
}
//export on_init
func onInit(ptr uint32, length uint32) uint32 {
halpradiosdk.Log(halpradiosdk.LogLevelInfo, "My Awesome Plugin initialized!")
return 0
}
//export on_track_change
func onTrackChange(ptr uint32, length uint32) uint32 {
if length == 0 {
return 0
}
data := unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), length)
payload, err := halpradiosdk.ParseTrackChange(data)
if err != nil {
return 1
}
// 1. Log to halpradio logs
halpradiosdk.Log(halpradiosdk.LogLevelInfo, fmt.Sprintf("Now playing: %s - %s", payload.Artist, payload.Title))
// 2. Flash a message on halpradio's status bar
halpradiosdk.Flash(fmt.Sprintf("🎵 %s", payload.Title))
// 3. Save to sandboxed local storage
_ = halpradiosdk.StorageSet("last_song", payload.Title)
return 0
}
//export on_playback_change
func onPlaybackChange(ptr uint32, length uint32) uint32 {
if length == 0 {
return 0
}
data := unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), length)
payload, err := halpradiosdk.ParsePlaybackChange(data)
if err != nil {
return 1
}
halpradiosdk.Log(halpradiosdk.LogLevelDebug, fmt.Sprintf("Playback status: %s", payload.Status))
return 0
}Compile directly using Go's built-in wasip1 compiler target (zero external tools needed):
GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -o plugin.wasm main.go- Create a plugin directory in your local config:
mkdir -p ~/.config/halpradio/plugins/my-awesome-plugin - Copy
manifest.yamlandplugin.wasm:cp manifest.yaml plugin.wasm ~/.config/halpradio/plugins/my-awesome-plugin/ - Start
halpradio, pressP, selectMy Awesome Plugin, and pressporSpaceto approve permissions and enable it!
Rust developers can compile plugins using wasm32-wasip1:
// Cargo.toml
// [lib]
// crate-type = ["cdylib"]
#[link(wasm_import_module = "halpradio")]
extern "C" {
fn log(level: u32, ptr: *const u8, len: u32);
fn ui_flash(ptr: *const u8, len: u32);
}
#[no_mangle]
pub extern "C" fn on_track_change(ptr: *const u8, len: u32) -> u32 {
let msg = "Hello from Rust plugin!";
unsafe {
ui_flash(msg.as_ptr(), msg.len() as u32);
}
0
}Build with:
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/my_plugin.wasm plugin.wasmThe halpradio runtime exposes the following host functions to guest WebAssembly modules under the "halpradio" namespace:
| Function | Signature | Capability Required | Description |
|---|---|---|---|
halpradio.log |
(level: u32, ptr: u32, len: u32) |
None | Emits a log entry (0: debug, 1: info, 2: warn, 3: error). |
halpradio.ui_notify |
(title_ptr: u32, title_len: u32, msg_ptr: u32, msg_len: u32) |
None | Triggers a desktop and/or in-app notification banner. |
halpradio.ui_flash |
(msg_ptr: u32, msg_len: u32) |
None | Shows a temporary status message in the bottom bar. |
halpradio.storage_get |
(key_ptr: u32, key_len: u32, out_ptr: u32, max_len: u32) -> u32 |
storage: ["local"] |
Reads a string value from sandboxed storage.json. Returns bytes written. |
halpradio.storage_set |
(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32) -> u32 |
storage: ["local"] |
Writes a key-value pair to sandboxed storage.json. Returns 0 on success. |
halpradio.http_fetch |
(url_ptr: u32, url_len: u32, method_ptr: u32, method_len: u32, body_ptr: u32, body_len: u32, out_ptr: u32, max_len: u32) -> u32 |
network: ["domain"] |
Performs an HTTP/HTTPS request against a permitted URL. Returns bytes written. |
Plugins can export any combination of the following lifecycle hooks:
| Hook | Signature | Trigger Event | Payload Format |
|---|---|---|---|
on_init |
(cfg_ptr: u32, cfg_len: u32) -> u32 |
Plugin loaded at app launch | JSON object of plugin settings |
on_track_change |
(ptr: u32, len: u32) -> u32 |
New song / ICY stream metadata | {"station": "...", "artist": "...", "title": "...", "bitrate": 128, "codec": "MP3", "timestamp": "..."} |
on_playback_change |
(ptr: u32, len: u32) -> u32 |
Play / Pause / Stop / Volume | {"status": "playing", "volume": 80, "backend": "mpv", "station": "..."} |
on_timer_tick |
(ptr: u32, len: u32) -> u32 |
Timer / Pomodoro progress tick | {"mode": "pomodoro", "state": "focus", "remaining_seconds": 1500, "total_seconds": 1500} |
alloc / malloc |
(size: u32) -> u32 |
Memory buffer allocation | Host calls this to allocate memory before passing payloads |
free / dealloc |
(ptr: u32, size: u32) |
Memory buffer deallocation | Host calls this after hook execution completes |
Want to share your plugin with the community?
- Fork
halpworld/halpradio-plugins. - Add your plugin folder under
plugins/<your-plugin-id>/containing:manifest.yaml- Source code (
main.go,src/, etc.) MakefileREADME.md- Compiled
plugin.wasm
- Calculate your binary's SHA-256 checksum:
shasum -a 256 plugin.wasm
- Add your plugin's metadata to
registry.json. - Open a Pull Request! Our automated CI will verify your build and security checks.