gitstack

grindlemire/go-tui code browser

13.2 KB markdown 392 lines 2026-04-18 · 28dfef2 raw

Component Interfaces Reference

Overview

go-tui uses interfaces to define component behavior. A struct component only needs to implement Component (the Render method). All other interfaces are optional. Implement them to add keyboard handling, mouse support, timers, or lifecycle hooks.

The framework discovers these interfaces through type assertions during the mount process. The order of operations when a component first mounts:

  1. AppBinder.BindApp -- wire up State and Events fields
  2. Initializer.Init -- run setup logic, capture cleanup function
  3. Component.Render -- produce the element tree
  4. WatcherProvider.Watchers -- discover and start background watchers
  5. KeyListener.KeyMap and MouseListener.HandleMouse -- discovered during event dispatch via tree walks

On subsequent renders (cached mount), the framework calls PropsUpdater.UpdateProps (if implemented), re-calls AppBinder.BindApp, then Render.

Component

type Component interface {
    Render(app *App) *Element
}

The only required interface. Every struct component must have a Render method that returns an element tree. In .gsx files, the templ (s *myStruct) Render() syntax generates this method.

Render is called on every frame where the UI is dirty. It should be a pure function of the component's state: read state, build elements, return. Side effects belong in Init, Watchers, or event handlers.

When to implement: Always. This is what makes a struct a component.

type counter struct {
    count *tui.State[int]
}

func Counter() *counter {
    return &counter{count: tui.NewState(0)}
}

templ (c *counter) Render() {
    <div class="p-1">
        <span class="text-cyan font-bold">{fmt.Sprintf("Count: %d", c.count.Get())}</span>
    </div>
}

KeyListener

type KeyListener interface {
    KeyMap() KeyMap
}

Provides keyboard bindings for the component. KeyMap() is called during event dispatch when the framework walks the component tree, so it can return different bindings depending on the component's current state.

KeyMap is a []KeyBinding. Bindings are checked in order; the first match wins. Use On, OnStop, and OnFocused with a KeyMatcher (Key constants, Rune(), or AnyRune) to build bindings.

When to implement: When your component needs to respond to keyboard input.

func (c *counter) KeyMap() tui.KeyMap {
    return tui.KeyMap{
        tui.On(tui.Rune('+'), func(ke tui.KeyEvent) {
            c.count.Update(func(v int) int { return v + 1 })
        }),
        tui.On(tui.Rune('-'), func(ke tui.KeyEvent) {
            c.count.Update(func(v int) int { return v - 1 })
        }),
        tui.On(tui.KeyEscape, func(ke tui.KeyEvent) {
            ke.App().Stop()
        }),
    }
}

MouseListener

type MouseListener interface {
    HandleMouse(MouseEvent) bool
}

Handles mouse events (clicks, wheel, drag). Return true if the event was consumed, false to let it propagate. Like KeyListener, discovered by walking the component tree.

Mouse events are only delivered when mouse support is enabled via tui.WithMouse() (enabled by default in full-screen mode, disabled by default in inline mode).

When to implement: When your component has clickable elements or needs mouse wheel handling.

func (c *counter) HandleMouse(me tui.MouseEvent) bool {
    return tui.HandleClicks(me,
        tui.Click(c.incBtn, func() {
            c.count.Update(func(v int) int { return v + 1 })
        }),
        tui.Click(c.decBtn, func() {
            c.count.Update(func(v int) int { return v - 1 })
        }),
    )
}

WatcherProvider

type WatcherProvider interface {
    Watchers() []Watcher
}

Returns background watchers (timers, channel readers) that should run while the component is mounted. The framework calls Watchers() after mounting and starts each returned watcher. When the component unmounts or the app stops, the framework closes the stop channel, which signals all watchers to exit.

Watcher handlers run on the main event loop, so they can safely mutate state without synchronization.

When to implement: When your component needs periodic updates (timers) or receives data from Go channels.

func (c *dashboard) Watchers() []tui.Watcher {
    return []tui.Watcher{
        tui.OnTimer(time.Second, func() {
            c.elapsed.Update(func(v int) int { return v + 1 })
        }),
        tui.Watch(c.dataCh, func(msg string) {
            c.messages.Update(func(list []string) []string {
                return append(list, msg)
            })
        }),
    }
}

Initializer

type Initializer interface {
    Init() func()
}

Called once when the component first enters the tree (first Mount call). The returned function, if non-nil, is called when the component leaves the tree (unmount cleanup). This pairs setup and teardown at the same call site.

Init runs after BindApp but before Render, so state and events are already wired up.

When to implement: When your component needs one-time setup (open a file, start a connection) or cleanup (close resources, cancel goroutines).

func (c *logger) Init() func() {
    f, err := os.Open("app.log")
    if err != nil {
        return nil
    }
    c.file = f

    // Returned function runs on unmount
    return func() {
        f.Close()
    }
}

AppBinder

type AppBinder interface {
    BindApp(app *App)
}

Called by the framework to wire up State and Events fields to the app. Generated code from .gsx files emits BindApp methods automatically for each State and Events field gets its BindApp called in turn.

Users never call BindApp directly. The mount system calls it before Init on first mount, and again after UpdateProps on subsequent renders (to bind any fresh Events fields from the props update).

When to implement: Almost never manually. The .gsx code generator handles this. Only implement it if you're building a component without .gsx and it contains State or Events fields.

func (c *counter) BindApp(app *tui.App) {
    c.count.BindApp(app)
}

AppUnbinder

type AppUnbinder interface {
    UnbindApp()
}

Called by the framework when a component leaves the tree: on unmount during sweep, or when the root is replaced via SetRootComponent, SetRoot, or SetRootView. This detaches app-bound resources such as topic-based Events[T] subscriptions.

You usually don't implement this manually. Generated code handles it for .gsx components that contain Events fields.

PropsUpdater

type PropsUpdater interface {
    UpdateProps(fresh Component)
}

Called on cached component instances when the parent re-renders and the component is mounted again at the same position. The fresh parameter is a new instance created by the factory function, containing the updated props. The cached instance should copy any relevant fields from fresh.

Without PropsUpdater, a mounted component's props are fixed at the values passed during the first render. Implement this interface when the component's constructor takes parameters that may change across renders.

When to implement: When your component receives props from a parent and those props can change.

type statusBar struct {
    label   string
    count   *tui.State[int]
}

func StatusBar(label string) *statusBar {
    return &statusBar{
        label: label,
        count: tui.NewState(0),
    }
}

func (s *statusBar) UpdateProps(fresh tui.Component) {
    if f, ok := fresh.(*statusBar); ok {
        s.label = f.label
        // Don't copy count, that's internal state, not a prop
    }
}

Viewable

type Viewable interface {
    GetRoot() *Element
    GetWatchers() []Watcher
}

Implemented by types that provide a root element tree. *Element, generated view structs, and struct components all implement this. Used by App.SetRootView, PrintAboveElement, and StreamWriter.WriteElement.

*Element implements Viewable directly (returning itself from GetRoot() and nil from GetWatchers()), so you can pass either a raw element or a templ-generated view struct to any API that accepts Viewable.

When to implement: When you have a custom type that wraps an element tree and optional watchers.

Focusable

type Focusable interface {
    IsFocusable() bool
    HandleEvent(event Event) bool
    Focus()
    Blur()
}

Implemented by elements that can receive keyboard focus. Element satisfies this interface directly via WithFocusable(true), so custom implementations are uncommon.

IsFocusable returns whether the element can receive focus. HandleEvent processes keyboard/mouse events (returns true if consumed). Focus and Blur are called by the focus manager when focus changes.

When to implement: Rarely. Use WithFocusable, WithOnFocus, and WithOnBlur options on elements instead. See Focus Reference for details.

Watcher

type Watcher interface {
    Start(eventQueue chan<- func(), stopCh <-chan struct{})
}

A background task that sends handler functions to the app's event loop. The framework calls Start during component mounting. The goroutine should select on both its data source and stopCh, exiting when stopCh closes.

Built-in implementations: OnTimer (periodic callbacks) and Watch/NewChannelWatcher (channel readers). See Watchers Reference for details.

When to implement: When you need a custom background data source beyond timers and channels.

type fileWatcher struct {
    path    string
    handler func([]byte)
}

func (w *fileWatcher) Start(eventQueue chan<- func(), stopCh <-chan struct{}) {
    go func() {
        ticker := time.NewTicker(2 * time.Second)
        defer ticker.Stop()
        for {
            select {
            case <-stopCh:
                return
            case <-ticker.C:
                data, err := os.ReadFile(w.path)
                if err != nil {
                    continue
                }
                d := data
                select {
                case eventQueue <- func() { w.handler(d) }:
                case <-stopCh:
                    return
                }
            }
        }
    }()
}

Terminal

type Terminal interface {
    Size() (width, height int)
    Flush(changes []CellChange)
    Clear()
    ClearToEnd()
    SetCursor(x, y int)
    HideCursor()
    ShowCursor()
    EnterRawMode() error
    ExitRawMode() error
    EnterAltScreen()
    ExitAltScreen()
    EnableMouse()
    DisableMouse()
    Caps() Capabilities
    WriteDirect([]byte) (int, error)
}

Abstracts terminal operations. The framework ships two implementations: ANSITerminal for real terminals and MockTerminal for testing.

When to implement: You won't need to. Use ANSITerminal for production and MockTerminal for tests. See Terminal Reference for the full API.

EventReader

type EventReader interface {
    PollEvent(timeout time.Duration) (Event, bool)
    Close() error
}

Reads input events from the terminal. PollEvent blocks up to timeout waiting for an event. Returns (event, true) on success, (nil, false) on timeout. A timeout of 0 performs a non-blocking check. A negative timeout blocks indefinitely.

The framework ships NewEventReader for real stdin input and MockEventReader for testing.

When to implement: You won't need to. Use NewEventReader for production and MockEventReader for tests.

InterruptibleReader

type InterruptibleReader interface {
    EventReader

    EnableInterrupt() error
    Interrupt() error
}

Extends EventReader with the ability to wake up a blocking PollEvent call. Used internally for blocking input mode (InputLatencyBlocking) where PollEvent(-1) would otherwise block forever. The interrupt mechanism uses a self-pipe: EnableInterrupt creates the pipe, and Interrupt writes a byte to it, causing select() to return.

When to implement: This is an internal detail of the blocking input mode. The built-in stdinReader and MockEventReader already implement it.

Interface Discovery Order

When the framework mounts a component via App.Mount, it checks interfaces in this order:

  1. AppBinder -- bind state and events to the app
  2. Initializer -- run setup, capture cleanup
  3. Component -- call Render to get the element tree

On cached re-renders (same mount position):

  1. PropsUpdater -- update props from a fresh factory instance
  2. AppBinder -- rebind (fresh Events fields may be unbound after props update)
  3. Component -- call Render

During event dispatch (tree walk):

  • KeyListener -- checked on each component in the tree for key bindings
  • MouseListener -- checked on each component in the tree for mouse handling

During root setup:

  • WatcherProvider -- watchers are discovered and started

See Also