01 — Hello World
If you can write a Go program that prints to stdout, you can write a lofigui app. The model function below is ordinary Go code — Print(), a loop, a sleep. The only difference is the output goes to a web page instead of a terminal. No WebSocket, no JavaScript — just the browser's built-in refresh mechanism doing the work.
The model — your application logic
A lofigui app has two parts: a model that does the work and a server that wires it to the web. This is the model:
// Model function - this is your application logic.
// Just like a terminal program: print output and sleep between steps.
func model(app *lofigui.App) {
lofigui.Print("Hello world.")
for i := 0; i < 5; i++ {
app.Sleep(1 * time.Second)
lofigui.Printf("Count %d", i)
}
lofigui.Print("Done.")
}
fmt.Println() — each call adds a line of output. The difference: instead of writing to the terminal, it appends HTML to a buffer that the browser displays.
Handle calls StartAction() before launching the model goroutine, which enables auto-refresh polling. When the model function returns, Handle calls EndAction() automatically — the browser stops refreshing and the output stays put.
The server — wiring it up
Two lines: create an app, run the model.
func main() {
app := lofigui.NewApp()
app.Run(":1340", model)
}
/, a cancel handler on /cancel, and starts the server with graceful shutdown. When the model completes, the server exits. This is the HTTP equivalent of RunWASM — one call does everything.
NewApp() provides a built-in template (Bulma-styled navbar with cancel button), 1-second refresh, and a /favicon.ico handler. Later examples unbundle Run into Handle, HandleCancel, and ListenAndServe when they need custom routes or multiple endpoints.
The full source is split across two files: main.go (the server) and model.go (the application logic). The model is in its own file so it can be shared with the WASM build — if you don't need a WASM version, a single main.go is all you need.
How it works
The browser hits /, the server starts the model and returns a page with a Refresh header. The browser reloads every second, showing new output as the model prints. When the model returns, polling stops and the server exits cleanly.
See technical details for a full sequence diagram and internals.
Cancellation
Both the server and WASM builds support cancelling a running model mid-flow. The navbar shows a Cancel button while the model is running. app.Run() handles this automatically; when unbundled, register the cancel endpoint explicitly:
// Unbundled form (used in later examples with custom routes):
http.HandleFunc("/cancel", app.HandleCancel("/"))
// WASM: goCancel() is exported automatically by RunWASM
EndAction() cancels the context. The next call to Print, Sleep, or Yield in the model goroutine panics with an internal sentinel. Handle's recover wrapper catches it, and the goroutine exits cleanly. The buffer retains its partial output. The model doesn't need any explicit cancellation code.
See technical details for the full cancel flow.
WASM: running in the browser
The live demo runs the same model() function compiled to WebAssembly — entirely in your browser, no server required. Because the model lives in its own file (model.go), both the server and WASM builds share it unchanged.
A separate main_wasm.go file (build-tagged js && wasm) replaces the server with a single call:
//go:build js && wasm
package main
import "codeberg.org/hum3/lofigui"
func main() { lofigui.RunWASM(model) }
goStart(), goCancel(), goRender(), and goIsRunning() — then calls wasmReady() and blocks. A small JavaScript timer calls goRender() every 500ms to update the page. For apps that need custom JS exports (extra buttons, multiple render functions), write the wiring by hand instead — see examples 07-12.
GOOS=js GOARCH=wasm go build -o main.wasm . produces the binary. Go provides wasm_exec.js as a loader. The Taskfile.yml docs:build-wasm task automates this for all examples.
TinyGo WASM
The same code also compiles with TinyGo for a smaller binary — no source changes needed:
tinygo build -o main-tinygo.wasm -target wasm .
| Build | Binary size | Loader |
|---|---|---|
| Go WASM | ~8 MB | wasm_exec.js (from Go) |
| TinyGo WASM | ~2.4 MB | wasm_exec.js (from TinyGo) |
wasm_exec.js. They are not interchangeable. The docs:build-wasm task builds both and generates separate demo pages: Go WASM demo and TinyGo WASM demo.
Every lofigui example builds with both Go and TinyGo. The model.go is identical — only the compiler and loader differ.
Server vs WASM lifecycle
The server app and WASM app run the same model, but their lifecycles differ:
- Server — the model starts automatically on the first HTTP request to
/. While the model runs, the browser polls for updates via the Refresh header. When the model completes, the server exits (unlessLOFIGUI_HOLD=1is set). - WASM — the page loads with a Start button. The user clicks to begin, and JavaScript polls
goRender()every 500ms to update the output. After the model completes, clicking Start again restarts it.
Handle() which auto-starts on an empty buffer — request-driven. WASM uses RunWASM() which exports goStart() behind a button — user-driven. The model code itself is identical in both cases.