App Reference
Overview
The App type is the top-level container that manages terminal setup, the event loop, rendering, and the component tree. Most programs create an App, set a root component, call Run(), and defer Close(). For custom event loops, use Open() with Events(), Step(), or DispatchEvents() instead of Run().
package main
import (
"fmt"
"os"
tui "github.com/grindlemire/go-tui"
)
func main() {
app, err := tui.NewApp(
tui.WithRootComponent(MyApp()),
)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
defer app.Close()
if err := app.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
Creating an App
NewApp
func NewApp(opts ...AppOption) (*App, error)
Creates a new application. Sets the terminal to raw mode and alternate screen (unless inline mode is configured). Pass AppOption functions to configure behavior.
Mouse behavior defaults:
- Full screen mode: mouse events enabled
- Inline mode: mouse events disabled (preserves terminal scrollback)
- Use
WithMouse()orWithoutMouse()to override
app, err := tui.NewApp(
tui.WithRootComponent(MyApp()),
tui.WithFrameRate(30),
)
NewAppWithReader
func NewAppWithReader(reader EventReader, opts ...AppOption) (*App, error)
Creates an App with a custom EventReader, typically for testing. Accepts the same options as NewApp.
reader := tui.NewMockEventReader(
tui.KeyEvent{Key: tui.KeyEnter},
)
app, err := tui.NewAppWithReader(reader,
tui.WithRootComponent(MyApp()),
)
AppOption Functions
Options configure the App before the event loop starts. Each returns an AppOption (which is func(*App) error).
WithRootComponent
func WithRootComponent(component Component) AppOption
Sets a struct component as the root. This is the most common way to wire up your UI. The component's Render method is called each frame when state is dirty, and the framework manages its lifecycle (key handling, mouse, watchers, mounting).
tui.NewApp(tui.WithRootComponent(MyApp()))
WithRoot
func WithRoot(root *Element) AppOption
Sets a raw *Element as the root. Use this when you're building the element tree manually rather than through a struct component.
WithRootView
func WithRootView(view Viewable) AppOption
Sets a Viewable as the root. A Viewable provides both a root element and a set of watchers. The framework starts the watchers automatically when the view is set.
WithFrameRate
func WithFrameRate(fps int) AppOption
Sets the target render frame rate. Default is 60 fps. Valid range: 1-240.
tui.WithFrameRate(30) // 30 fps, ~33ms per frame
WithInputLatency
func WithInputLatency(d time.Duration) AppOption
Sets the polling timeout for the event reader. Default is tui.InputLatencyBlocking, which blocks until input arrives (zero syscalls while idle). Use a positive duration like 50 * time.Millisecond for polling mode. A value of 0 is not allowed.
tui.WithInputLatency(50 * time.Millisecond) // poll every 50ms instead of blocking
WithEventQueueSize
func WithEventQueueSize(size int) AppOption
Sets the capacity of the internal event queue buffer. Default is 256. Must be at least 1.
WithGlobalKeyHandler
func WithGlobalKeyHandler(fn func(KeyEvent) bool) AppOption
Sets a handler that runs before key events reach the component tree (legacy path). If the handler returns true, the event is consumed and not dispatched further.
When using the component model (struct components with KeyMap()), key dispatch goes through the dispatch table instead. Prefer KeyMap() on your components over global handlers.
tui.WithGlobalKeyHandler(func(ke tui.KeyEvent) bool {
if ke.Key == tui.KeyEscape {
ke.App().Stop()
return true
}
return false
})
WithMouse
func WithMouse() AppOption
Explicitly enables mouse event reporting. Use this to turn on mouse support in inline mode, where it's off by default.
WithoutMouse
func WithoutMouse() AppOption
Explicitly disables mouse event reporting. Use this to turn off mouse support in full screen mode, where it's on by default.
WithCursor
func WithCursor() AppOption
Keeps the cursor visible during app execution. By default, the cursor is hidden.
WithInlineHeight
func WithInlineHeight(rows int) AppOption
Enables inline widget mode. The app manages only the bottom N rows of the terminal, and normal terminal output is preserved above. Must be at least 1.
In inline mode:
- Alternate screen is not used, so terminal history is preserved
- Mouse events are disabled by default
- Use
PrintAbove()/PrintAboveln()to print scrolling content above the widget
tui.WithInlineHeight(5) // 5-row widget at the bottom of the terminal
WithInlineStartupMode
func WithInlineStartupMode(mode InlineStartupMode) AppOption
Configures how inline mode handles existing visible terminal content at startup. See InlineStartupMode Constants for the available modes.
WithOnSuspend
func WithOnSuspend(fn func()) AppOption
Sets a callback that runs before the app suspends on Ctrl+Z. Use this to save state or pause timers before the process stops.
WithOnResume
func WithOnResume(fn func()) AppOption
Sets a callback that runs after the app resumes from suspension (when the user runs fg). Use this to restart timers or refresh stale data.
Lifecycle Methods
Run
func (a *App) Run() error
Starts the main event loop. Blocks until Stop() is called or a SIGINT (Ctrl+C) is received. The loop processes input events, re-renders when state is dirty, and sleeps for the remaining frame budget to maintain a consistent frame rate. Calls Open() internally if the app has not been opened yet, and calls Close() on exit.
if err := app.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
Open
func (a *App) Open() error
Initializes the event loop without blocking. Registers signal handlers (SIGINT, SIGWINCH, SIGTSTP/SIGCONT), starts the input reader goroutine, and performs the initial render. Returns an error if the app is already open.
Use Open() instead of Run() when you want to drive your own event loop. After Open(), use Events(), Dispatch(), Render(), Step(), or DispatchEvents() to process events at your own pace. Call Close() when done.
if err := app.Open(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
defer app.Close()
Stop
func (a *App) Stop()
Signals the event loop to exit gracefully. All watchers receive a stop signal and their goroutines exit. Safe to call multiple times (idempotent).
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) {
ke.App().Stop()
})
Close
func (a *App) Close() error
Restores the terminal to its original state: stops goroutines, cleans up signal handlers, disables mouse, shows cursor, exits alternate screen (or clears the inline region), exits raw mode, and closes the event reader. Safe to call multiple times (idempotent via sync.Once).
app, err := tui.NewApp(...)
if err != nil { ... }
defer app.Close()
Root Management
SetRoot
func (a *App) SetRoot(root *Element)
Sets a raw *Element as the root for rendering. Clears any previous root component, discovers focusable elements and watchers in the new tree, and marks the app dirty.
SetRootView
func (a *App) SetRootView(view Viewable)
Sets a Viewable as the root. Calls BindApp if the view implements AppBinder, extracts the root element via GetRoot(), and starts watchers returned by GetWatchers().
SetRootComponent
func (a *App) SetRootComponent(component Component)
Sets a struct component as the root. Calls BindApp if the component implements AppBinder, renders it to produce the element tree, and marks the app dirty. On subsequent frames, the component's Render method is called again whenever the app is dirty.
Root
func (a *App) Root() *Element
Returns the current root element.
Rendering
Render
func (a *App) Render()
Checks the dirty flag and, if set, re-renders the component tree and flushes changes to the terminal. No-op if nothing has changed since the last render. After rendering, the dispatch table is rebuilt from the current component tree. Handles buffer resizing, inline mode offsets, and the mark-and-sweep cycle for mounted sub-components. Automatically performs a full redraw after a resize.
Safe to call frequently (e.g., after every select case in a manual event loop). The dirty check makes redundant calls cheap.
RenderFull
func (a *App) RenderFull()
Forces a complete redraw of the entire buffer to the terminal. Use this after events that may corrupt the terminal display.
MarkDirty
func (a *App) MarkDirty()
Marks the app as needing a render on the next frame. You rarely need to call this directly; State.Set() and State.Update() call it automatically.
Buffer
func (a *App) Buffer() *Buffer
Returns the underlying double-buffered character grid. For advanced use cases only.
Size
func (a *App) Size() (width, height int)
Returns the current terminal dimensions in columns and rows.
SnapshotFrame
func (a *App) SnapshotFrame() string
Returns the current frame as a trimmed string. Useful for debugging and testing.
Event Handling
Events
func (a *App) Events() <-chan Event
Returns a read-only channel carrying all events: key, mouse, resize, and queued updates. Input events are prioritized over background updates internally. Use this with select to multiplex go-tui events with your own channels.
for {
select {
case ev := <-app.Events():
app.Dispatch(ev)
case msg := <-myChannel:
handleMsg(msg)
case <-app.StopCh():
return
}
app.Render()
}
Dispatch
func (a *App) Dispatch(event Event) bool
Routes a single event through the dispatch system. Returns true if the event was consumed.
- UpdateEvent: executes the queued closure.
- KeyEvent: goes through the dispatch table (component model) or global key handler (legacy path), then falls through to the focus manager.
- MouseEvent: translated for inline mode, dispatched to MouseListener components, then hit-tested against elements.
- ResizeEvent: updates buffer size, marks root dirty, schedules a full redraw.
Used in manual event loops with Events(), and in tests.
app.Dispatch(tui.KeyEvent{Key: tui.KeyEnter})
DispatchEvents
func (a *App) DispatchEvents() bool
Reads and dispatches all pending events from the Events channel. Returns false if the app has been stopped, true otherwise. Non-blocking: returns immediately when no events are pending.
if !app.DispatchEvents() {
return // app stopped
}
app.Render()
Step
func (a *App) Step() bool
Combines DispatchEvents() and Render() into a single call. Returns false if the app has been stopped.
for {
<-ticker.C
if !app.Step() {
return
}
}
SetGlobalKeyHandler
func (a *App) SetGlobalKeyHandler(fn func(KeyEvent) bool)
Sets (or replaces) a handler that runs before key events reach the focus manager. If the handler returns true, the event is consumed.
QueueUpdate
func (a *App) QueueUpdate(fn func())
Enqueues a function to run on the main event loop. Safe to call from any goroutine. If the updates channel is full, the update is dropped. Use this when you need to update state from a background goroutine and you are using Run().
When using Open() with Step() or Events(), you can mutate state directly on the main goroutine without QueueUpdate.
go func() {
result := fetchData()
app.QueueUpdate(func() {
data.Set(result)
})
}()
PollEvent
func (a *App) PollEvent(timeout time.Duration) (Event, bool)
Reads the next input event with a timeout. Returns the event and true if one was available, or a zero event and false on timeout. Convenience wrapper around the EventReader.
Focus
FocusNext
func (a *App) FocusNext()
Moves focus to the next focusable element in document order.
FocusPrev
func (a *App) FocusPrev()
Moves focus to the previous focusable element.
Focused
func (a *App) Focused() Focusable
Returns the currently focused element, or nil if nothing is focused.
Inline Mode
These methods only take effect when the app was created with WithInlineHeight. In full-screen mode they are no-ops.
PrintAbove
func (a *App) PrintAbove(format string, args ...any)
Prints formatted content above the inline widget. Does not add a trailing newline. Must be called from the app's main event loop.
PrintAboveln
func (a *App) PrintAboveln(format string, args ...any)
Same as PrintAbove, but appends a newline. Must be called from the main event loop.
QueuePrintAbove
func (a *App) QueuePrintAbove(format string, args ...any)
Thread-safe version of PrintAbove. Safe to call from any goroutine.
QueuePrintAboveln
func (a *App) QueuePrintAboveln(format string, args ...any)
Thread-safe version of PrintAboveln. Safe to call from any goroutine.
StreamAbove
func (a *App) StreamAbove() *StreamWriter
Returns a *StreamWriter that streams text character by character to the history region above the inline widget. The writer implements io.WriteCloser for plain byte streaming, and adds WriteStyled and WriteGradient methods for styled output. The writer is goroutine-safe: writes are queued onto the main event loop internally.
Closing the writer finalizes the current partial line, making it a permanent row in the history. If a previous stream writer is still open when StreamAbove() is called again, the framework finalizes and closes it before returning the new one.
Returns a no-op writer (silently discards all bytes) when not in inline mode.
go func() {
w := app.StreamAbove()
// Plain write (backward compatible with io.WriteCloser)
fmt.Fprint(w, "hello ")
// Styled write
w.WriteStyled("important", tui.NewStyle().Bold().Foreground(tui.Red))
// Gradient write (per-character color interpolation)
grad := tui.NewGradient(tui.Cyan, tui.Magenta)
w.WriteGradient("gradient text", grad)
// Gradient with base style attributes
w.WriteGradient("bold gradient", grad, tui.NewStyle().Bold())
w.Close()
}()
StreamWriter Methods
| Method | Signature | Description |
|---|---|---|
Write |
Write(p []byte) (int, error) |
Plain byte write (backward compatible). Does not track column position. |
Close |
Close() error |
Finalizes the partial line. |
WriteStyled |
WriteStyled(text string, style Style) (int, error) |
Writes text with ANSI style prefix and reset suffix. Tracks column position. |
WriteGradient |
WriteGradient(text string, g Gradient, base ...Style) (int, error) |
Writes each character with an interpolated gradient foreground color. Optional base style provides attributes (bold, italic) and background. Tracks column position. |
WriteElement |
WriteElement(el *Element) |
Renders an element and inserts its rows mid-stream. Finalizes the current partial line first. Resets column position. |
Column tracking: WriteStyled and WriteGradient advance an internal column counter by each character's display width (CJK-aware). Newlines reset the column to 0. The column wraps at the terminal width. This counter drives gradient color interpolation.
If PrintAbove or PrintAboveln is called while a stream writer is active, the stream's partial line is finalized first, then the new content is printed on the next line. Further writes to the finalized writer return io.ErrClosedPipe.
PrintAboveElement
func (a *App) PrintAboveElement(el *Element)
Renders an element tree and inserts the resulting rows into the inline scrollback as static ANSI text. The element is laid out at the terminal's current width using the standard flexbox layout engine, then each row is converted to ANSI escape sequences and inserted via the same machinery as PrintAboveStyled.
This enables inserting structured content (tables, styled cards, templ component output) into the scrollback alongside streamed text.
Must be called from the app's main event loop. No-op if not in inline mode or if the element is nil.
// Insert a templ-generated table
table := DataTable(myRows)
app.PrintAboveElement(table)
// Insert a hand-built element
el := tui.NewElement(tui.WithText("result"), tui.WithBorder(tui.BorderSingle))
app.PrintAboveElement(el)
Any active stream writer's partial line is finalized before the element is inserted, same as PrintAbove.
QueuePrintAboveElement
func (a *App) QueuePrintAboveElement(el *Element)
Thread-safe version of PrintAboveElement. Safe to call from any goroutine. The element is rendered and inserted on the main event loop.
go func() {
table := buildTable(result)
app.QueuePrintAboveElement(table)
}()
SetInlineHeight
func (a *App) SetInlineHeight(rows int)
Changes the inline widget height at runtime. The height change takes effect immediately. Capped at terminal height, minimum of 1. Should be called from render functions or the main event loop.
InlineHeight
func (a *App) InlineHeight() int
Returns the current inline height. Returns 0 if the app is not in inline mode.
Alternate Screen
These methods let an inline-mode app switch to a full-screen overlay and back, for things like settings panels or help screens that shouldn't affect terminal scrollback.
EnterAlternateScreen
func (a *App) EnterAlternateScreen() error
Switches to alternate screen mode for a full-screen UI overlay. Saves the current inline mode state, clears the inline region, enters the alternate screen, and resizes the buffer to full terminal dimensions. No-op if already in alternate mode.
ExitAlternateScreen
func (a *App) ExitAlternateScreen() error
Returns from alternate screen mode and restores the previous inline mode state. The terminal content from before EnterAlternateScreen reappears. No-op if not in alternate mode.
IsInAlternateScreen
func (a *App) IsInAlternateScreen() bool
Returns true if the app is currently displaying in the alternate screen overlay.
Job Control (Ctrl+Z)
Pressing Ctrl+Z suspends the app, and running fg in the shell resumes it. This works automatically for all go-tui apps.
On suspend, the framework automatically:
- Disables mouse reporting
- Shows the cursor
- Exits the alternate screen (full-screen mode)
- Restores normal terminal mode
On resume, it reverses all of these and forces a full redraw.
Hooks
Use WithOnSuspend and WithOnResume to run custom logic:
app, err := tui.NewApp(
tui.WithOnSuspend(func() {
// Save state before suspend
}),
tui.WithOnResume(func() {
// Refresh data after resume
}),
)
Suspend
func (a *App) Suspend()
Programmatically triggers a suspend (same as Ctrl+Z). Safe to call from any goroutine.
Overriding Ctrl+Z
To prevent Ctrl+Z from suspending, bind KeyCtrlZ with a Stop handler in a component's KeyMap():
func (c *myComponent) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.OnStop(tui.KeyCtrlZ, func(ke tui.KeyEvent) {
// Custom behavior instead of suspend
}),
}
}
State Batching
Batch
func (a *App) Batch(fn func())
Executes fn with all state binding callbacks deferred until fn returns. Multiple Set() calls within a batch coalesce into a single round of binding execution, which avoids redundant intermediate renders.
app.Batch(func() {
firstName.Set("Alice")
lastName.Set("Smith")
}) // Bindings fire once here, not twice
Component Mounting
Mount
func (a *App) Mount(parent Component, key any, factory func() Component) *Element
Creates or retrieves a cached component instance and returns its rendered element tree. Generated code calls this; you typically don't call it directly.
On first call for a given (parent, key) pair: executes factory, caches the instance, calls BindApp and Init() (if implemented). On subsequent calls: returns the cached instance's Render() result. If the instance implements PropsUpdater, UpdateProps is called with a fresh instance so the cached component can pick up new props.
The key must be a comparable value. Generated code passes a plain int for a component declared once, and a MountKey composite for components inside loops, so each loop item gets its own cached instance.
If a cached instance's concrete type differs from what the factory produces (a key collision between two call sites), the stale instance is evicted, its cleanup runs, and the fresh component mounts in its place. The collision is logged through the debug log rather than rendering the wrong component silently.
Stale cache entries are swept after each render pass. When a component disappears from the tree, its cleanup function runs.
MountPersistent
func (a *App) MountPersistent(parent Component, key any, factory func() Component) *Element
Like Mount, but the instance survives the sweep even when it is not rendered. Generated code uses this for component elements (<textarea>, <input>, <modal>, <markdown>) declared outside loops without a key attribute, so a textarea hidden by an if keeps its draft text when it reappears. Component elements inside loops, or with a key={...} attribute, use plain Mount instead: their identity comes from data, and persisting every key ever seen would leak instances as the data changes.
MountKey
func MountKey(site int, parts ...any) any
Builds the comparable cache key generated code passes to Mount for loop call sites. The first argument identifies the call site within the component; each part is one enclosing loop's key value (a slice index, a map key, or the expression from a key={...} attribute). Every part must itself be comparable; a non-comparable value panics when the key enters the cache. Keys built from different call sites or different loop values never compare equal, so distinct items cannot collide.
Other
Terminal
func (a *App) Terminal() Terminal
Returns the underlying Terminal implementation. For advanced use cases such as direct ANSI escape output.
EventQueue
func (a *App) EventQueue() chan<- func()
Returns the event queue channel for manual watcher setup. Prefer WatcherProvider on components instead.
StopCh
func (a *App) StopCh() <-chan struct{}
Returns a channel that closes when the app stops. For manual watcher setup. Prefer WatcherProvider on components instead.
InlineStartupMode Constants
These constants control how inline mode initializes the visible terminal viewport at startup. Pass them to WithInlineStartupMode.
| Constant | Description |
|---|---|
InlineStartupPreserveVisible |
Default. Keeps existing visible rows on launch. Unknown history drains naturally as new PrintAbove content is appended. |
InlineStartupFreshViewport |
Clears the visible viewport immediately. Existing visible rows are discarded. |
InlineStartupSoftReset |
Pushes existing visible rows into scrollback via newline flow, then clears the viewport. |
InputLatencyBlocking Constant
const InputLatencyBlocking = -1 * time.Millisecond
The default value for WithInputLatency. The event reader blocks until input arrives, producing zero syscalls while idle. The framework sets up the interrupt pipe automatically so that SIGWINCH and shutdown can wake the blocked reader.
Interfaces
These interfaces are relevant when working with the App's root management methods.
Viewable
type Viewable interface {
GetRoot() *Element
GetWatchers() []Watcher
}
Implemented by *Element, generated view structs, and struct components. Allows SetRootView to extract the root element and start watchers. Also accepted by PrintAboveElement and StreamWriter.WriteElement.
Component
type Component interface {
Render(app *App) *Element
}
The base interface for struct components. Any struct with a matching Render method can serve as a component.
Optional Component Interfaces
Components can implement these additional interfaces for extended behavior:
| Interface | Method | Purpose |
|---|---|---|
KeyListener |
KeyMap() KeyMap |
Keyboard handling |
MouseListener |
HandleMouse(MouseEvent) bool |
Mouse handling |
WatcherProvider |
Watchers() []Watcher |
Timers and channel watchers |
Initializer |
Init() func() |
Setup on mount, returns cleanup function |
AppBinder |
BindApp(app *App) |
Receives app reference (called automatically by mount) |
AppUnbinder |
UnbindApp() |
Detaches app-bound resources on unmount or root swap |
PropsUpdater |
UpdateProps(fresh Component) |
Receives updated props on re-render from cache |
See the Component Interfaces Reference for full details on each.