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.

Three variants of the same app: 01 uses the compact app.RunWASM(model) call — the library auto-generates the SW bootstrap so the example itself has no templates/ directory. 01a is the same behaviour with every wire visible (explicit setupRoutes(), hand-written SW bootstrap). 01b adds gzipped WASM on top of 01a, showing the DecompressionStream + cache plumbing.
During polling — partial output
During polling
After completion — full output
Complete

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.")
}
lofigui.Print() works like 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.
The loop and app.Sleep() are just a facsimile of a long-running task — standing in for real work like processing files, running a simulation, or querying an API. The model runs in a background goroutine; while it works, the browser keeps refreshing to show new output. Cancellation is transparent — if the user restarts, the framework terminates the old goroutine automatically.
StartAction / EndActionHandle 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.

model.go source on Codeberg


The server — wiring it up

Two lines: create an app, run the model.

func main() {
    app := lofigui.NewApp()
    app.Run(":1340", model)
}
app.Run() registers the model on /, 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.
DefaultsNewApp() 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
Transparent cancellation — when cancel is triggered, 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. A service worker intercepts HTTP requests and routes them to Go's net/http handlers running inside the WASM binary. The browser sees real HTTP responses — forms, redirects, Refresh headers — identical to the server version.

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() {
    app := lofigui.NewApp()
    app.RunWASM(model)
}
App.RunWASM is the service worker equivalent of App.Run. It registers the same routes (display, start, cancel, favicon) on an http.ServeMux and serves them via go-wasm-http-server. The browser page uses a service worker (sw.js) that loads the WASM binary and intercepts fetch events. No custom JavaScript polling — just standard HTTP.
Building: 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.

WASM source on Codeberg

Gzipped WASM

Example 01 itself only ships the plain (~11 MB) WASM binary — the compact API accepts that trade for a one-line deployment. Projects that want a smaller download follow example 01b, which layers DecompressionStream + a cached decompressed binary on top of 01a's explicit SW wiring. The decompression plumbing is visible enough that it's worth reading as its own tutorial rather than hidden behind a flag.

Server vs WASM lifecycle

Both the server and WASM builds use the same HTTP handler pattern. The lifecycles differ only in how the server starts:

  • Serverapp.Run(":1340", model) starts a real HTTP server. The model auto-starts on the first request to /. When the model completes, the server exits (unless LOFIGUI_HOLD=1 is set).
  • WASMapp.RunWASM(model) registers the same routes but serves them via a service worker. The user clicks a Start button (HTML form POST to /start). The browser's Refresh header handles polling — no custom JavaScript needed.
Explicit wiring — for custom routes, templates, or multi-page apps, see example 01a which unbundles RunWASM into setupRoutes() + wasmhttp.Serve().