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}