terminal_reader.go

  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	// Timeout is the escape character timeout duration. Most escape sequences
 59	// start with an escape character [ansi.ESC] and are followed by one or
 60	// more characters. If the next character is not received within this
 61	// timeout, the reader will assume that the escape sequence is complete and
 62	// will process the received characters as a complete escape sequence.
 63	//
 64	// By default, this is set to [DefaultEscTimeout] (50 milliseconds).
 65	Timeout time.Duration
 66
 67	r     io.Reader
 68	rd    cancelreader.CancelReader
 69	table map[string]Key // table is a lookup table for key sequences.
 70
 71	term string // term is the terminal name $TERM.
 72
 73	// paste is the bracketed paste mode buffer.
 74	// When nil, bracketed paste mode is disabled.
 75	paste []byte
 76
 77	lookup bool   // lookup indicates whether to use the lookup table for key sequences.
 78	buf    []byte // buffer to hold the read data.
 79
 80	// keyState keeps track of the current Windows Console API key events state.
 81	// It is used to decode ANSI escape sequences and utf16 sequences.
 82	keyState win32InputState //nolint:all
 83
 84	// This indicates whether the reader is closed or not. It is used to
 85	// prevent	multiple calls to the Close() method.
 86	closed    bool
 87	started   bool          // started indicates whether the reader has been started.
 88	close     chan struct{} // close is a channel used to signal the reader to close.
 89	closeOnce sync.Once
 90	notify    chan []byte // notify is a channel used to notify the reader of new input events.
 91	timeout   *time.Timer
 92	timedout  atomic.Bool
 93	esc       atomic.Bool
 94	err       atomic.Value // err is the last error encountered by the reader.
 95
 96	logger Logger // The logger to use for debugging.
 97}
 98
 99// NewTerminalReader returns a new input event reader. The reader reads input
100// events from the terminal and parses escape sequences into human-readable
101// events. It supports reading Terminfo databases.
102//
103// Use [TerminalReader.UseTerminfo] to use Terminfo defined key sequences.
104// Use [TerminalReader.Legacy] to control legacy key encoding behavior.
105//
106// Example:
107//
108//	r, _ := input.NewTerminalReader(os.Stdin, os.Getenv("TERM"))
109//	defer r.Close()
110//	events, _ := r.ReadEvents()
111//	for _, ev := range events {
112//	  log.Printf("%v", ev)
113//	}
114func NewTerminalReader(r io.Reader, termType string) *TerminalReader {
115	return &TerminalReader{
116		Timeout: DefaultEscTimeout,
117		r:       r,
118		term:    termType,
119		lookup:  true, // Use lookup table by default.
120	}
121}
122
123// SetLogger sets the logger to use for debugging. If nil, no logging will be
124// performed.
125func (d *TerminalReader) SetLogger(logger Logger) {
126	d.logger = logger
127}
128
129// Start initializes the reader and prepares it for reading input events. It
130// sets up the cancel reader and the key sequence parser. It also sets up the
131// lookup table for key sequences if it is not already set. This function
132// should be called before reading input events.
133func (d *TerminalReader) Start() (err error) {
134	if d.rd == nil {
135		d.rd, err = newCancelreader(d.r)
136		if err != nil {
137			return err
138		}
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.Timeout)
146	d.notify = make(chan []byte)
147	d.close = make(chan struct{}, 1)
148	d.closeOnce = sync.Once{}
149	go d.run()
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	closingFunc := func() error {
198		// If we're closing, make sure to send any remaining events even if
199		// they are incomplete.
200		d.timedout.Store(true)
201		d.sendEvents(events)
202		err, ok := d.err.Load().(error)
203		if !ok {
204			return nil
205		}
206		return err
207	}
208
209	for {
210		select {
211		case <-ctx.Done():
212			return closingFunc()
213		case <-d.close:
214			return closingFunc()
215		case <-d.timeout.C:
216			d.timedout.Store(true)
217			d.sendEvents(events)
218			d.esc.Store(false)
219		case buf := <-d.notify:
220			d.buf = append(d.buf, buf...)
221			if !d.esc.Load() {
222				d.sendEvents(events)
223				d.timedout.Store(false)
224			}
225		}
226	}
227}
228
229func (d *TerminalReader) run() {
230	for {
231		if d.closed {
232			return
233		}
234
235		var readBuf [256]byte
236		n, err := d.rd.Read(readBuf[:])
237		if err != nil {
238			d.err.Store(err)
239			d.closeEvents()
240			return
241		}
242		if d.closed {
243			return
244		}
245
246		d.logf("input: %q", readBuf[:n])
247		// This handles small inputs that start with an ESC like:
248		// - "\x1b" (escape key press)
249		// - "\x1b\x1b" (alt+escape key press)
250		// - "\x1b[" (alt+[ key press)
251		// - "\x1bP" (alt+shift+p key press)
252		// - "\x1bX" (alt+shift+x key press)
253		// - "\x1b_" (alt+_ key press)
254		// - "\x1b^" (alt+^ key press)
255		esc := n > 0 && n <= 2 && readBuf[0] == ansi.ESC
256		if esc {
257			d.esc.Store(true)
258			d.timeout.Reset(d.Timeout)
259		}
260
261		d.notify <- readBuf[:n]
262	}
263}
264
265func (d *TerminalReader) sendEvents(events chan<- Event) {
266	// Lookup table first
267	if d.lookup && d.timedout.Load() && len(d.buf) > 0 && d.buf[0] == ansi.ESC {
268		if k, ok := d.table[string(d.buf)]; ok {
269			events <- KeyPressEvent(k)
270			d.buf = d.buf[:0]
271			return
272		}
273	}
274
275LOOP:
276	for len(d.buf) > 0 {
277		nb, ev := d.parseSequence(d.buf)
278
279		// Handle bracketed-paste
280		if d.paste != nil {
281			if _, ok := ev.(PasteEndEvent); !ok {
282				d.paste = append(d.paste, d.buf[0])
283				d.buf = d.buf[1:]
284				continue
285			}
286		}
287
288		var isUnknownEvent bool
289		switch ev.(type) {
290		case ignoredEvent:
291			ev = nil // ignore this event
292		case UnknownEvent:
293			isUnknownEvent = true
294
295			// If the sequence is not recognized by the parser, try looking it up.
296			if k, ok := d.table[string(d.buf[:nb])]; ok {
297				ev = KeyPressEvent(k)
298			}
299
300			d.logf("unknown sequence: %q", d.buf[:nb])
301			if !d.timedout.Load() {
302				if nb > 0 {
303					// This handles unknown escape sequences that might be incomplete.
304					if slices.Contains([]byte{
305						ansi.ESC, ansi.CSI, ansi.OSC, ansi.DCS, ansi.APC, ansi.SOS, ansi.PM,
306					}, d.buf[0]) {
307						d.esc.Store(true)
308						d.timeout.Reset(d.Timeout)
309					}
310				}
311				// If this is the entire buffer, we can break and assume this
312				// is an incomplete sequence.
313				break LOOP
314			}
315			d.logf("timed out, skipping unknown sequence: %q", d.buf[:nb])
316		case PasteStartEvent:
317			d.paste = []byte{}
318		case PasteEndEvent:
319			// Decode the captured data into runes.
320			var paste []rune
321			for len(d.paste) > 0 {
322				r, w := utf8.DecodeRune(d.paste)
323				if r != utf8.RuneError {
324					paste = append(paste, r)
325				}
326				d.paste = d.paste[w:]
327			}
328			d.paste = nil // reset the d.buffer
329			events <- PasteEvent(paste)
330		}
331
332		if ev != nil {
333			if !isUnknownEvent && d.esc.Load() {
334				// If we are in an escape sequence, and the event is a valid
335				// one, we need to reset the escape state.
336				d.esc.Store(false)
337			}
338
339			if mevs, ok := ev.(MultiEvent); ok {
340				for _, mev := range mevs {
341					events <- mev
342				}
343			} else {
344				events <- ev
345			}
346		}
347
348		d.buf = d.buf[nb:]
349	}
350}
351
352func (d *TerminalReader) logf(format string, v ...interface{}) {
353	if d.logger == nil {
354		return
355	}
356	d.logger.Printf(format, v...)
357}