gitstack

grindlemire/go-tui code browser

3.4 KB Go 142 lines 2026-03-07 · a1db92f raw
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
package tui

import (
	"time"

	"github.com/grindlemire/go-tui/internal/debug"
)

// Watcher represents a deferred event source that starts when the app runs.
// Watchers are collected during component construction and started by SetRoot.
type Watcher interface {
	// Start begins the watcher goroutine. Called by App.SetRoot().
	// The eventQueue channel and stopCh are provided by the App.
	Start(eventQueue chan<- func(), stopCh <-chan struct{})
}

// ChannelWatcher watches a channel and calls handler for each value.
type ChannelWatcher[T any] struct {
	ch      <-chan T
	handler func(T)
}

// NewChannelWatcher creates a watcher that calls fn for each value received on ch.
// The handler is called on the main event loop, not in a separate goroutine.
//
// Example:
//
//	dataCh := make(chan string)
//	w := tui.NewChannelWatcher(dataCh, func(s string) {
//	    // Handle received data
//	})
func NewChannelWatcher[T any](ch <-chan T, fn func(T)) *ChannelWatcher[T] {
	return &ChannelWatcher[T]{
		ch:      ch,
		handler: fn,
	}
}

// Watch creates a channel watcher. The handler is called on the main loop
// whenever data arrives on the channel.
func Watch[T any](ch <-chan T, handler func(T)) Watcher {
	return NewChannelWatcher(ch, handler)
}

// Start the watcher.
func (w *ChannelWatcher[T]) Start(eventQueue chan<- func(), stopCh <-chan struct{}) {
	go func() {
		for {
			select {
			case <-stopCh:
				return
			case val, ok := <-w.ch:
				if !ok {
					return // Channel closed
				}
				// Capture val for closure
				v := val
				select {
				case eventQueue <- func() {
					w.handler(v)
				}:
				case <-stopCh:
					return
				}
			}
		}
	}()
}

// stateWatcher watches a State[T] and calls handler when the value changes.
// The handler also fires once at start time with the current value.
type stateWatcher[T any] struct {
	state   *State[T]
	handler func(T)
	stopped chan struct{} // closed after unbind completes; nil unless set by tests
}

// OnChange creates a watcher that calls handler when the state value changes.
// The handler is also called once at start time with the current value.
// The handler runs on the main event loop.
//
// Example:
//
//	tui.OnChange(c.selectedTab, func(tab string) {
//	    c.contentArea.ScrollTo(0)
//	})
func OnChange[T any](state *State[T], handler func(T)) Watcher {
	return &stateWatcher[T]{state: state, handler: handler}
}

// Start the watcher.
func (w *stateWatcher[T]) Start(eventQueue chan<- func(), stopCh <-chan struct{}) {
	// Fire once with the current value.
	w.handler(w.state.Get())

	unbind := w.state.Bind(func(v T) {
		w.handler(v)
	})

	go func() {
		<-stopCh
		unbind()
		if w.stopped != nil {
			close(w.stopped)
		}
	}()
}

// timerWatcher fires at a regular interval.
type timerWatcher struct {
	interval time.Duration
	handler  func()
}

// OnTimer creates a timer watcher that fires at the given interval.
// The handler is called on the main loop.
func OnTimer(interval time.Duration, handler func()) Watcher {
	return &timerWatcher{interval: interval, handler: handler}
}

// Start the watcher.
func (w *timerWatcher) Start(eventQueue chan<- func(), stopCh <-chan struct{}) {
	go func() {
		debug.Log("timerWatcher started")
		ticker := time.NewTicker(w.interval)
		defer ticker.Stop()

		for {
			select {
			case <-stopCh:
				return
			case <-ticker.C:
				debug.Log("timerWatcher ticked")
				select {
				case eventQueue <- w.handler:
				case <-stopCh:
					return
				}
			}
		}
	}()
}