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}