Kao is a widget library for Plan 9's draw device, written in Go. It provides native GUI components—buttons, inputs, checklists, progress bars, sparklines, tab bars, file browsers, and more—with a classic, lightweight look and event-driven architecture.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mike 'Fuzzy' Partin e550f8e43a
All checks were successful
CI / ci (pull_request) Successful in 1m20s
ci: remove push trigger from workflow
2026-05-01 16:11:44 -07:00
.forgejo/workflows ci: remove push trigger from workflow 2026-05-01 16:11:44 -07:00
examples/demo refactor: reorganize imports and refine text widget drawing logic 2026-05-01 16:07:29 -07:00
internal refactor: rename EventType to Type and reorganize event definitions 2026-05-01 16:06:55 -07:00
vendor feat: add 9fans.net/go draw package vendoring 2026-03-28 01:52:33 -07:00
widget refactor: use image.Point{} instead of image.ZP in draw calls and cleanup imports 2026-05-01 16:07:09 -07:00
.gitignore chore: update gitignore to include go build artifacts and os generated files 2026-05-01 15:55:41 -07:00
.golangci.yml feat: add golangci-lint configuration file 2026-05-01 15:52:09 -07:00
.pre-commit-config.yaml feat: add pre-commit configuration for code quality checks 2026-05-01 15:54:18 -07:00
go.mod fix: remove indirect dependency declaration from go.mod 2026-03-28 01:21:27 -07:00
go.sum Phase 1: Project Foundation and Draw System Integration 2026-03-27 23:58:18 -07:00
README.md docs: add comprehensive README with library overview, features, and usage examples 2026-03-29 03:40:08 -07:00
ROADMAP.md feat: mark radiobutton, progress bar, and integration phases as complete 2026-03-28 01:51:51 -07:00
screen.go refactor: reorganize imports and refine text widget drawing logic 2026-05-01 16:07:29 -07:00

Kao

Kao is a widget library for the Plan 9 draw library, written in Go. It provides a collection of interactive GUI components—buttons, text inputs, checklists, radio buttons, progress bars, sparklines, tab bars, file browsers, and more—that can be used to build native Plan 9 applications with a classic, lightweight look and feel. The library is currently in active development, but all core widgets are implemented and ready for use.

The library is designed around a simple, eventdriven architecture where each widget implements a common Widget interface, making it easy to compose, extend, and embed custom components.

Features

  • Core Widgets Button, Input, Checklist, Radiobutton, ProgressBar, Sparkline, TabBar, Container, FileBrowser, FileViewer.
  • Plan 9 draw Integration Uses the 9fans.net/go/draw package for lowlevel graphics and input.
  • EventDriven Unified event system for mouse, keyboard, and resize events.
  • Flexible Theming Color palettes (default and dark) with perwidget overrides.
  • Easy Embedding All widgets embed a BaseWidget that provides common geometry, visibility, and enablement logic.
  • Demo Application A fullyfunctional example (examples/demo/) that showcases every widget and interactive wiring.

Installation

Kao requires Go 1.24.11 or later and the 9fans.net/go/draw package.

go get git.lan.thwap.org/thwap/kao

Because the library depends on Plan 9s draw library, you must have a Plan 9 environment (or a compatible Unix system with the draw device) to run applications. On Linux/Unix, you can use drawterm to connect to a Plan 9 CPU server, or run a local Plan 9 instance via 9vx or similar.

Quick Start

Create a new Go file (e.g., main.go) with the following minimal example:

package main

import (
    "fmt"
    "git.lan.thwap.org/thwap/kao"
    "git.lan.thwap.org/thwap/kao/widget"
    "image"
)

func main() {
    // Initialize a screen (window)
    screen, err := kao.NewScreen("Hello Kao", "400x300")
    if err != nil {
        fmt.Printf("Failed to create screen: %v\n", err)
        return
    }
    defer screen.Close()

    // Create a button with a click handler
    btn := widget.NewButton("Click Me!", image.Rect(50, 50, 200, 100), func() {
        fmt.Println("Button clicked!")
    })

    // Set the button as the root widget
    screen.SetRoot(btn)

    // Start the event loop
    screen.Run()
}

Run the program:

go run main.go

A window titled “Hello Kao” will appear with a clickable button.

Widgets

Widget Description
Button Clickable button with label and handler.
Input Singleline text field with cursor and change callback.
Checklist List of checkable items with keyboard navigation.
Radiobutton / RadioGroup Mutuallyexclusive radio buttons.
ProgressBar Horizontal/vertical progress bar with thresholds.
Sparkline Scrolling timeseries graph with multiple lanes and rendering modes.
TabBar Tabbed container switching between child widgets.
Container Simple container for grouping widgets.
FileBrowser Filesystem browser with directory navigation and integrated viewer.
FileViewer Displays file contents as text or hex.

Button (widget.Button)

A clickable button with configurable label, padding, and alignment.

btn := widget.NewButton("Label", image.Rect(x0, y0, x1, y1), func() {
    // handler
})

Input (widget.Input)

A singleline text field with cursor, focus management, and an optional OnChange callback.

input := widget.NewInput(image.Rect(x0, y0, x1, y1))
input.SetOnChange(func(text string) {
    fmt.Printf("Text changed: %s\n", text)
})

Checklist (widget.Checklist)

A list of checkable items with keyboard navigation.

cl := widget.NewChecklist(image.Rect(x0, y0, x1, y1))
cl.AddItem("Item 1", false)
cl.AddItem("Item 2", true)
cl.OnChange = func() {
    // called when any items checked state changes
}

Radiobutton (widget.Radiobutton) and RadioGroup (widget.RadioGroup)

Mutuallyexclusive radio buttons grouped by a RadioGroup.

group := widget.NewRadioGroup()
rb1 := widget.NewRadiobutton("Option 1", image.Rect(...))
rb1.SetGroup(group)
rb1.SetOnSelect(func() { fmt.Println("Option 1 selected") })

ProgressBar (widget.ProgressBar)

Horizontal or vertical progress bar with optional thresholds and color changes.

pb := widget.NewProgressBar(image.Rect(...), 100) // max = 100
pb.SetValue(30)
pb.SetOrientation(widget.Vertical)

Sparkline (widget.Sparkline)

A scrolling timeseries graph supporting multiple lanes, different rendering modes (line, bar, area, step), and dynamic scaling.

sp := widget.NewSparkline(image.Rect(...), 1000) // 1000point window
sp.SetMode(widget.ModeLine)
sp.AddLane("CPU", widget.DefaultPalette().Accent, 1000)
sp.AddValue(42)          // add to default lane
sp.AddValueToLane(1, 75) // add to lane 1

TabBar (widget.TabBar)

A tabbed container that switches between child widgets.

tb := widget.NewTabBar(screen.Size())
tb.AddTab("First", child1)
tb.AddTab("Second", child2)

Container (widget.Container)

A simple container that holds and draws multiple child widgets.

c := widget.NewContainer(image.Rectangle{})
c.AddChild(btn)
c.AddChild(input)

FileBrowser (widget.FileBrowser)

A filesystem browser with directory navigation, file selection, and an integrated file viewer.

fb := widget.NewFileBrowser(image.Rectangle{}, "/home/user")
fb.OnSelect = func(path string, isDir bool) {
    fmt.Printf("Selected: %s\n", path)
}

FileViewer (widget.FileViewer)

Displays file contents as text or hex; invoked automatically by the file browser.

fv := widget.NewFileViewer(image.Rectangle{}, "/path/to/file.txt")
fv.OnClose = func() {
    // close the viewer
}

Events

Events are defined in internal/event and include:

  • MouseEvent button press/release, movement, drag.
  • KeyboardEvent rune and key code.
  • ResizeEvent window size change.
  • ExposeEvent region needing redraw.

Widgets receive events via the Handle(interface{}) (bool, error) method. The Screens event loop forwards events to the root widget, which can propagate them to children.

Theming

Color palettes are defined in widget.Palette. Two builtin palettes are provided:

  • widget.DefaultPalette() light theme.
  • widget.DarkPalette() dark theme.

Set a widgets palette with SetPalette():

btn.SetPalette(widget.DarkPalette())

A PaletteManager is available for applicationwide palette management with perwidget overrides.

Architecture

Widget Interface

Every widget must implement the Widget interface:

type Widget interface {
    Draw(r image.Rectangle, screen *draw.Image)
    Handle(event interface{}) (bool, error)
    Resize(r image.Rectangle)
    Rect() image.Rectangle
}

BaseWidget

BaseWidget provides default implementations for Rect, Resize, SetVisible, SetEnabled, Contains, and palette management. Concrete widgets embed *BaseWidget and override Draw and Handle.

Screen

The Screen type (screen.go) manages the draw connection, mouse/keyboard controllers, and the main event loop. It creates a window, starts an eventprocessing goroutine, and calls Redraw() after each event or periodically (for animations).

Examples

The repository includes a comprehensive demo in examples/demo/ that demonstrates every widget and wires them together:

  • Button toggles autoincrement of a progress bar.
  • Input field updates a checklist item label.
  • Checklist selection prints checked items.
  • Radio buttons change the progress bars color theme.
  • Sparkline displays two lanes of randomwalk data.
  • Tab bar switches between three tabs (demo widgets, info text, file browser).

Run the demo:

cd examples/demo
go run main.go

Contributing

Contributions are welcome! Please follow the existing code style and add tests where applicable.

  1. Fork the repository.
  2. Create a feature branch.
  3. Implement your changes.
  4. Ensure the demo still works correctly.
  5. Submit a pull request.

For major changes, please open an issue first to discuss what you would like to change.

License

Kao is distributed under the same license as the 9fans.net/go/draw package (typically a permissive BSDstyle license). See the vendor/9fans.net/go/LICENSE file for details.

Roadmap

See ROADMAP.md for a detailed list of completed phases and future plans.


Kao simple, native widgets for Plan 9 draw.