summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/input/driver.go
blob: 1e34677aba6fecb1a451a89e75ea9c23330f4438 (plain)
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
//nolint:unused,revive,nolintlint
package input

import (
	"bytes"
	"io"
	"unicode/utf8"

	"github.com/muesli/cancelreader"
)

// Logger is a simple logger interface.
type Logger interface {
	Printf(format string, v ...any)
}

// win32InputState is a state machine for parsing key events from the Windows
// Console API into escape sequences and utf8 runes, and keeps track of the last
// control key state to determine modifier key changes. It also keeps track of
// the last mouse button state and window size changes to determine which mouse
// buttons were released and to prevent multiple size events from firing.
type win32InputState struct {
	ansiBuf                    [256]byte
	ansiIdx                    int
	utf16Buf                   [2]rune
	utf16Half                  bool
	lastCks                    uint32 // the last control key state for the previous event
	lastMouseBtns              uint32 // the last mouse button state for the previous event
	lastWinsizeX, lastWinsizeY int16  // the last window size for the previous event to prevent multiple size events from firing
}

// Reader represents an input event reader. It reads input events and parses
// escape sequences from the terminal input buffer and translates them into
// human‑readable events.
type Reader struct {
	rd         cancelreader.CancelReader
	table      map[string]Key // table is a lookup table for key sequences.
	term       string         // $TERM
	paste      []byte         // bracketed paste buffer; nil when disabled
	buf        [256]byte      // read buffer
	partialSeq []byte         // holds incomplete escape sequences
	keyState   win32InputState
	parser     Parser
	logger     Logger
}

// NewReader returns a new input event reader.
func NewReader(r io.Reader, termType string, flags int) (*Reader, error) {
	d := new(Reader)
	cr, err := newCancelreader(r, flags)
	if err != nil {
		return nil, err
	}

	d.rd = cr
	d.table = buildKeysTable(flags, termType)
	d.term = termType
	d.parser.flags = flags
	return d, nil
}

// SetLogger sets a logger for the reader.
func (d *Reader) SetLogger(l Logger) { d.logger = l }

// Read implements io.Reader.
func (d *Reader) Read(p []byte) (int, error) { return d.rd.Read(p) }

// Cancel cancels the underlying reader.
func (d *Reader) Cancel() bool { return d.rd.Cancel() }

// Close closes the underlying reader.
func (d *Reader) Close() error { return d.rd.Close() }

func (d *Reader) readEvents() ([]Event, error) {
	nb, err := d.rd.Read(d.buf[:])
	if err != nil {
		return nil, err
	}

	var events []Event

	// Combine any partial sequence from previous read with new data.
	var buf []byte
	if len(d.partialSeq) > 0 {
		buf = make([]byte, len(d.partialSeq)+nb)
		copy(buf, d.partialSeq)
		copy(buf[len(d.partialSeq):], d.buf[:nb])
		d.partialSeq = nil
	} else {
		buf = d.buf[:nb]
	}

	// Fast path: direct lookup for simple escape sequences.
	if bytes.HasPrefix(buf, []byte{0x1b}) {
		if k, ok := d.table[string(buf)]; ok {
			if d.logger != nil {
				d.logger.Printf("input: %q", buf)
			}
			events = append(events, KeyPressEvent(k))
			return events, nil
		}
	}

	var i int
	for i < len(buf) {
		consumed, ev := d.parser.parseSequence(buf[i:])
		if d.logger != nil && consumed > 0 {
			d.logger.Printf("input: %q", buf[i:i+consumed])
		}

		// Incomplete sequence – store remainder and exit.
		if consumed == 0 && ev == nil {
			rem := len(buf) - i
			if rem > 0 {
				d.partialSeq = make([]byte, rem)
				copy(d.partialSeq, buf[i:])
			}
			break
		}

		// Handle bracketed paste specially so we don’t emit a paste event for
		// every byte.
		if d.paste != nil {
			if _, ok := ev.(PasteEndEvent); !ok {
				d.paste = append(d.paste, buf[i])
				i++
				continue
			}
		}

		switch ev.(type) {
		case PasteStartEvent:
			d.paste = []byte{}
		case PasteEndEvent:
			var paste []rune
			for len(d.paste) > 0 {
				r, w := utf8.DecodeRune(d.paste)
				if r != utf8.RuneError {
					paste = append(paste, r)
				}
				d.paste = d.paste[w:]
			}
			d.paste = nil
			events = append(events, PasteEvent(paste))
		case nil:
			i++
			continue
		}

		if mevs, ok := ev.(MultiEvent); ok {
			events = append(events, []Event(mevs)...)
		} else {
			events = append(events, ev)
		}
		i += consumed
	}

	// Collapse bursts of wheel/motion events into a single event each.
	events = coalesceMouseEvents(events)
	return events, nil
}

// coalesceMouseEvents reduces the volume of MouseWheelEvent and MouseMotionEvent
// objects that arrive in rapid succession by keeping only the most recent
// event in each contiguous run.
func coalesceMouseEvents(in []Event) []Event {
	if len(in) < 2 {
		return in
	}

	out := make([]Event, 0, len(in))
	for _, ev := range in {
		switch ev.(type) {
		case MouseWheelEvent:
			if len(out) > 0 {
				if _, ok := out[len(out)-1].(MouseWheelEvent); ok {
					out[len(out)-1] = ev // replace previous wheel event
					continue
				}
			}
		case MouseMotionEvent:
			if len(out) > 0 {
				if _, ok := out[len(out)-1].(MouseMotionEvent); ok {
					out[len(out)-1] = ev // replace previous motion event
					continue
				}
			}
		}
		out = append(out, ev)
	}
	return out
}