1package uv
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "slices"
8 "sync"
9 "sync/atomic"
10 "time"
11 "unicode/utf8"
12
13 "github.com/charmbracelet/x/ansi"
14 "github.com/muesli/cancelreader"
15)
16
17// Logger is a simple logger interface.
18type Logger interface {
19 Printf(format string, v ...interface{})
20}
21
22// win32InputState is a state machine for parsing key events from the Windows
23// Console API into escape sequences and utf8 runes, and keeps track of the last
24// control key state to determine modifier key changes. It also keeps track of
25// the last mouse button state and window size changes to determine which mouse
26// buttons were released and to prevent multiple size events from firing.
27//
28//nolint:all
29type win32InputState struct {
30 ansiBuf [256]byte
31 ansiIdx int
32 utf16Buf [2]rune
33 utf16Half bool
34 lastCks uint32 // the last control key state for the previous event
35 lastMouseBtns uint32 // the last mouse button state for the previous event
36 lastWinsizeX, lastWinsizeY int16 // the last window size for the previous event to prevent multiple size events from firing
37}
38
39// ErrReaderNotStarted is returned when the reader has not been started yet.
40var ErrReaderNotStarted = fmt.Errorf("reader not started")
41
42// DefaultEscTimeout is the default timeout at which the [TerminalReader] will
43// process ESC sequences. It is set to 50 milliseconds.
44const DefaultEscTimeout = 50 * time.Millisecond
45
46// TerminalReader represents an input event reader. It reads input events and
47// parses escape sequences from the terminal input buffer and translates them
48// into human-readable events.
49type TerminalReader struct {
50 SequenceParser
51
52 // MouseMode determines whether mouse events are enabled or not. This is a
53 // platform-specific feature and is only available on Windows. When this is
54 // true, the reader will be initialized to read mouse events using the
55 // Windows Console API.
56 MouseMode *MouseMode
57
58 // EscTimeout is the escape character timeout duration. Most escape
59 // sequences start with an escape character [ansi.ESC] and are followed by
60 // one or more characters. If the next character is not received within
61 // this timeout, the reader will assume that the escape sequence is
62 // complete and will process the received characters as a complete escape
63 // sequence.
64 //
65 // By default, this is set to [DefaultEscTimeout] (50 milliseconds).
66 EscTimeout time.Duration
67
68 r io.Reader
69 rd cancelreader.CancelReader
70 table map[string]Key // table is a lookup table for key sequences.
71
72 term string // term is the terminal name $TERM.
73
74 // paste is the bracketed paste mode buffer.
75 // When nil, bracketed paste mode is disabled.
76 paste []byte
77
78 lookup bool // lookup indicates whether to use the lookup table for key sequences.
79 buf []byte // buffer to hold the read data.
80
81 // keyState keeps track of the current Windows Console API key events state.
82 // It is used to decode ANSI escape sequences and utf16 sequences.
83 keyState win32InputState //nolint:all
84
85 // This indicates whether the reader is closed or not. It is used to
86 // prevent multiple calls to the Close() method.
87 closed bool
88 started bool // started indicates whether the reader has been started.
89 runOnce sync.Once // runOnce is used to ensure that the reader is only started once.
90 close chan struct{} // close is a channel used to signal the reader to close.
91 closeOnce sync.Once
92 notify chan []byte // notify is a channel used to notify the reader of new input events.
93 timeout *time.Timer
94 timedout atomic.Bool
95 esc atomic.Bool
96 err atomic.Value // err is the last error encountered by the reader.
97
98 logger Logger // The logger to use for debugging.
99}
100
101// NewTerminalReader returns a new input event reader. The reader reads input
102// events from the terminal and parses escape sequences into human-readable
103// events. It supports reading Terminfo databases.
104//
105// Use [TerminalReader.UseTerminfo] to use Terminfo defined key sequences.
106// Use [TerminalReader.Legacy] to control legacy key encoding behavior.
107//
108// Example:
109//
110// r, _ := input.NewTerminalReader(os.Stdin, os.Getenv("TERM"))
111// defer r.Close()
112// events, _ := r.ReadEvents()
113// for _, ev := range events {
114// log.Printf("%v", ev)
115// }
116func NewTerminalReader(r io.Reader, termType string) *TerminalReader {
117 return &TerminalReader{
118 EscTimeout: DefaultEscTimeout,
119 r: r,
120 term: termType,
121 lookup: true, // Use lookup table by default.
122 }
123}
124
125// SetLogger sets the logger to use for debugging. If nil, no logging will be
126// performed.
127func (d *TerminalReader) SetLogger(logger Logger) {
128 d.logger = logger
129}
130
131// Start initializes the reader and prepares it for reading input events. It
132// sets up the cancel reader and the key sequence parser. It also sets up the
133// lookup table for key sequences if it is not already set. This function
134// should be called before reading input events.
135func (d *TerminalReader) Start() (err error) {
136 d.rd, err = newCancelreader(d.r)
137 if err != nil {
138 return err
139 }
140 if d.table == nil {
141 d.table = buildKeysTable(d.Legacy, d.term, d.UseTerminfo)
142 }
143 d.started = true
144 d.esc.Store(false)
145 d.timeout = time.NewTimer(d.EscTimeout)
146 d.notify = make(chan []byte)
147 d.close = make(chan struct{}, 1)
148 d.closeOnce = sync.Once{}
149 d.runOnce = sync.Once{}
150 return nil
151}
152
153// Read implements [io.Reader].
154func (d *TerminalReader) Read(p []byte) (int, error) {
155 if err := d.Start(); err != nil {
156 return 0, err
157 }
158 return d.rd.Read(p) //nolint:wrapcheck
159}
160
161// Cancel cancels the underlying reader.
162func (d *TerminalReader) Cancel() bool {
163 if d.rd == nil {
164 return false
165 }
166 return d.rd.Cancel()
167}
168
169// Close closes the underlying reader.
170func (d *TerminalReader) Close() (rErr error) {
171 if d.closed {
172 return nil
173 }
174 if !d.started {
175 return ErrReaderNotStarted
176 }
177 if err := d.rd.Close(); err != nil {
178 return fmt.Errorf("failed to close reader: %w", err)
179 }
180 d.closed = true
181 d.started = false
182 d.closeEvents()
183 return nil
184}
185
186func (d *TerminalReader) closeEvents() {
187 d.closeOnce.Do(func() {
188 close(d.close) // signal the reader to close
189 })
190}
191
192func (d *TerminalReader) receiveEvents(ctx context.Context, events chan<- Event) error {
193 if !d.started {
194 return ErrReaderNotStarted
195 }
196
197 // Start the reader loop if it hasn't been started yet.
198 d.runOnce.Do(func() {
199 go d.run()
200 })
201
202 closingFunc := func() error {
203 // If we're closing, make sure to send any remaining events even if
204 // they are incomplete.
205 d.timedout.Store(true)
206 d.sendEvents(events)
207 err, ok := d.err.Load().(error)
208 if !ok {
209 return nil
210 }
211 return err
212 }
213
214 for {
215 select {
216 case <-ctx.Done():
217 return closingFunc()
218 case <-d.close:
219 return closingFunc()
220 case <-d.timeout.C:
221 d.timedout.Store(true)
222 d.sendEvents(events)
223 d.esc.Store(false)
224 case buf := <-d.notify:
225 d.buf = append(d.buf, buf...)
226 if !d.esc.Load() {
227 d.sendEvents(events)
228 d.timedout.Store(false)
229 }
230 }
231 }
232}
233
234func (d *TerminalReader) run() {
235 for {
236 if d.closed {
237 return
238 }
239
240 var readBuf [256]byte
241 n, err := d.rd.Read(readBuf[:])
242 if err != nil {
243 d.err.Store(err)
244 d.closeEvents()
245 return
246 }
247 if d.closed {
248 return
249 }
250
251 d.logf("input: %q", readBuf[:n])
252 // This handles small inputs that start with an ESC like:
253 // - "\x1b" (escape key press)
254 // - "\x1b\x1b" (alt+escape key press)
255 // - "\x1b[" (alt+[ key press)
256 // - "\x1bP" (alt+shift+p key press)
257 // - "\x1bX" (alt+shift+x key press)
258 // - "\x1b_" (alt+_ key press)
259 // - "\x1b^" (alt+^ key press)
260 esc := n > 0 && n <= 2 && readBuf[0] == ansi.ESC
261 if esc {
262 d.resetEsc()
263 }
264
265 d.notify <- readBuf[:n]
266 }
267}
268
269func (d *TerminalReader) resetEsc() {
270 // Reset the escape sequence state and timer.
271 d.esc.Store(true)
272 d.timeout.Reset(d.EscTimeout)
273}
274
275func (d *TerminalReader) sendEvents(events chan<- Event) {
276 // Lookup table first
277 if d.lookup && d.timedout.Load() && len(d.buf) > 2 && d.buf[0] == ansi.ESC {
278 if k, ok := d.table[string(d.buf)]; ok {
279 events <- KeyPressEvent(k)
280 d.buf = d.buf[:0]
281 return
282 }
283 }
284
285LOOP:
286 for len(d.buf) > 0 {
287 nb, ev := d.parseSequence(d.buf)
288
289 // Handle bracketed-paste
290 if d.paste != nil {
291 if _, ok := ev.(PasteEndEvent); !ok {
292 d.paste = append(d.paste, d.buf[0])
293 d.buf = d.buf[1:]
294 continue
295 }
296 }
297
298 var isUnknownEvent bool
299 switch ev.(type) {
300 case ignoredEvent:
301 ev = nil // ignore this event
302 case UnknownEvent:
303 isUnknownEvent = true
304
305 // If the sequence is not recognized by the parser, try looking it up.
306 if k, ok := d.table[string(d.buf[:nb])]; ok {
307 ev = KeyPressEvent(k)
308 }
309
310 d.logf("unknown sequence: %q", d.buf[:nb])
311 if !d.timedout.Load() {
312 if nb > 0 {
313 // This handles unknown escape sequences that might be incomplete.
314 if slices.Contains([]byte{
315 ansi.ESC, ansi.CSI, ansi.OSC, ansi.DCS, ansi.APC, ansi.SOS, ansi.PM,
316 }, d.buf[0]) {
317 d.resetEsc()
318 }
319 }
320 // If this is the entire buffer, we can break and assume this
321 // is an incomplete sequence.
322 break LOOP
323 }
324 d.logf("timed out, skipping unknown sequence: %q", d.buf[:nb])
325 case PasteStartEvent:
326 d.paste = []byte{}
327 case PasteEndEvent:
328 // Decode the captured data into runes.
329 var paste []rune
330 for len(d.paste) > 0 {
331 r, w := utf8.DecodeRune(d.paste)
332 if r != utf8.RuneError {
333 paste = append(paste, r)
334 }
335 d.paste = d.paste[w:]
336 }
337 d.paste = nil // reset the d.buffer
338 events <- PasteEvent(paste)
339 }
340
341 if ev != nil {
342 if !isUnknownEvent && d.esc.Load() {
343 // If we are in an escape sequence, and the event is a valid
344 // one, we need to reset the escape state.
345 d.esc.Store(false)
346 }
347
348 if mevs, ok := ev.(MultiEvent); ok {
349 for _, mev := range mevs {
350 events <- mev
351 }
352 } else {
353 events <- ev
354 }
355 }
356
357 d.buf = d.buf[nb:]
358 }
359}
360
361func (d *TerminalReader) logf(format string, v ...interface{}) {
362 if d.logger == nil {
363 return
364 }
365 d.logger.Printf(format, v...)
366}