tui

package
v0.7.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2026 License: MIT Imports: 11 Imported by: 0

README

tui

A full-screen terminal UI framework for building interactive CLI applications — chat interfaces, log viewers, AI assistants, and more.

Layout

  Scrollable output / conversation history
  (auto-scrolls to bottom; Page Up/Down/wheel to scroll)

  Command palette (visible when / is typed)

┌─────────────────────────────────────── spinner / status ─┐
│                                                          │
│ > input goes here                                        │
│                                                          │
└─ myapp ──────────────────────────────────────────────────┘

The input box top border carries the spinner, progress bar, scroll hint, or StatusRight text (in that priority order). StatusLeft is embedded in the bottom border.

Quick Start

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/paularlott/cli/tui"
)

func main() {
    var t *tui.TUI

    t = tui.New(tui.Config{
        Theme:       tui.ThemeAmber,
        StatusLeft:  "myapp",
        StatusRight: "Ctrl+C to exit",
        Commands: []*tui.Command{
            {
                Name:        "exit",
                Description: "Exit the application",
                Handler:     func(_ string) { t.Exit() },
            },
            {
                Name:        "clear",
                Description: "Clear conversation history",
                Handler:     func(_ string) { t.ClearOutput() },
            },
        },
        OnSubmit: func(text string) {
            t.AddMessage(tui.RoleUser, text)
            t.AddMessage(tui.RoleAssistant, "Echo: "+text)
        },
    })

    t.AddMessage(tui.RoleSystem, "Welcome! Type / for commands.")

    if err := t.Run(context.Background()); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Configuration

type Config struct {
    Theme          *Theme      // Active theme. Defaults to ThemeDefault.
    Themes         []*Theme    // Additional themes registered into the global registry.
    Commands       []*Command  // Slash commands shown in the palette.
    OnSubmit       func(text string) // Called when the user submits input.
    OnEscape       func()            // Called when Escape is pressed outside the palette.
    UserLabel      string      // Label for user messages. Empty string hides the header.
    AssistantLabel string      // Label for assistant messages. Empty string hides the header.
    SystemLabel    string      // Label for system messages. Empty string hides the header.
    StatusLeft     string      // Text shown bottom-left.
    StatusRight    string      // Text shown bottom-right (overridden by spinner/progress/scroll hint).
    ShowCharCount  bool        // Show character counter below the input box. Default: false.
    HideHeaders    bool        // Suppress all role headers regardless of label values. Default: false.
    InputEnabled   *bool       // Set to false for output-only / log-viewer mode.
}

Labels

Headers are shown only when a label is set. Empty string (the default) hides the header for that role.

// Set at construction time:
t = tui.New(tui.Config{
    UserLabel:      "You",
    AssistantLabel: "GPT-4o",
    SystemLabel:    "System",
})

// Update at runtime (empty string leaves that label unchanged):
t.SetLabels("You", "Claude 3.5", "")

Individual messages can override the default label:

t.AddMessageAs(tui.RoleAssistant, "GPT-4o", "Here is my answer…")
t.StartStreamingAs("Claude 3.5")

Messages

// Append a message from a standard role.
t.AddMessage(tui.RoleUser, "Hello!")
t.AddMessage(tui.RoleAssistant, "Hi there.")
t.AddMessage(tui.RoleSystem, "Connected.")

// Append a message with a custom label (overrides the role label for this message).
t.AddMessageAs(tui.RoleAssistant, "GPT-4o", "Here is my answer…")

Message content supports fenced code blocks:

t.AddMessage(tui.RoleAssistant, "Example:\n\n```go\nfmt.Println(\"hello\")\n```")

Streaming

For token-by-token responses:

t.StartStreamingAs("Claude 3.5") // or t.StartStreaming() for the default label
for chunk := range tokenCh {
    if !t.IsStreaming() {
        break // stopped externally via StopStreaming()
    }
    t.StreamChunk(chunk)
}
t.StreamComplete()

// To stop early and finalise the message (e.g. from OnEscape):
t.StopStreaming()

Commands

All commands are supplied by the caller — there are no built-ins. Register them in Config.Commands at construction time, or add/remove them at runtime:

Commands: []*tui.Command{
    {
        Name:        "exit",
        Description: "Exit the application",
        Handler:     func(_ string) { t.Exit() },
    },
    {
        Name:        "theme",
        Description: "Switch theme",
        Args:        []string{"amber", "blue", "green"}, // shown as sub-options
        Handler: func(args string) {
            if th, ok := tui.ThemeByName(args); ok {
                t.SetTheme(th)
            }
        },
    },
},

// Add a command after construction:
t.AddCommand(&tui.Command{
    Name:        "save",
    Description: "Save current session",
    Handler:     func(_ string) { saveSession() },
})

// Remove a command by name:
t.RemoveCommand("save")

Type / to open the palette. Use / to navigate, Tab to complete, Enter to execute, Esc to close.

Spinner

Displays an animated braille spinner in the input box top border:

t.StartSpinner("Thinking…")
// … do work …
t.StopSpinner()

Calling StartSpinner while one is already running replaces the text. Starting a progress bar stops the spinner automatically.

Progress Bar

Displays a labelled progress bar in the input box top border:

for i := 0; i <= 100; i++ {
    t.SetProgress("Uploading…", float64(i)/100)
}
t.ClearProgress()

The border renders: ┌──────── Uploading… [████████████░░░░░░░░] 60% ┐

Spinner and progress bar are mutually exclusive. Both are overridden by the scroll hint when the user has scrolled up.

Status Bar

t.SetStatus("myapp", "v1.2.3")   // set both at once
t.SetStatusLeft("myapp")
t.SetStatusRight("v1.2.3")

Output

t.ClearOutput() // remove all messages from the output region

Styled Text

Styled wraps a string in a theme color for use in message content:

t.AddMessage(tui.RoleSystem,
    tui.Styled(t.Theme().Text, "myapp") + "\n" +
    tui.Styled(t.Theme().Primary, "v1.2.3"),
)

t.Theme() returns the active theme so you can reference its color fields (Primary, Secondary, Text, Dim, Error, etc.) at call time, picking up any theme changes automatically.

Menus

t.OpenMenu(m) replaces the input box with a navigable bordered panel. t.CloseMenu() dismisses it programmatically.

t.OpenMenu(&tui.Menu{
    Title: "Settings",
    Items: []*tui.MenuItem{
        {
            Label: "Theme",
            Children: []*tui.MenuItem{          // sub-menu — shows › indicator
                {Label: "Amber", Value: "amber", OnSelect: func(item *tui.MenuItem, _ string) {
                    if th, ok := tui.ThemeByName(item.Value); ok { t.SetTheme(th) }
                }},
            },
        },
        {
            Label:  "API Key",
            Prompt: "Enter API key:",            // text-entry mode
            OnSelect: func(_ *tui.MenuItem, input string) {
                // input holds what the user typed
            },
        },
        {
            Label: "About",
            OnSelect: func(_ *tui.MenuItem, _ string) {
                t.AddMessage(tui.RoleSystem, "v1.0.0")
            },
        },
    },
})
MenuItem fields
Field Type Description
Label string Display text
Value string Optional data passed to OnSelect
Prompt string If set, selecting this item opens a text-entry prompt (shows indicator)
Children []*MenuItem If set, selecting pushes a sub-menu (shows indicator)
OnSelect func(item *MenuItem, input string) Called when item is confirmed; input is non-empty for Prompt items
Navigation
Key Action
/ Move selection
Enter Select item / confirm prompt
Esc Cancel prompt → back to list; pop sub-menu → close at root

The title bar shows a breadcrumb when inside a sub-menu: Settings › Theme.

Output-Only Mode

Set InputEnabled to false to hide the input box, char count, and palette. Only scrolling and Ctrl+C remain active. Useful for log viewers or progress displays driven entirely by the application.

enabled := false
t = tui.New(tui.Config{
    InputEnabled: &enabled,
    // …
})

Context & Shutdown

Run accepts a context.Context. Cancelling the context shuts down the event loop cleanly:

ctx, cancel := context.WithCancel(context.Background())
// cancel() from anywhere to exit

t.Run(ctx)

// Retrieve the context later (e.g. inside a command handler):
ctx := t.Context()

t.Exit() is a convenience method that sets the quit flag directly — wire it to a /exit command.

Themes

Built-in themes
Name Description
default Dark navy, cyan primary, crimson secondary (default)
amber Dark warm background, amber primary, teal secondary
blue Deep dark background, periwinkle primary, sky secondary
green Dark terminal, mint primary, gold secondary
purple Dark background, lavender primary, rose secondary
light Light background, blue primary, green secondary
plain No colors (monochrome)
Selecting by name
if th, ok := tui.ThemeByName("amber"); ok {
    t.SetTheme(th)
}

// List all available theme names:
names := tui.ThemeNames() // ["amber", "blue", "default", "green", "light", "plain", "purple"]
Custom themes

Define a Theme struct and either pass it in Config.Themes (registered automatically) or call RegisterTheme directly:

myTheme := &tui.Theme{
    Name:      "solarized",
    Primary:   0x268BD2,
    Secondary: 0x2AA198,
    Text:      0x839496,
    UserText:  0x268BD2,
    Dim:       0x586E75,
    CodeBG:    0x073642,
    CodeText:  0x839496,
    Error:     0xDC322F,
}

// Option A — via Config (registered before Run):
t = tui.New(tui.Config{
    Themes: []*tui.Theme{myTheme},
    Theme:  myTheme,
})

// Option B — global registry at any time:
tui.RegisterTheme(myTheme)

Color values are 24-bit RGB packed as 0xRRGGBB. A zero value means the terminal's default color.

Keyboard Reference

Key Action
Enter Submit input / execute selected command
Shift+Enter Insert newline
/ Move cursor up/down in multi-line input, navigate history (single-line), or navigate palette
/ Move cursor left/right
Home / End Jump to start/end of line
Backspace Delete character before cursor
Delete Delete character at cursor
Ctrl+A Move to start of line
Ctrl+K Delete to end of line
Ctrl+U Delete to start of line
Ctrl+W Delete word before cursor
Page Up/Down Scroll output half a page
Mouse wheel Scroll output 3 lines
Tab Complete selected palette command/arg
Esc Close palette / fire OnEscape
Ctrl+C Exit

Documentation

Overview

Package tui provides a full-screen terminal UI framework for building interactive CLI applications, inspired by modern AI assistants.

Layout

The screen is divided into three vertical regions:

┌─────────────────────────────────────────────────────────┐
│  Scrollable output / conversation history               │
│  (auto-scrolls to bottom; Page Up/Down/wheel to scroll) │
├─────────────────────────────────────────────────────────┤
│  Command palette (visible when / is typed)              │
├─────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────┐    │
│  │ > input goes here      Ctrl+C to exit           │    │
│  └─────────────────────────────────────────────────┘    │
│  myapp                                                  │
└─────────────────────────────────────────────────────────┘

Quick Start

t := tui.New(tui.Config{
    Theme: tui.ThemeAmber,
    Commands: []*tui.Command{
        {Name: "clear", Description: "Clear history", Handler: func(_ string) { t.ClearOutput() }},
    },
    OnSubmit: func(text string) {
        t.AddMessage(tui.RoleUser, text)
        t.AddMessage(tui.RoleAssistant, "Echo: "+text)
    },
})
t.Run(context.Background())

Streaming

For token-by-token responses:

t.StartStreamingAs("GPT-4o")
for chunk := range tokenCh {
    t.StreamChunk(chunk)
}
t.StreamComplete()

Themes

Seven built-in themes: ThemeAmber, ThemeBlue, ThemeGreen, ThemePurple, ThemeLight, ThemePlain, ThemeDefault. Look up by name with ThemeByName. Register custom themes with RegisterTheme or via [Config.Themes].

Index

Constants

This section is empty.

Variables

View Source
var (
	// ThemeDefault is the default theme — dark navy background, cyan primary, crimson secondary.
	ThemeDefault = &Theme{
		Name:      "default",
		Primary:   0x4EB8C8,
		Secondary: 0xC0395A,
		Text:      0xE8EAF0,
		UserText:  0x4EB8C8,
		Dim:       0x7A8492,
		CodeBG:    0x111A26,
		CodeText:  0xE8EAF0,
		Error:     0xC0395A,
	}

	// ThemeAmber — warm dark background, amber primary, teal secondary.
	ThemeAmber = &Theme{
		Name:      "amber",
		Primary:   0xE8A87C,
		Secondary: 0x7EC8A4,
		Text:      0xCDD6F4,
		UserText:  0xE8A87C,
		Dim:       0x6C6F85,
		CodeBG:    0x1E1A14,
		CodeText:  0xCDD6F4,
		Error:     0xF38BA8,
	}

	// ThemeBlue — deep dark background, periwinkle primary, sky secondary.
	ThemeBlue = &Theme{
		Name:      "blue",
		Primary:   0x7BA7E8,
		Secondary: 0x5BC8D8,
		Text:      0xD0D8F0,
		UserText:  0x7BA7E8,
		Dim:       0x5A6070,
		CodeBG:    0x0D1117,
		CodeText:  0xD0D8F0,
		Error:     0xE06C75,
	}

	// ThemeGreen — dark terminal, mint primary, gold secondary.
	ThemeGreen = &Theme{
		Name:      "green",
		Primary:   0x7EC87A,
		Secondary: 0xD4A843,
		Text:      0xD8E0D0,
		UserText:  0x7EC87A,
		Dim:       0x5A6650,
		CodeBG:    0x0D1A0F,
		CodeText:  0xD8E0D0,
		Error:     0xE05050,
	}

	// ThemePurple — dark background, lavender primary, rose secondary.
	ThemePurple = &Theme{
		Name:      "purple",
		Primary:   0xB48EE8,
		Secondary: 0xE87EB4,
		Text:      0xE0D8F0,
		UserText:  0xB48EE8,
		Dim:       0x6A6080,
		CodeBG:    0x130D1E,
		CodeText:  0xE0D8F0,
		Error:     0xF07070,
	}

	// ThemeLight — light background, blue primary, green secondary.
	ThemeLight = &Theme{
		Name:      "light",
		Primary:   0x1A56CC,
		Secondary: 0x0A7A50,
		Text:      0x1A1A2E,
		UserText:  0x1A56CC,
		Dim:       0x666677,
		CodeBG:    0xE8EAF0,
		CodeText:  0x1A1A2E,
		Error:     0xCC2020,
	}

	// ThemePlain uses no colors (monochrome).
	ThemePlain = &Theme{
		Name: "plain",
	}
)

Built-in themes.

Functions

func RegisterTheme

func RegisterTheme(t *Theme)

RegisterTheme adds a custom theme to the global registry, keyed by Theme.Name.

func Styled

func Styled(color Color, text string) string

Styled returns text wrapped in the given color, reset after. Use the theme color fields directly: t.Theme().Primary, t.Theme().Secondary, etc.

func ThemeNames

func ThemeNames() []string

ThemeNames returns a sorted slice of all registered theme names.

Types

type Color

type Color uint32

Color is a 24-bit RGB color packed as 0xRRGGBB. Zero means "default terminal color".

type Command

type Command struct {
	Name        string
	Description string
	Args        []string // Optional sub-options shown in palette after a space
	Handler     func(args string)
}

Command is a slash command that can be registered with the TUI.

type Config

type Config struct {
	// Theme controls colors. Defaults to ThemeAmber if nil.
	Theme *Theme

	// Commands are the slash commands available in the palette.
	Commands []*Command

	// Themes registers additional themes into the global theme registry,
	// making them available via ThemeByName.
	Themes []*Theme

	// OnSubmit is called when the user presses Enter to submit input.
	// The TUI does NOT add a user message automatically; the caller decides.
	OnSubmit func(text string)

	// OnEscape is called when Escape is pressed and the palette is not active.
	OnEscape func()

	// UserLabel is the label shown for user messages. Defaults to "You".
	UserLabel string

	// AssistantLabel is the default label for assistant messages. Defaults to "Assistant".
	AssistantLabel string

	// SystemLabel is the label shown for system messages. Defaults to "System".
	SystemLabel string

	// HideHeaders suppresses the role header line between messages.
	HideHeaders bool

	// StatusLeft is optional text shown in the bottom-left status bar.
	StatusLeft string

	// StatusRight is optional text shown in the bottom-right status bar.
	StatusRight string

	// ShowCharCount enables the character counter below the input box. Defaults to false.
	ShowCharCount bool

	// InputEnabled controls whether the input box is shown. Defaults to true.
	// When false, the input box, char count, and palette are hidden and
	// keyboard input only handles scrolling and Ctrl+C.
	InputEnabled *bool
}

Config holds the configuration for a TUI instance.

type Menu struct {
	Title string
	Items []*MenuItem
}

Menu is a navigable panel that replaces the input box.

type MenuItem struct {
	Label    string
	Value    string                             // optional value passed to OnSelect
	Prompt   string                             // if set, selecting this item opens a text-entry prompt with this label
	Children []*MenuItem                        // non-nil → sub-menu
	OnSelect func(item *MenuItem, input string) // input is non-empty only for Prompt items
}

MenuItem is a single entry in a Menu.

type MessageRole

type MessageRole int

MessageRole identifies who sent a message.

const (
	RoleAssistant MessageRole = iota
	RoleUser
	RoleSystem
)

type TUI

type TUI struct {
	// contains filtered or unexported fields
}

TUI is the main terminal UI instance.

func New

func New(cfg Config) *TUI

New creates a new TUI with the given configuration.

func (*TUI) AddCommand added in v0.7.6

func (t *TUI) AddCommand(cmd *Command)

AddCommand registers a new slash command at runtime.

func (*TUI) AddMessage

func (t *TUI) AddMessage(role MessageRole, content string)

AddMessage appends a complete message to the output region.

func (*TUI) AddMessageAs

func (t *TUI) AddMessageAs(role MessageRole, label, content string)

AddMessageAs appends a complete message with a custom label.

func (*TUI) ClearOutput

func (t *TUI) ClearOutput()

ClearOutput removes all messages from the output region.

func (*TUI) ClearProgress

func (t *TUI) ClearProgress()

ClearProgress removes the progress bar from the separator.

func (*TUI) CloseMenu

func (t *TUI) CloseMenu()

CloseMenu closes the menu and restores the input box.

func (*TUI) Context

func (t *TUI) Context() context.Context

Context returns the context that was passed to Run. Returns nil if Run has not been called yet.

func (*TUI) Exit

func (t *TUI) Exit()

Exit cleanly shuts down the TUI event loop. Useful as a /exit command handler: func(_ string) { t.Exit() }

func (*TUI) IsStreaming

func (t *TUI) IsStreaming() bool

IsStreaming returns true if a streaming message is in progress.

func (*TUI) OpenMenu

func (t *TUI) OpenMenu(m *Menu)

OpenMenu opens a navigable menu panel, replacing the input box.

func (*TUI) RemoveCommand added in v0.7.6

func (t *TUI) RemoveCommand(name string)

RemoveCommand removes a slash command by name.

func (*TUI) Run

func (t *TUI) Run(ctx context.Context) error

Run enters the event loop. It blocks until the user exits (Ctrl+C, t.Exit(), or ctx cancellation).

func (*TUI) SetInputEnabled added in v0.7.9

func (t *TUI) SetInputEnabled(enabled bool)

SetInputEnabled toggles the input box at runtime.

func (*TUI) SetLabels added in v0.7.6

func (t *TUI) SetLabels(user, assistant, system string)

SetLabels updates the default role labels shown in message headers. Empty strings leave the corresponding label unchanged.

func (*TUI) SetProgress

func (t *TUI) SetProgress(label string, value float64)

SetProgress shows a labelled progress bar in the separator (0.0–1.0). Stops any active spinner. Call ClearProgress to remove it.

func (*TUI) SetStatus

func (t *TUI) SetStatus(left, right string)

SetStatus updates both status bar texts.

func (*TUI) SetStatusLeft

func (t *TUI) SetStatusLeft(s string)

SetStatusLeft updates the left status bar text.

func (*TUI) SetStatusRight

func (t *TUI) SetStatusRight(s string)

SetStatusRight updates the right status bar text.

func (*TUI) SetTheme

func (t *TUI) SetTheme(theme *Theme)

SetTheme changes the active theme.

func (*TUI) StartSpinner

func (t *TUI) StartSpinner(text string)

StartSpinner shows an animated spinner in the separator with the given text. Calling StartSpinner while one is running replaces the text.

func (*TUI) StartStreaming

func (t *TUI) StartStreaming()

StartStreaming begins a new streaming assistant message.

func (*TUI) StartStreamingAs

func (t *TUI) StartStreamingAs(label string)

StartStreamingAs begins a new streaming assistant message with a custom label.

func (*TUI) StopSpinner

func (t *TUI) StopSpinner()

StopSpinner stops the spinner and clears it from the separator.

func (*TUI) StopStreaming

func (t *TUI) StopStreaming()

StopStreaming finalises any in-progress streaming message.

func (*TUI) StreamChunk

func (t *TUI) StreamChunk(chunk string)

StreamChunk appends a chunk to the current streaming message.

func (*TUI) StreamComplete

func (t *TUI) StreamComplete()

StreamComplete finalises the streaming message.

func (*TUI) Theme

func (t *TUI) Theme() *Theme

Theme returns the active theme, allowing callers to access color values for use with Styled.

type Theme

type Theme struct {
	Name      string
	Primary   Color // Accents, prompt >
	Secondary Color // Muted text, hints
	Text      Color // Normal content
	UserText  Color // User message text
	Dim       Color // Very muted (scrollbar, borders)
	CodeBG    Color // Code block background
	CodeText  Color // Code block text
	Error     Color // Error messages
}

Theme defines the visual identity of the TUI.

func ThemeByName

func ThemeByName(name string) (*Theme, bool)

ThemeByName returns the theme registered under name, or (nil, false).

Directories

Path Synopsis
Example TUI application.
Example TUI application.
Example TUI log viewer — output-only mode.
Example TUI log viewer — output-only mode.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL