1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
|
# 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
```go
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.
```gsx
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
```go
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.
```go
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
```go
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.
```go
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
```go
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.
```go
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
```go
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).
```go
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
```go
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.
```go
func (c *counter) BindApp(app *tui.App) {
c.count.BindApp(app)
}
```
## AppUnbinder
```go
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
```go
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.
```go
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
```go
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
```go
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](focus.md) for details.
## Watcher
```go
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](watchers.md) for details.
**When to implement:** When you need a custom background data source beyond timers and channels.
```go
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
```go
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](terminal.md) for the full API.
## EventReader
```go
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
```go
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
- [App Reference](app.md) -- app lifecycle, `Mount`, root management
- [Events Reference](events.md) -- `KeyEvent`, `MouseEvent`, `KeyMap` types
- [Focus Reference](focus.md) -- `Focusable` interface and focus management
- [Watchers Reference](watchers.md) -- `Watcher` implementations in detail
- [Terminal Reference](terminal.md) -- `Terminal` interface and `ANSITerminal`
- [Testing Reference](testing.md) -- `MockTerminal` and `MockEventReader`
|