Skip to content

Commit 4f955f2

Browse files
committed
fix(tui): mouse scroll ansi parsing and perf
1 parent bbeb579 commit 4f955f2

File tree

2 files changed

+68
-71
lines changed

2 files changed

+68
-71
lines changed

packages/tui/input/driver.go

Lines changed: 66 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -31,42 +31,20 @@ type win32InputState struct {
3131

3232
// Reader represents an input event reader. It reads input events and parses
3333
// escape sequences from the terminal input buffer and translates them into
34-
// human-readable events.
34+
// humanreadable events.
3535
type Reader struct {
36-
rd cancelreader.CancelReader
37-
table map[string]Key // table is a lookup table for key sequences.
38-
39-
term string // term is the terminal name $TERM.
40-
41-
// paste is the bracketed paste mode buffer.
42-
// When nil, bracketed paste mode is disabled.
43-
paste []byte
44-
45-
buf [256]byte // do we need a larger buffer?
46-
47-
// partialSeq holds incomplete escape sequences that need more data
48-
partialSeq []byte
49-
50-
// keyState keeps track of the current Windows Console API key events state.
51-
// It is used to decode ANSI escape sequences and utf16 sequences.
52-
keyState win32InputState
53-
54-
parser Parser
55-
logger Logger
36+
rd cancelreader.CancelReader
37+
table map[string]Key // table is a lookup table for key sequences.
38+
term string // $TERM
39+
paste []byte // bracketed paste buffer; nil when disabled
40+
buf [256]byte // read buffer
41+
partialSeq []byte // holds incomplete escape sequences
42+
keyState win32InputState
43+
parser Parser
44+
logger Logger
5645
}
5746

58-
// NewReader returns a new input event reader. The reader reads input events
59-
// from the terminal and parses escape sequences into human-readable events. It
60-
// supports reading Terminfo databases. See [Parser] for more information.
61-
//
62-
// Example:
63-
//
64-
// r, _ := input.NewReader(os.Stdin, os.Getenv("TERM"), 0)
65-
// defer r.Close()
66-
// events, _ := r.ReadEvents()
67-
// for _, ev := range events {
68-
// log.Printf("%v", ev)
69-
// }
47+
// NewReader returns a new input event reader.
7048
func NewReader(r io.Reader, termType string, flags int) (*Reader, error) {
7149
d := new(Reader)
7250
cr, err := newCancelreader(r, flags)
@@ -82,46 +60,38 @@ func NewReader(r io.Reader, termType string, flags int) (*Reader, error) {
8260
}
8361

8462
// SetLogger sets a logger for the reader.
85-
func (d *Reader) SetLogger(l Logger) {
86-
d.logger = l
87-
}
63+
func (d *Reader) SetLogger(l Logger) { d.logger = l }
8864

89-
// Read implements [io.Reader].
90-
func (d *Reader) Read(p []byte) (int, error) {
91-
return d.rd.Read(p) //nolint:wrapcheck
92-
}
65+
// Read implements io.Reader.
66+
func (d *Reader) Read(p []byte) (int, error) { return d.rd.Read(p) }
9367

9468
// Cancel cancels the underlying reader.
95-
func (d *Reader) Cancel() bool {
96-
return d.rd.Cancel()
97-
}
69+
func (d *Reader) Cancel() bool { return d.rd.Cancel() }
9870

9971
// Close closes the underlying reader.
100-
func (d *Reader) Close() error {
101-
return d.rd.Close() //nolint:wrapcheck
102-
}
72+
func (d *Reader) Close() error { return d.rd.Close() }
10373

10474
func (d *Reader) readEvents() ([]Event, error) {
10575
nb, err := d.rd.Read(d.buf[:])
10676
if err != nil {
107-
return nil, err //nolint:wrapcheck
77+
return nil, err
10878
}
10979

11080
var events []Event
11181

112-
// Combine any partial sequence from previous read with new data
82+
// Combine any partial sequence from previous read with new data.
11383
var buf []byte
11484
if len(d.partialSeq) > 0 {
11585
buf = make([]byte, len(d.partialSeq)+nb)
11686
copy(buf, d.partialSeq)
11787
copy(buf[len(d.partialSeq):], d.buf[:nb])
118-
d.partialSeq = nil // clear the partial sequence
88+
d.partialSeq = nil
11989
} else {
12090
buf = d.buf[:nb]
12191
}
12292

123-
// Lookup table first
124-
if bytes.HasPrefix(buf, []byte{'\x1b'}) {
93+
// Fast path: direct lookup for simple escape sequences.
94+
if bytes.HasPrefix(buf, []byte{0x1b}) {
12595
if k, ok := d.table[string(buf)]; ok {
12696
if d.logger != nil {
12797
d.logger.Printf("input: %q", buf)
@@ -133,24 +103,23 @@ func (d *Reader) readEvents() ([]Event, error) {
133103

134104
var i int
135105
for i < len(buf) {
136-
nb, ev := d.parser.parseSequence(buf[i:])
137-
if d.logger != nil && nb > 0 {
138-
d.logger.Printf("input: %q", buf[i:i+nb])
106+
consumed, ev := d.parser.parseSequence(buf[i:])
107+
if d.logger != nil && consumed > 0 {
108+
d.logger.Printf("input: %q", buf[i:i+consumed])
139109
}
140110

141-
// Handle incomplete sequences - when parseSequence returns (0, nil)
142-
// it means we need more data to complete the sequence
143-
if nb == 0 && ev == nil {
144-
// Store the remaining data for the next read
145-
remaining := len(buf) - i
146-
if remaining > 0 {
147-
d.partialSeq = make([]byte, remaining)
111+
// Incomplete sequence – store remainder and exit.
112+
if consumed == 0 && ev == nil {
113+
rem := len(buf) - i
114+
if rem > 0 {
115+
d.partialSeq = make([]byte, rem)
148116
copy(d.partialSeq, buf[i:])
149117
}
150118
break
151119
}
152120

153-
// Handle bracketed-paste
121+
// Handle bracketed paste specially so we don’t emit a paste event for
122+
// every byte.
154123
if d.paste != nil {
155124
if _, ok := ev.(PasteEndEvent); !ok {
156125
d.paste = append(d.paste, buf[i])
@@ -160,15 +129,9 @@ func (d *Reader) readEvents() ([]Event, error) {
160129
}
161130

162131
switch ev.(type) {
163-
// case UnknownEvent:
164-
// // If the sequence is not recognized by the parser, try looking it up.
165-
// if k, ok := d.table[string(buf[i:i+nb])]; ok {
166-
// ev = KeyPressEvent(k)
167-
// }
168132
case PasteStartEvent:
169133
d.paste = []byte{}
170134
case PasteEndEvent:
171-
// Decode the captured data into runes.
172135
var paste []rune
173136
for len(d.paste) > 0 {
174137
r, w := utf8.DecodeRune(d.paste)
@@ -177,7 +140,7 @@ func (d *Reader) readEvents() ([]Event, error) {
177140
}
178141
d.paste = d.paste[w:]
179142
}
180-
d.paste = nil // reset the buffer
143+
d.paste = nil
181144
events = append(events, PasteEvent(paste))
182145
case nil:
183146
i++
@@ -189,8 +152,41 @@ func (d *Reader) readEvents() ([]Event, error) {
189152
} else {
190153
events = append(events, ev)
191154
}
192-
i += nb
155+
i += consumed
193156
}
194157

158+
// Collapse bursts of wheel/motion events into a single event each.
159+
events = coalesceMouseEvents(events)
195160
return events, nil
196161
}
162+
163+
// coalesceMouseEvents reduces the volume of MouseWheelEvent and MouseMotionEvent
164+
// objects that arrive in rapid succession by keeping only the most recent
165+
// event in each contiguous run.
166+
func coalesceMouseEvents(in []Event) []Event {
167+
if len(in) < 2 {
168+
return in
169+
}
170+
171+
out := make([]Event, 0, len(in))
172+
for _, ev := range in {
173+
switch ev.(type) {
174+
case MouseWheelEvent:
175+
if len(out) > 0 {
176+
if _, ok := out[len(out)-1].(MouseWheelEvent); ok {
177+
out[len(out)-1] = ev // replace previous wheel event
178+
continue
179+
}
180+
}
181+
case MouseMotionEvent:
182+
if len(out) > 0 {
183+
if _, ok := out[len(out)-1].(MouseMotionEvent); ok {
184+
out[len(out)-1] = ev // replace previous motion event
185+
continue
186+
}
187+
}
188+
}
189+
out = append(out, ev)
190+
}
191+
return out
192+
}

packages/tui/input/parse.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ func (p *Parser) parseCsi(b []byte) (int, Event) {
303303
return i, CursorPositionEvent{Y: row - 1, X: col - 1}
304304
case 'm' | '<'<<parser.PrefixShift, 'M' | '<'<<parser.PrefixShift:
305305
// Handle SGR mouse
306-
if paramsLen == 3 {
306+
if paramsLen >= 3 {
307+
pa = pa[:3]
307308
return i, parseSGRMouseEvent(cmd, pa)
308309
}
309310
case 'm' | '>'<<parser.PrefixShift:

0 commit comments

Comments
 (0)