gitstack

grindlemire/go-tui code browser

3.1 KB Go 134 lines 2026-03-18 · 60730a0 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
package tui

// Dispatch routes a single event through go-tui's dispatch system.
// KeyEvent goes through the dispatch table (component model) or global key handler
// (legacy), then falls through to the focus manager.
// MouseEvent is translated for inline mode, dispatched to MouseListener components,
// then hit-tested against elements.
// ResizeEvent updates buffer dimensions.
// UpdateEvent executes the queued closure.
// Returns true if the event was consumed.
func (a *App) Dispatch(event Event) bool {
	switch e := event.(type) {
	case UpdateEvent:
		if e.fn != nil {
			e.fn()
		}
		return true

	case KeyEvent:
		e.app = a
		// Component model path: use broadcast dispatch table.
		if a.dispatchTable != nil {
			if a.dispatchTable.dispatch(e) {
				return true
			}
			// Ctrl+Z fallback if not consumed
			if e.Key == KeyRune && e.Rune == 'z' && e.Mod == ModCtrl {
				a.suspend()
				return true
			}
		} else {
			// Legacy path: global key handler
			if a.globalKeyHandler != nil && a.globalKeyHandler(e) {
				return true
			}
			if e.Key == KeyRune && e.Rune == 'z' && e.Mod == ModCtrl {
				a.suspend()
				return true
			}
		}
		return a.focus.Dispatch(e)

	case MouseEvent:
		e.app = a
		// Inline mode: translate terminal-space Y to buffer-space Y
		if !a.inAlternateScreen && a.inlineHeight > 0 {
			e.Y -= a.inlineStartRow
			if e.Y < 0 || e.Y >= a.inlineHeight {
				return false
			}
		}
		// Component model: dispatch to MouseListener components first
		if a.dispatchMouseToComponents(e) {
			return true
		}
		// Element hit testing
		if a.root == nil {
			return false
		}
		if target := a.root.ElementAtPoint(e.X, e.Y); target != nil {
			return target.HandleEvent(e)
		}
		return false

	case ResizeEvent:
		if a.inAlternateScreen {
			a.buffer.Resize(e.Width, e.Height)
		} else if a.inlineHeight > 0 {
			a.syncInlineGeometryOnResize(e.Width, e.Height)
		} else {
			a.buffer.Resize(e.Width, e.Height)
		}
		if a.root != nil {
			a.root.MarkDirty()
		}
		a.needsFullRedraw = true
		return true
	}

	// Unknown event types fall through to the focus manager.
	return a.focus.Dispatch(event)
}

// dispatchMouseToComponents walks the component tree and dispatches a mouse
// event to all MouseListener components. Returns true if any consumed it.
func (a *App) dispatchMouseToComponents(me MouseEvent) bool {
	if a.root == nil {
		return false
	}
	consumed := false
	walkComponents(a.rootComponent, a.root, func(comp Component) {
		if consumed {
			return
		}
		if ml, ok := comp.(MouseListener); ok {
			if ml.HandleMouse(me) {
				consumed = true
			}
		}
	})
	return consumed
}

// readInputEvents reads terminal input in a goroutine and queues events.
func (a *App) readInputEvents() {
	for {
		select {
		case <-a.stopCh:
			return
		default:
		}

		event, ok := a.reader.PollEvent(a.inputLatency)
		if !ok {
			continue
		}

		select {
		case a.inputEvents <- event:
		case <-a.stopCh:
			return
		}
	}
}

func (a *App) syncInlineGeometryOnResize(width, termHeight int) {
	a.inlineStartRow = termHeight - a.inlineHeight
	if a.buffer.Width() == width {
		return
	}

	a.buffer.Resize(width, a.inlineHeight)
	a.invalidateInlineLayoutForWidthChange(a.inlineStartRow)
}