Build declarative terminal UIs in Go

Compose node trees. Tooey handles layout, diffing, and rendering — you just describe what the screen should look like.

View on GitHub
$go get github.com/stukennedy/tooey

See it in action


Everything you need for terminal UIs

A batteries-included framework with an Elm-inspired architecture, high-performance rendering, and declarative composition.

fn

Elm Architecture

Init, Update, View. Pure functions, message-driven updates, immutable state. Predictable and testable by design.

30

30fps Diffing

Efficient frame diffing renders only changed cells as minimal ANSI sequences. Smooth, flicker-free output.

<>

Declarative Nodes

Text, Box, Row, Column, List, Pane, Spacer — compose UIs with value structs and method chaining.

go

Async Commands

Return commands from Update that run in goroutines and deliver messages back to the update loop.

Focus Management

Declarative focus with Tab/Shift-Tab cycling and escape-to-pop context stack navigation.

Server-Driven UI

Built-in SSE client for server-sent events with HTTP action posting for real-time updates.


The rendering pipeline

From keypress to screen update in one clean pipeline. Each stage is independent and composable.

Input
Update
View
Layout
Paint
Diff
ANSI

Virtual node tree → single-pass flex layout → row-major cell buffer → frame diff → minimal ANSI output


Simple, expressive, Go-native

Build complete terminal applications with just a few functions. No boilerplate, no ceremony.

main.goGo
package main

import (
    "context"
    "github.com/stukennedy/tooey/app"
    "github.com/stukennedy/tooey/node"
)

type model struct {
    count int
}

func main() {
    a := &app.App{
        Init: func() interface{} {
            return &model{}
        },
        Update: func(m interface{}, msg app.Msg) app.UpdateResult {
            mdl := m.(*model)
            switch msg.(type) {
            case app.KeyMsg:
                if msg.(app.KeyMsg).Key == "q" {
                    return app.UpdateResult{Model: nil} // quit
                }
                mdl.count++
            }
            return app.UpdateResult{Model: mdl}
        },
        View: func(m interface{}, focused string) node.Node {
            mdl := m.(*model)
            return node.Column(
                node.Text("Press any key to increment, q to quit"),
                node.Text(fmt.Sprintf("Count: %d", mdl.count)),
            )
        },
    }
    a.Run(context.Background())
}
composition.goGo
// Compose UI with value structs and chaining
node.Column(
    node.Box(node.BorderRounded,
        node.Text("Hello Tooey").WithFocusable(),
    ),
    node.Row(
        node.Text("Left"),
        node.Spacer(),
        node.Text("Right"),
    ),
    node.List(items...).WithFlex(1).WithScrollToBottom(),
)

Up and running in seconds

One dependency. Standard Go tooling. No CGo, no build scripts.

1

Install

go get github.com/stukennedy/tooey
2

Requirements

Go 1.24+ and a terminal supporting ANSI escape sequences. Only external dependency: golang.org/x/term

3

Run the Demos

go run ./demos/list    # Interactive selection list
go run ./demos/maude   # Chat TUI with markdown