blob: bddb2e2aebd4a194afd0c7186289dcdeb1718a0b [file] [log] [blame]
David K. Bainbridgebd6b2882021-08-26 13:31:02 +00001// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package term
6
7import (
8 "bytes"
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +05309 "fmt"
David K. Bainbridgebd6b2882021-08-26 13:31:02 +000010 "io"
11 "runtime"
12 "strconv"
13 "sync"
14 "unicode/utf8"
15)
16
17// EscapeCodes contains escape sequences that can be written to the terminal in
18// order to achieve different styles of text.
19type EscapeCodes struct {
20 // Foreground colors
21 Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte
22
23 // Reset all attributes
24 Reset []byte
25}
26
27var vt100EscapeCodes = EscapeCodes{
28 Black: []byte{keyEscape, '[', '3', '0', 'm'},
29 Red: []byte{keyEscape, '[', '3', '1', 'm'},
30 Green: []byte{keyEscape, '[', '3', '2', 'm'},
31 Yellow: []byte{keyEscape, '[', '3', '3', 'm'},
32 Blue: []byte{keyEscape, '[', '3', '4', 'm'},
33 Magenta: []byte{keyEscape, '[', '3', '5', 'm'},
34 Cyan: []byte{keyEscape, '[', '3', '6', 'm'},
35 White: []byte{keyEscape, '[', '3', '7', 'm'},
36
37 Reset: []byte{keyEscape, '[', '0', 'm'},
38}
39
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053040// A History provides a (possibly bounded) queue of input lines read by [Terminal.ReadLine].
41type History interface {
42 // Add will be called by [Terminal.ReadLine] to add
43 // a new, most recent entry to the history.
44 // It is allowed to drop any entry, including
45 // the entry being added (e.g., if it's deemed an invalid entry),
46 // the least-recent entry (e.g., to keep the history bounded),
47 // or any other entry.
48 Add(entry string)
49
50 // Len returns the number of entries in the history.
51 Len() int
52
53 // At returns an entry from the history.
54 // Index 0 is the most-recently added entry and
55 // index Len()-1 is the least-recently added entry.
56 // If index is < 0 or >= Len(), it panics.
57 At(idx int) string
58}
59
David K. Bainbridgebd6b2882021-08-26 13:31:02 +000060// Terminal contains the state for running a VT100 terminal that is capable of
61// reading lines of input.
62type Terminal struct {
63 // AutoCompleteCallback, if non-null, is called for each keypress with
64 // the full input line and the current position of the cursor (in
65 // bytes, as an index into |line|). If it returns ok=false, the key
66 // press is processed normally. Otherwise it returns a replacement line
67 // and the new cursor position.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053068 //
69 // This will be disabled during ReadPassword.
David K. Bainbridgebd6b2882021-08-26 13:31:02 +000070 AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool)
71
72 // Escape contains a pointer to the escape codes for this terminal.
73 // It's always a valid pointer, although the escape codes themselves
74 // may be empty if the terminal doesn't support them.
75 Escape *EscapeCodes
76
77 // lock protects the terminal and the state in this object from
78 // concurrent processing of a key press and a Write() call.
79 lock sync.Mutex
80
81 c io.ReadWriter
82 prompt []rune
83
84 // line is the current line being entered.
85 line []rune
86 // pos is the logical position of the cursor in line
87 pos int
88 // echo is true if local echo is enabled
89 echo bool
90 // pasteActive is true iff there is a bracketed paste operation in
91 // progress.
92 pasteActive bool
93
94 // cursorX contains the current X value of the cursor where the left
95 // edge is 0. cursorY contains the row number where the first row of
96 // the current line is 0.
97 cursorX, cursorY int
98 // maxLine is the greatest value of cursorY so far.
99 maxLine int
100
101 termWidth, termHeight int
102
103 // outBuf contains the terminal data to be sent.
104 outBuf []byte
105 // remainder contains the remainder of any partial key sequences after
106 // a read. It aliases into inBuf.
107 remainder []byte
108 inBuf [256]byte
109
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530110 // History records and retrieves lines of input read by [ReadLine] which
111 // a user can retrieve and navigate using the up and down arrow keys.
112 //
113 // It is not safe to call ReadLine concurrently with any methods on History.
114 //
115 // [NewTerminal] sets this to a default implementation that records the
116 // last 100 lines of input.
117 History History
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000118 // historyIndex stores the currently accessed history entry, where zero
119 // means the immediately previous entry.
120 historyIndex int
121 // When navigating up and down the history it's possible to return to
122 // the incomplete, initial line. That value is stored in
123 // historyPending.
124 historyPending string
125}
126
127// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
128// a local terminal, that terminal must first have been put into raw mode.
129// prompt is a string that is written at the start of each input line (i.e.
130// "> ").
131func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
132 return &Terminal{
133 Escape: &vt100EscapeCodes,
134 c: c,
135 prompt: []rune(prompt),
136 termWidth: 80,
137 termHeight: 24,
138 echo: true,
139 historyIndex: -1,
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530140 History: &stRingBuffer{},
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000141 }
142}
143
144const (
145 keyCtrlC = 3
146 keyCtrlD = 4
147 keyCtrlU = 21
148 keyEnter = '\r'
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530149 keyLF = '\n'
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000150 keyEscape = 27
151 keyBackspace = 127
152 keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota
153 keyUp
154 keyDown
155 keyLeft
156 keyRight
157 keyAltLeft
158 keyAltRight
159 keyHome
160 keyEnd
161 keyDeleteWord
162 keyDeleteLine
163 keyClearScreen
164 keyPasteStart
165 keyPasteEnd
166)
167
168var (
169 crlf = []byte{'\r', '\n'}
170 pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'}
171 pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'}
172)
173
174// bytesToKey tries to parse a key sequence from b. If successful, it returns
175// the key and the remainder of the input. Otherwise it returns utf8.RuneError.
176func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
177 if len(b) == 0 {
178 return utf8.RuneError, nil
179 }
180
181 if !pasteActive {
182 switch b[0] {
183 case 1: // ^A
184 return keyHome, b[1:]
185 case 2: // ^B
186 return keyLeft, b[1:]
187 case 5: // ^E
188 return keyEnd, b[1:]
189 case 6: // ^F
190 return keyRight, b[1:]
191 case 8: // ^H
192 return keyBackspace, b[1:]
193 case 11: // ^K
194 return keyDeleteLine, b[1:]
195 case 12: // ^L
196 return keyClearScreen, b[1:]
197 case 23: // ^W
198 return keyDeleteWord, b[1:]
199 case 14: // ^N
200 return keyDown, b[1:]
201 case 16: // ^P
202 return keyUp, b[1:]
203 }
204 }
205
206 if b[0] != keyEscape {
207 if !utf8.FullRune(b) {
208 return utf8.RuneError, b
209 }
210 r, l := utf8.DecodeRune(b)
211 return r, b[l:]
212 }
213
214 if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' {
215 switch b[2] {
216 case 'A':
217 return keyUp, b[3:]
218 case 'B':
219 return keyDown, b[3:]
220 case 'C':
221 return keyRight, b[3:]
222 case 'D':
223 return keyLeft, b[3:]
224 case 'H':
225 return keyHome, b[3:]
226 case 'F':
227 return keyEnd, b[3:]
228 }
229 }
230
231 if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
232 switch b[5] {
233 case 'C':
234 return keyAltRight, b[6:]
235 case 'D':
236 return keyAltLeft, b[6:]
237 }
238 }
239
240 if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) {
241 return keyPasteStart, b[6:]
242 }
243
244 if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) {
245 return keyPasteEnd, b[6:]
246 }
247
248 // If we get here then we have a key that we don't recognise, or a
249 // partial sequence. It's not clear how one should find the end of a
250 // sequence without knowing them all, but it seems that [a-zA-Z~] only
251 // appears at the end of a sequence.
252 for i, c := range b[0:] {
253 if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' {
254 return keyUnknown, b[i+1:]
255 }
256 }
257
258 return utf8.RuneError, b
259}
260
261// queue appends data to the end of t.outBuf
262func (t *Terminal) queue(data []rune) {
263 t.outBuf = append(t.outBuf, []byte(string(data))...)
264}
265
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000266var space = []rune{' '}
267
268func isPrintable(key rune) bool {
269 isInSurrogateArea := key >= 0xd800 && key <= 0xdbff
270 return key >= 32 && !isInSurrogateArea
271}
272
273// moveCursorToPos appends data to t.outBuf which will move the cursor to the
274// given, logical position in the text.
275func (t *Terminal) moveCursorToPos(pos int) {
276 if !t.echo {
277 return
278 }
279
280 x := visualLength(t.prompt) + pos
281 y := x / t.termWidth
282 x = x % t.termWidth
283
284 up := 0
285 if y < t.cursorY {
286 up = t.cursorY - y
287 }
288
289 down := 0
290 if y > t.cursorY {
291 down = y - t.cursorY
292 }
293
294 left := 0
295 if x < t.cursorX {
296 left = t.cursorX - x
297 }
298
299 right := 0
300 if x > t.cursorX {
301 right = x - t.cursorX
302 }
303
304 t.cursorX = x
305 t.cursorY = y
306 t.move(up, down, left, right)
307}
308
309func (t *Terminal) move(up, down, left, right int) {
310 m := []rune{}
311
312 // 1 unit up can be expressed as ^[[A or ^[A
313 // 5 units up can be expressed as ^[[5A
314
315 if up == 1 {
316 m = append(m, keyEscape, '[', 'A')
317 } else if up > 1 {
318 m = append(m, keyEscape, '[')
319 m = append(m, []rune(strconv.Itoa(up))...)
320 m = append(m, 'A')
321 }
322
323 if down == 1 {
324 m = append(m, keyEscape, '[', 'B')
325 } else if down > 1 {
326 m = append(m, keyEscape, '[')
327 m = append(m, []rune(strconv.Itoa(down))...)
328 m = append(m, 'B')
329 }
330
331 if right == 1 {
332 m = append(m, keyEscape, '[', 'C')
333 } else if right > 1 {
334 m = append(m, keyEscape, '[')
335 m = append(m, []rune(strconv.Itoa(right))...)
336 m = append(m, 'C')
337 }
338
339 if left == 1 {
340 m = append(m, keyEscape, '[', 'D')
341 } else if left > 1 {
342 m = append(m, keyEscape, '[')
343 m = append(m, []rune(strconv.Itoa(left))...)
344 m = append(m, 'D')
345 }
346
347 t.queue(m)
348}
349
350func (t *Terminal) clearLineToRight() {
351 op := []rune{keyEscape, '[', 'K'}
352 t.queue(op)
353}
354
355const maxLineLength = 4096
356
357func (t *Terminal) setLine(newLine []rune, newPos int) {
358 if t.echo {
359 t.moveCursorToPos(0)
360 t.writeLine(newLine)
361 for i := len(newLine); i < len(t.line); i++ {
362 t.writeLine(space)
363 }
364 t.moveCursorToPos(newPos)
365 }
366 t.line = newLine
367 t.pos = newPos
368}
369
370func (t *Terminal) advanceCursor(places int) {
371 t.cursorX += places
372 t.cursorY += t.cursorX / t.termWidth
373 if t.cursorY > t.maxLine {
374 t.maxLine = t.cursorY
375 }
376 t.cursorX = t.cursorX % t.termWidth
377
378 if places > 0 && t.cursorX == 0 {
379 // Normally terminals will advance the current position
380 // when writing a character. But that doesn't happen
381 // for the last character in a line. However, when
382 // writing a character (except a new line) that causes
383 // a line wrap, the position will be advanced two
384 // places.
385 //
386 // So, if we are stopping at the end of a line, we
387 // need to write a newline so that our cursor can be
388 // advanced to the next line.
389 t.outBuf = append(t.outBuf, '\r', '\n')
390 }
391}
392
393func (t *Terminal) eraseNPreviousChars(n int) {
394 if n == 0 {
395 return
396 }
397
398 if t.pos < n {
399 n = t.pos
400 }
401 t.pos -= n
402 t.moveCursorToPos(t.pos)
403
404 copy(t.line[t.pos:], t.line[n+t.pos:])
405 t.line = t.line[:len(t.line)-n]
406 if t.echo {
407 t.writeLine(t.line[t.pos:])
408 for i := 0; i < n; i++ {
409 t.queue(space)
410 }
411 t.advanceCursor(n)
412 t.moveCursorToPos(t.pos)
413 }
414}
415
416// countToLeftWord returns then number of characters from the cursor to the
417// start of the previous word.
418func (t *Terminal) countToLeftWord() int {
419 if t.pos == 0 {
420 return 0
421 }
422
423 pos := t.pos - 1
424 for pos > 0 {
425 if t.line[pos] != ' ' {
426 break
427 }
428 pos--
429 }
430 for pos > 0 {
431 if t.line[pos] == ' ' {
432 pos++
433 break
434 }
435 pos--
436 }
437
438 return t.pos - pos
439}
440
441// countToRightWord returns then number of characters from the cursor to the
442// start of the next word.
443func (t *Terminal) countToRightWord() int {
444 pos := t.pos
445 for pos < len(t.line) {
446 if t.line[pos] == ' ' {
447 break
448 }
449 pos++
450 }
451 for pos < len(t.line) {
452 if t.line[pos] != ' ' {
453 break
454 }
455 pos++
456 }
457 return pos - t.pos
458}
459
460// visualLength returns the number of visible glyphs in s.
461func visualLength(runes []rune) int {
462 inEscapeSeq := false
463 length := 0
464
465 for _, r := range runes {
466 switch {
467 case inEscapeSeq:
468 if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
469 inEscapeSeq = false
470 }
471 case r == '\x1b':
472 inEscapeSeq = true
473 default:
474 length++
475 }
476 }
477
478 return length
479}
480
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530481// histroryAt unlocks the terminal and relocks it while calling History.At.
482func (t *Terminal) historyAt(idx int) (string, bool) {
483 t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.
484 defer t.lock.Lock() // panic in At (or Len) protection.
485 if idx < 0 || idx >= t.History.Len() {
486 return "", false
487 }
488 return t.History.At(idx), true
489}
490
491// historyAdd unlocks the terminal and relocks it while calling History.Add.
492func (t *Terminal) historyAdd(entry string) {
493 t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.
494 defer t.lock.Lock() // panic in Add protection.
495 t.History.Add(entry)
496}
497
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000498// handleKey processes the given key and, optionally, returns a line of text
499// that the user has entered.
500func (t *Terminal) handleKey(key rune) (line string, ok bool) {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530501 if t.pasteActive && key != keyEnter && key != keyLF {
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000502 t.addKeyToLine(key)
503 return
504 }
505
506 switch key {
507 case keyBackspace:
508 if t.pos == 0 {
509 return
510 }
511 t.eraseNPreviousChars(1)
512 case keyAltLeft:
513 // move left by a word.
514 t.pos -= t.countToLeftWord()
515 t.moveCursorToPos(t.pos)
516 case keyAltRight:
517 // move right by a word.
518 t.pos += t.countToRightWord()
519 t.moveCursorToPos(t.pos)
520 case keyLeft:
521 if t.pos == 0 {
522 return
523 }
524 t.pos--
525 t.moveCursorToPos(t.pos)
526 case keyRight:
527 if t.pos == len(t.line) {
528 return
529 }
530 t.pos++
531 t.moveCursorToPos(t.pos)
532 case keyHome:
533 if t.pos == 0 {
534 return
535 }
536 t.pos = 0
537 t.moveCursorToPos(t.pos)
538 case keyEnd:
539 if t.pos == len(t.line) {
540 return
541 }
542 t.pos = len(t.line)
543 t.moveCursorToPos(t.pos)
544 case keyUp:
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530545 entry, ok := t.historyAt(t.historyIndex + 1)
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000546 if !ok {
547 return "", false
548 }
549 if t.historyIndex == -1 {
550 t.historyPending = string(t.line)
551 }
552 t.historyIndex++
553 runes := []rune(entry)
554 t.setLine(runes, len(runes))
555 case keyDown:
556 switch t.historyIndex {
557 case -1:
558 return
559 case 0:
560 runes := []rune(t.historyPending)
561 t.setLine(runes, len(runes))
562 t.historyIndex--
563 default:
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530564 entry, ok := t.historyAt(t.historyIndex - 1)
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000565 if ok {
566 t.historyIndex--
567 runes := []rune(entry)
568 t.setLine(runes, len(runes))
569 }
570 }
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530571 case keyEnter, keyLF:
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000572 t.moveCursorToPos(len(t.line))
573 t.queue([]rune("\r\n"))
574 line = string(t.line)
575 ok = true
576 t.line = t.line[:0]
577 t.pos = 0
578 t.cursorX = 0
579 t.cursorY = 0
580 t.maxLine = 0
581 case keyDeleteWord:
582 // Delete zero or more spaces and then one or more characters.
583 t.eraseNPreviousChars(t.countToLeftWord())
584 case keyDeleteLine:
585 // Delete everything from the current cursor position to the
586 // end of line.
587 for i := t.pos; i < len(t.line); i++ {
588 t.queue(space)
589 t.advanceCursor(1)
590 }
591 t.line = t.line[:t.pos]
592 t.moveCursorToPos(t.pos)
593 case keyCtrlD:
594 // Erase the character under the current position.
595 // The EOF case when the line is empty is handled in
596 // readLine().
597 if t.pos < len(t.line) {
598 t.pos++
599 t.eraseNPreviousChars(1)
600 }
601 case keyCtrlU:
602 t.eraseNPreviousChars(t.pos)
603 case keyClearScreen:
604 // Erases the screen and moves the cursor to the home position.
605 t.queue([]rune("\x1b[2J\x1b[H"))
606 t.queue(t.prompt)
607 t.cursorX, t.cursorY = 0, 0
608 t.advanceCursor(visualLength(t.prompt))
609 t.setLine(t.line, t.pos)
610 default:
611 if t.AutoCompleteCallback != nil {
612 prefix := string(t.line[:t.pos])
613 suffix := string(t.line[t.pos:])
614
615 t.lock.Unlock()
616 newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key)
617 t.lock.Lock()
618
619 if completeOk {
620 t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos]))
621 return
622 }
623 }
624 if !isPrintable(key) {
625 return
626 }
627 if len(t.line) == maxLineLength {
628 return
629 }
630 t.addKeyToLine(key)
631 }
632 return
633}
634
635// addKeyToLine inserts the given key at the current position in the current
636// line.
637func (t *Terminal) addKeyToLine(key rune) {
638 if len(t.line) == cap(t.line) {
639 newLine := make([]rune, len(t.line), 2*(1+len(t.line)))
640 copy(newLine, t.line)
641 t.line = newLine
642 }
643 t.line = t.line[:len(t.line)+1]
644 copy(t.line[t.pos+1:], t.line[t.pos:])
645 t.line[t.pos] = key
646 if t.echo {
647 t.writeLine(t.line[t.pos:])
648 }
649 t.pos++
650 t.moveCursorToPos(t.pos)
651}
652
653func (t *Terminal) writeLine(line []rune) {
654 for len(line) != 0 {
655 remainingOnLine := t.termWidth - t.cursorX
656 todo := len(line)
657 if todo > remainingOnLine {
658 todo = remainingOnLine
659 }
660 t.queue(line[:todo])
661 t.advanceCursor(visualLength(line[:todo]))
662 line = line[todo:]
663 }
664}
665
666// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n.
667func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) {
668 for len(buf) > 0 {
669 i := bytes.IndexByte(buf, '\n')
670 todo := len(buf)
671 if i >= 0 {
672 todo = i
673 }
674
675 var nn int
676 nn, err = w.Write(buf[:todo])
677 n += nn
678 if err != nil {
679 return n, err
680 }
681 buf = buf[todo:]
682
683 if i >= 0 {
684 if _, err = w.Write(crlf); err != nil {
685 return n, err
686 }
687 n++
688 buf = buf[1:]
689 }
690 }
691
692 return n, nil
693}
694
695func (t *Terminal) Write(buf []byte) (n int, err error) {
696 t.lock.Lock()
697 defer t.lock.Unlock()
698
699 if t.cursorX == 0 && t.cursorY == 0 {
700 // This is the easy case: there's nothing on the screen that we
701 // have to move out of the way.
702 return writeWithCRLF(t.c, buf)
703 }
704
705 // We have a prompt and possibly user input on the screen. We
706 // have to clear it first.
707 t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */)
708 t.cursorX = 0
709 t.clearLineToRight()
710
711 for t.cursorY > 0 {
712 t.move(1 /* up */, 0, 0, 0)
713 t.cursorY--
714 t.clearLineToRight()
715 }
716
717 if _, err = t.c.Write(t.outBuf); err != nil {
718 return
719 }
720 t.outBuf = t.outBuf[:0]
721
722 if n, err = writeWithCRLF(t.c, buf); err != nil {
723 return
724 }
725
726 t.writeLine(t.prompt)
727 if t.echo {
728 t.writeLine(t.line)
729 }
730
731 t.moveCursorToPos(t.pos)
732
733 if _, err = t.c.Write(t.outBuf); err != nil {
734 return
735 }
736 t.outBuf = t.outBuf[:0]
737 return
738}
739
740// ReadPassword temporarily changes the prompt and reads a password, without
741// echo, from the terminal.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530742//
743// The AutoCompleteCallback is disabled during this call.
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000744func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
745 t.lock.Lock()
746 defer t.lock.Unlock()
747
748 oldPrompt := t.prompt
749 t.prompt = []rune(prompt)
750 t.echo = false
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530751 oldAutoCompleteCallback := t.AutoCompleteCallback
752 t.AutoCompleteCallback = nil
753 defer func() {
754 t.AutoCompleteCallback = oldAutoCompleteCallback
755 }()
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000756
757 line, err = t.readLine()
758
759 t.prompt = oldPrompt
760 t.echo = true
761
762 return
763}
764
765// ReadLine returns a line of input from the terminal.
766func (t *Terminal) ReadLine() (line string, err error) {
767 t.lock.Lock()
768 defer t.lock.Unlock()
769
770 return t.readLine()
771}
772
773func (t *Terminal) readLine() (line string, err error) {
774 // t.lock must be held at this point
775
776 if t.cursorX == 0 && t.cursorY == 0 {
777 t.writeLine(t.prompt)
778 t.c.Write(t.outBuf)
779 t.outBuf = t.outBuf[:0]
780 }
781
782 lineIsPasted := t.pasteActive
783
784 for {
785 rest := t.remainder
786 lineOk := false
787 for !lineOk {
788 var key rune
789 key, rest = bytesToKey(rest, t.pasteActive)
790 if key == utf8.RuneError {
791 break
792 }
793 if !t.pasteActive {
794 if key == keyCtrlD {
795 if len(t.line) == 0 {
796 return "", io.EOF
797 }
798 }
799 if key == keyCtrlC {
800 return "", io.EOF
801 }
802 if key == keyPasteStart {
803 t.pasteActive = true
804 if len(t.line) == 0 {
805 lineIsPasted = true
806 }
807 continue
808 }
809 } else if key == keyPasteEnd {
810 t.pasteActive = false
811 continue
812 }
813 if !t.pasteActive {
814 lineIsPasted = false
815 }
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530816 // If we have CR, consume LF if present (CRLF sequence) to avoid returning an extra empty line.
817 if key == keyEnter && len(rest) > 0 && rest[0] == keyLF {
818 rest = rest[1:]
819 }
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000820 line, lineOk = t.handleKey(key)
821 }
822 if len(rest) > 0 {
823 n := copy(t.inBuf[:], rest)
824 t.remainder = t.inBuf[:n]
825 } else {
826 t.remainder = nil
827 }
828 t.c.Write(t.outBuf)
829 t.outBuf = t.outBuf[:0]
830 if lineOk {
831 if t.echo {
832 t.historyIndex = -1
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530833 t.historyAdd(line)
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000834 }
835 if lineIsPasted {
836 err = ErrPasteIndicator
837 }
838 return
839 }
840
841 // t.remainder is a slice at the beginning of t.inBuf
842 // containing a partial key sequence
843 readBuf := t.inBuf[len(t.remainder):]
844 var n int
845
846 t.lock.Unlock()
847 n, err = t.c.Read(readBuf)
848 t.lock.Lock()
849
850 if err != nil {
851 return
852 }
853
854 t.remainder = t.inBuf[:n+len(t.remainder)]
855 }
856}
857
858// SetPrompt sets the prompt to be used when reading subsequent lines.
859func (t *Terminal) SetPrompt(prompt string) {
860 t.lock.Lock()
861 defer t.lock.Unlock()
862
863 t.prompt = []rune(prompt)
864}
865
866func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) {
867 // Move cursor to column zero at the start of the line.
868 t.move(t.cursorY, 0, t.cursorX, 0)
869 t.cursorX, t.cursorY = 0, 0
870 t.clearLineToRight()
871 for t.cursorY < numPrevLines {
872 // Move down a line
873 t.move(0, 1, 0, 0)
874 t.cursorY++
875 t.clearLineToRight()
876 }
877 // Move back to beginning.
878 t.move(t.cursorY, 0, 0, 0)
879 t.cursorX, t.cursorY = 0, 0
880
881 t.queue(t.prompt)
882 t.advanceCursor(visualLength(t.prompt))
883 t.writeLine(t.line)
884 t.moveCursorToPos(t.pos)
885}
886
887func (t *Terminal) SetSize(width, height int) error {
888 t.lock.Lock()
889 defer t.lock.Unlock()
890
891 if width == 0 {
892 width = 1
893 }
894
895 oldWidth := t.termWidth
896 t.termWidth, t.termHeight = width, height
897
898 switch {
899 case width == oldWidth:
900 // If the width didn't change then nothing else needs to be
901 // done.
902 return nil
903 case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0:
904 // If there is nothing on current line and no prompt printed,
905 // just do nothing
906 return nil
907 case width < oldWidth:
908 // Some terminals (e.g. xterm) will truncate lines that were
909 // too long when shinking. Others, (e.g. gnome-terminal) will
910 // attempt to wrap them. For the former, repainting t.maxLine
911 // works great, but that behaviour goes badly wrong in the case
912 // of the latter because they have doubled every full line.
913
914 // We assume that we are working on a terminal that wraps lines
915 // and adjust the cursor position based on every previous line
916 // wrapping and turning into two. This causes the prompt on
917 // xterms to move upwards, which isn't great, but it avoids a
918 // huge mess with gnome-terminal.
919 if t.cursorX >= t.termWidth {
920 t.cursorX = t.termWidth - 1
921 }
922 t.cursorY *= 2
923 t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2)
924 case width > oldWidth:
925 // If the terminal expands then our position calculations will
926 // be wrong in the future because we think the cursor is
927 // |t.pos| chars into the string, but there will be a gap at
928 // the end of any wrapped line.
929 //
930 // But the position will actually be correct until we move, so
931 // we can move back to the beginning and repaint everything.
932 t.clearAndRepaintLinePlusNPrevious(t.maxLine)
933 }
934
935 _, err := t.c.Write(t.outBuf)
936 t.outBuf = t.outBuf[:0]
937 return err
938}
939
940type pasteIndicatorError struct{}
941
942func (pasteIndicatorError) Error() string {
943 return "terminal: ErrPasteIndicator not correctly handled"
944}
945
946// ErrPasteIndicator may be returned from ReadLine as the error, in addition
947// to valid line data. It indicates that bracketed paste mode is enabled and
948// that the returned line consists only of pasted data. Programs may wish to
949// interpret pasted data more literally than typed data.
950var ErrPasteIndicator = pasteIndicatorError{}
951
952// SetBracketedPasteMode requests that the terminal bracket paste operations
953// with markers. Not all terminals support this but, if it is supported, then
954// enabling this mode will stop any autocomplete callback from running due to
955// pastes. Additionally, any lines that are completely pasted will be returned
956// from ReadLine with the error set to ErrPasteIndicator.
957func (t *Terminal) SetBracketedPasteMode(on bool) {
958 if on {
959 io.WriteString(t.c, "\x1b[?2004h")
960 } else {
961 io.WriteString(t.c, "\x1b[?2004l")
962 }
963}
964
965// stRingBuffer is a ring buffer of strings.
966type stRingBuffer struct {
967 // entries contains max elements.
968 entries []string
969 max int
970 // head contains the index of the element most recently added to the ring.
971 head int
972 // size contains the number of elements in the ring.
973 size int
974}
975
976func (s *stRingBuffer) Add(a string) {
977 if s.entries == nil {
978 const defaultNumEntries = 100
979 s.entries = make([]string, defaultNumEntries)
980 s.max = defaultNumEntries
981 }
982
983 s.head = (s.head + 1) % s.max
984 s.entries[s.head] = a
985 if s.size < s.max {
986 s.size++
987 }
988}
989
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530990func (s *stRingBuffer) Len() int {
991 return s.size
992}
993
994// At returns the value passed to the nth previous call to Add.
David K. Bainbridgebd6b2882021-08-26 13:31:02 +0000995// If n is zero then the immediately prior value is returned, if one, then the
996// next most recent, and so on. If such an element doesn't exist then ok is
997// false.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530998func (s *stRingBuffer) At(n int) string {
Akash Reddy Kankanalac0014632025-05-21 17:12:20 +0530999 if n < 0 || n >= s.size {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +05301000 panic(fmt.Sprintf("term: history index [%d] out of range [0,%d)", n, s.size))
David K. Bainbridgebd6b2882021-08-26 13:31:02 +00001001 }
1002 index := s.head - n
1003 if index < 0 {
1004 index += s.max
1005 }
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +05301006 return s.entries[index]
David K. Bainbridgebd6b2882021-08-26 13:31:02 +00001007}
1008
1009// readPasswordLine reads from reader until it finds \n or io.EOF.
1010// The slice returned does not include the \n.
1011// readPasswordLine also ignores any \r it finds.
1012// Windows uses \r as end of line. So, on Windows, readPasswordLine
1013// reads until it finds \r and ignores any \n it finds during processing.
1014func readPasswordLine(reader io.Reader) ([]byte, error) {
1015 var buf [1]byte
1016 var ret []byte
1017
1018 for {
1019 n, err := reader.Read(buf[:])
1020 if n > 0 {
1021 switch buf[0] {
1022 case '\b':
1023 if len(ret) > 0 {
1024 ret = ret[:len(ret)-1]
1025 }
1026 case '\n':
1027 if runtime.GOOS != "windows" {
1028 return ret, nil
1029 }
1030 // otherwise ignore \n
1031 case '\r':
1032 if runtime.GOOS == "windows" {
1033 return ret, nil
1034 }
1035 // otherwise ignore \r
1036 default:
1037 ret = append(ret, buf[0])
1038 }
1039 continue
1040 }
1041 if err != nil {
1042 if err == io.EOF && len(ret) > 0 {
1043 return ret, nil
1044 }
1045 return ret, err
1046 }
1047 }
1048}