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:
AppBinder.BindApp-- wire upStateandEventsfieldsInitializer.Init-- run setup logic, capture cleanup functionComponent.Render-- produce the element treeWatcherProvider.Watchers-- discover and start background watchersKeyListener.KeyMapandMouseListener.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. For .gsx receiver components the generator emits this automatically; see Generated lifecycle methods for the override and delegation rules.
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
}
}
Generated lifecycle methods
For a receiver component defined in a .gsx file (templ (c *T) Render()), the generator emits UpdateProps, BindApp, and UnbindApp automatically when the struct has fields that need them. If you declare one of these methods yourself, in the .gsx file or in any plain .go file of the same package, the generator skips its version and yours is used. Test files (_test.go) are ignored so a test-only method cannot suppress a method production builds need.
Because generation reads sibling files, adding or removing one of these methods in a .go file changes what the .gsx file should generate. Rerun tui generate after such an edit; until then the stale generated file may fail to compile.
Each generated method is a thin wrapper around an unexported helper containing the actual logic:
| Generated method | Delegation helper |
|---|---|
UpdateProps |
updatePropsFields(fresh Component) copies prop fields |
BindApp |
bindAppFields(app *App) wires State, Events, and *App fields |
UnbindApp |
unbindAppFields() detaches Events subscriptions |
The helpers are always emitted, so an override can delegate the generated work and add custom behavior on top:
func (c *statusBar) UpdateProps(fresh tui.Component) {
c.updatePropsFields(fresh)
c.recomputeLayout()
}
The three helper names are reserved by the generator; declaring your own method with one of these names on a templ receiver type is rejected at tui generate/tui check time. For function templs (templ Name()), the generator also reserves the <Name>View type name for the generated view struct.
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:
- AppBinder -- bind state and events to the app
- Initializer -- run setup, capture cleanup
- Component -- call
Renderto get the element tree
On cached re-renders (same mount position):
- PropsUpdater -- update props from a fresh factory instance
- AppBinder -- rebind (fresh Events fields may be unbound after props update)
- 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
- App Reference -- app lifecycle,
Mount, root management - Events Reference --
KeyEvent,MouseEvent,KeyMaptypes - Focus Reference --
Focusableinterface and focus management - Watchers Reference --
Watcherimplementations in detail - Terminal Reference --
Terminalinterface andANSITerminal - Testing Reference --
MockTerminalandMockEventReader