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