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.

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. 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) }
RunWASM exports four functions to JavaScript — 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.
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

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)
Different loaders — Go and TinyGo each ship their own 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 (unless LOFIGUI_HOLD=1 is 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.
Why the difference? The server uses 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.