tui

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 0 Imported by: 0

README

tui

Single source of truth for the TUI handler-kind contract shared by tinywasm/devtui (the terminal renderer) and tinywasm/app (the daemon that serializes handler state to the client).

Why this exists

A "handler" is any value a consumer registers with the TUI. The TUI inspects the handler's Go interface to decide how to render it and how to serialize it over the daemon↔client wire. That mapping used to be re-derived independently in several places (devtui's local switch, its wire constants, app's hand-copied constants and a second detection switch). Adding or reordering a handler kind silently broke one side — e.g. a Selection handler (radio buttons) collapsing into a plain text field in tinywasm -tui.

This package centralizes the entire contract so there is exactly one place to change, and a completeness test that fails loudly when a new kind isn't fully wired.

Surface

Symbol Purpose
HandlerDisplay, HandlerEdit, HandlerExecution, HandlerInteractive, HandlerSelection, Loggable, StreamingLoggable, ShortcutProvider, Cancelable, TabAware The named interfaces a handler implements. The whole contract lives here — nowhere else.
Kind + Kind* consts The frozen handler-kind enum (also the wire value). Append-only.
AllKinds() Every kind in wire order — iterate it in consumer guardrail tests.
Classify(h any) (Kind, hasField bool) The one ordered walk, switching on the interfaces above directly — no shadow copies.
Extract(h any) Meta Pulls Name/Label/Value/Options/Shortcuts off a handler.
StateEntry The JSON wire format (produced by the daemon, consumed by the client).

Why interfaces.go lives here, not in devtui

Originally devtui owned the named interfaces (HandlerSelection, etc.) as documentation for handler authors, while its detection switch and the daemon's detection switch each re-declared their own anonymous copy of the same method shapes to do the actual type-switching. Three definitions of "what a Selection handler looks like" — the exact drift this package exists to prevent, just one level deeper. Classify now type-switches on HandlerSelection etc. directly, so there is one shape, used for both the doc a handler author reads and the detection that renders/serializes it. If you edit an interface here, Classify sees the change immediately — there is nothing else to keep in sync.

The guardrail

AllKinds() + Classify let every consumer write a test that iterates all kinds and asserts it handles each one. Appending a Kind here turns those tests red until the interface, detection, serialization, reconstruction and rendering are all wired — instead of degrading silently. See classify_test.go for the in-repo completeness checks.

Zero heavy dependencies

Detection uses structural interfaces, so this package imports nothing from devtui and no charmbracelet/bubbletea/lipgloss. The daemon can import it without pulling the terminal renderer. The dependency arrow is one-way: devtui/apptui, never back.

Adding a new handler kind

  1. Add the named interface to interfaces.go (e.g. HandlerFoo).
  2. Append a Kind<Name> constant (new highest value — never renumber).
  3. Add it to AllKinds().
  4. Add a spec to specs in classify.go, switching on the interface from step 1 — never redeclare its method set inline.
  5. Add a sample to sampleFor in the test.
  6. go test ./... — the completeness tests tell you what's missing.
  7. Bump the version; consumers wire the new kind, guided by their own AllKinds() guardrail tests going red.

Documentation

Overview

Package tui is the single source of truth for the TUI handler-kind contract shared by github.com/tinywasm/devtui (the renderer/consumer) and github.com/tinywasm/app (the daemon producer).

A "handler" is any value a consumer registers with the TUI. The TUI inspects the handler's Go interface to decide how to render it (button, text field, radio group, log stream…) and, in client/daemon mode, how to serialize it over the wire (StateEntry). Historically each consumer re-derived that mapping independently, so adding or reordering a handler kind silently broke one side. This package centralizes the whole contract:

  • Kind — the frozen enum (also the wire value).
  • Classify/Extract — the ONE ordered interface-detection walk + metadata.
  • StateEntry — the JSON wire format.
  • AllKinds — lets every consumer write a guardrail test that fails loudly when a new Kind is not fully wired.

It has zero heavy dependencies (no charmbracelet/bubbletea/lipgloss), so the daemon can import it without pulling the terminal renderer.

Index

Constants

View Source
const (
	LogOpen  = "[..." // Start or update same line with auto-animation
	LogClose = "...]" // Update same line and stop auto-animation
)

LogOpen and LogClose are special prefixes for progress indication. Use LogOpen at the start of a long operation to show an animated spinner. Use LogClose when the operation completes to stop the animation.

Example:

handler.log(tui.LogOpen, "Deploying to production")
// ... long operation ...
handler.log(tui.LogClose, "Deployment complete")

Variables

This section is empty.

Functions

This section is empty.

Types

type Cancelable added in v0.1.1

type Cancelable interface {
	Cancel() // Called when user presses ESC to exit interactive mode
}

Cancelable defines the optional interface for handlers that want to be notified when the user cancels. Interactive handlers can implement this to clean up or reset their state when ESC is pressed.

type HandlerDisplay added in v0.1.1

type HandlerDisplay interface {
	Name() string    // Full text to display in footer (handler responsible for content) eg. "System Status Information Display"
	Content() string // Display content (e.g., "help\n1-..\n2-...", "executing deploy wait...")
}

HandlerDisplay defines the interface for read-only information display handlers. These handlers show static or dynamic content without user interaction.

type HandlerEdit added in v0.1.1

type HandlerEdit interface {
	Name() string           // Identifier for logging: "ServerPort", "DatabaseURL"
	Label() string          // Field label (e.g., "Server Port", "Host Configuration")
	Value() string          // Current/initial value (e.g., "8080", "localhost")
	Change(newValue string) // Handle user input + content display via log
}

HandlerEdit defines the interface for interactive fields that accept user input. These handlers allow users to modify values through text input.

type HandlerExecution added in v0.1.1

type HandlerExecution interface {
	Name() string  // Identifier for logging: "DeployProd", "BuildProject"
	Label() string // Button label (e.g., "Deploy to Production", "Build Project")
	Execute()      // Execute action + content display via log
}

HandlerExecution defines the interface for action buttons that execute operations. These handlers trigger business logic when activated by the user.

type HandlerInteractive added in v0.1.1

type HandlerInteractive interface {
	Name() string           // Identifier for logging: "ChatBot", "ConfigWizard"
	Label() string          // Field label (updates dynamically)
	Value() string          // Current input value
	Change(newValue string) // Handle user input + content display via log
	WaitingForUser() bool   // Should edit mode be auto-activated?
}

HandlerInteractive defines the interface for interactive content handlers. These handlers combine content display with user interaction capabilities. All content display is handled through progress() for consistency.

type HandlerSelection added in v0.1.1

type HandlerSelection interface {
	Name() string                 // Identifier for logging: "CompilerMode"
	Label() string                // Group label shown before the buttons: "Compiler Mode"
	Value() string                // Key of the currently active option (e.g. "L")
	Change(newValue string)       // Called on confirm with the selected option's key
	Options() []map[string]string // Ordered {value: label} pairs (>= 2 recommended)
}

HandlerSelection defines a radio / segmented-control field: a group of mutually-exclusive options rendered as evenly distributed buttons in the footer. Exactly one option is active at a time.

It is a superset of HandlerEdit: Name/Label/Value/Change behave identically, and Options() advertises the discrete choice set that the TUI renders as buttons instead of a free-text input. Classify checks HandlerSelection BEFORE HandlerEdit for exactly this reason (see classify.go's specs order).

Interaction: the user presses Enter to enter selection mode, moves the highlight with Left/Right, presses Enter to confirm (which calls Change(selectedValue)), or Esc to cancel.

Options() returns an ORDERED slice of single-entry maps {value: label}, identical in shape to ShortcutProvider.Shortcuts(). The map KEY is the stable value passed to Change(); the map VALUE is the human-readable button caption. Value() must return the key of the currently active option.

type Kind added in v0.1.0

type Kind int

Kind identifies how a registered handler is rendered and serialized.

The integer values are the FROZEN wire protocol: they are written into StateEntry.HandlerType and read back by remote consumers. Never reorder or renumber existing kinds — only append new ones at the end. TestKindWireValues guards this.

const (
	KindDisplay     Kind = 0 // read-only footer text (Name + Content)
	KindEdit        Kind = 1 // free-text input field (Name + Label + Value + Change)
	KindExecution   Kind = 2 // action button (Name + Label + Execute)
	KindInteractive Kind = 3 // auto-editing field (Edit + WaitingForUser)
	KindLoggable    Kind = 4 // log stream only, no footer field (Name + SetLog)
	KindSelection   Kind = 5 // radio / segmented buttons (Edit + Options)
)

func AllKinds added in v0.1.0

func AllKinds() []Kind

AllKinds returns every declared Kind in wire order. Consumers iterate this in their own guardrail tests to assert they handle every kind — so a newly appended Kind turns those tests red instead of degrading silently.

func Classify added in v0.1.0

func Classify(h any) (kind Kind, hasField bool)

Classify runs the single ordered detection walk and returns the handler's Kind and whether it renders a footer field. hasField is false only for KindLoggable (log stream, no field). A handler matching nothing is treated as a log-only handler with no field.

func (Kind) String added in v0.1.0

func (k Kind) String() string

String returns the kind's name, for readable log and test output.

type Loggable added in v0.1.1

type Loggable interface {
	Name() string
	SetLog(logger func(message ...any))
}

Loggable defines optional logging capability for handlers. Handlers implementing this receive a logger function from the TUI when registered via AddHandler.

The log function provided by the TUI: - Is never nil (safe to call immediately) - Automatically tracks messages by handler Name() - Stores full history internally - Displays only most recent log in terminal (clean view)

Example implementation:

type WasmClient struct {
    log func(message ...any)
}

func NewWasmClient() *WasmClient {
    return &WasmClient{
        log: func(message ...any) {}, // no-op until SetLog called
    }
}

func (w *WasmClient) Name() string { return "WASM" }

func (w *WasmClient) SetLog(logger func(message ...any)) {
    w.log = logger
}

func (w *WasmClient) Compile() {
    w.log("Compiling...")
}

type Meta added in v0.1.0

type Meta struct {
	Name      string
	Label     string
	Value     string
	Options   []map[string]string
	Shortcuts []map[string]string
}

Meta is the data Extract pulls off a handler. Fields are populated only when the handler exposes the corresponding method; the rest stay zero.

func Extract added in v0.1.0

func Extract(h any) Meta

Extract reads the metadata a StateEntry needs off any handler. Each field is filled only when the handler exposes the matching method. Options and Shortcuts are read via the same named interfaces (HandlerSelection, ShortcutProvider) that Classify uses, not ad-hoc method checks.

type ShortcutProvider added in v0.1.1

type ShortcutProvider interface {
	Shortcuts() []map[string]string // Returns ordered list of single-entry maps with shortcut->description, preserving registration order
}

ShortcutProvider defines the optional interface for handlers that provide global shortcuts. HandlerEdit/HandlerSelection implementations can implement this to enable global shortcut keys.

type StateEntry added in v0.1.0

type StateEntry struct {
	TabTitle     string              `json:"tab_title"`
	HandlerName  string              `json:"handler_name"`
	HandlerColor string              `json:"handler_color"`
	HandlerType  int                 `json:"handler_type"` // a Kind value
	Label        string              `json:"label"`
	Value        string              `json:"value"`
	Shortcut     string              `json:"shortcut"`  // primary key = handler Name()
	Shortcuts    []map[string]string `json:"shortcuts"` // from a Shortcuts() provider
	Options      []map[string]string `json:"options"`   // Selection buttons: ordered {value:label}
}

StateEntry is the JSON wire format for a single handler registered in the daemon TUI. Produced by the daemon (github.com/tinywasm/app's HeadlessTUI), consumed by the client (github.com/tinywasm/devtui client mode).

The JSON tags are the published contract: any producer must match them exactly, and existing tags must never change. Only additive changes (new fields) are allowed.

type StreamingLoggable added in v0.1.1

type StreamingLoggable interface {
	Loggable
	AlwaysShowAllLogs() bool // Return true to show all messages
}

StreamingLoggable enables handlers to display ALL log messages instead of the default "last message only" behavior.

type TabAware added in v0.1.1

type TabAware interface {
	OnTabActive()
}

TabAware defines the optional interface for handlers that want to be notified when their tab becomes active. This is useful for lazy initialization or delayed logging that requires the TUI's logger to be injected first.

Jump to

Keyboard shortcuts

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