Skip to content

Commit a809085

Browse files
ldemaillygopherbot
authored andcommitted
term: support pluggable history
Expose a new History interface that allows replacement of the default ring buffer to customize what gets added or not; as well as to allow saving/restoring history on either the default ringbuffer or a custom replacement. Fixes golang/go#68780 Change-Id: I7e61dc6bb438749c8502223705518ef8ff9025b4 GitHub-Last-Rev: 6212813 GitHub-Pull-Request: #20 Reviewed-on: https://go-review.googlesource.com/c/term/+/659835 Reviewed-by: Michael Pratt <mpratt@google.com> Auto-Submit: Jorropo <jorropo.pgm@gmail.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jorropo <jorropo.pgm@gmail.com> Reviewed-by: Austin Clements <austin@google.com>
1 parent 5d2308b commit a809085

File tree

1 file changed

+58
-10
lines changed

1 file changed

+58
-10
lines changed

terminal.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package term
66

77
import (
88
"bytes"
9+
"fmt"
910
"io"
1011
"runtime"
1112
"strconv"
@@ -36,6 +37,26 @@ var vt100EscapeCodes = EscapeCodes{
3637
Reset: []byte{keyEscape, '[', '0', 'm'},
3738
}
3839

40+
// A History provides a (possibly bounded) queue of input lines read by [Terminal.ReadLine].
41+
type 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+
3960
// Terminal contains the state for running a VT100 terminal that is capable of
4061
// reading lines of input.
4162
type Terminal struct {
@@ -86,9 +107,14 @@ type Terminal struct {
86107
remainder []byte
87108
inBuf [256]byte
88109

89-
// history contains previously entered commands so that they can be
90-
// accessed with the up and down keys.
91-
history stRingBuffer
110+
// 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
92118
// historyIndex stores the currently accessed history entry, where zero
93119
// means the immediately previous entry.
94120
historyIndex int
@@ -111,6 +137,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
111137
termHeight: 24,
112138
echo: true,
113139
historyIndex: -1,
140+
History: &stRingBuffer{},
114141
}
115142
}
116143

@@ -450,6 +477,23 @@ func visualLength(runes []rune) int {
450477
return length
451478
}
452479

480+
// histroryAt unlocks the terminal and relocks it while calling History.At.
481+
func (t *Terminal) historyAt(idx int) (string, bool) {
482+
t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.
483+
defer t.lock.Lock() // panic in At (or Len) protection.
484+
if idx < 0 || idx >= t.History.Len() {
485+
return "", false
486+
}
487+
return t.History.At(idx), true
488+
}
489+
490+
// historyAdd unlocks the terminal and relocks it while calling History.Add.
491+
func (t *Terminal) historyAdd(entry string) {
492+
t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.
493+
defer t.lock.Lock() // panic in Add protection.
494+
t.History.Add(entry)
495+
}
496+
453497
// handleKey processes the given key and, optionally, returns a line of text
454498
// that the user has entered.
455499
func (t *Terminal) handleKey(key rune) (line string, ok bool) {
@@ -497,7 +541,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
497541
t.pos = len(t.line)
498542
t.moveCursorToPos(t.pos)
499543
case keyUp:
500-
entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1)
544+
entry, ok := t.historyAt(t.historyIndex + 1)
501545
if !ok {
502546
return "", false
503547
}
@@ -516,7 +560,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
516560
t.setLine(runes, len(runes))
517561
t.historyIndex--
518562
default:
519-
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1)
563+
entry, ok := t.historyAt(t.historyIndex - 1)
520564
if ok {
521565
t.historyIndex--
522566
runes := []rune(entry)
@@ -781,7 +825,7 @@ func (t *Terminal) readLine() (line string, err error) {
781825
if lineOk {
782826
if t.echo {
783827
t.historyIndex = -1
784-
t.history.Add(line)
828+
t.historyAdd(line)
785829
}
786830
if lineIsPasted {
787831
err = ErrPasteIndicator
@@ -938,19 +982,23 @@ func (s *stRingBuffer) Add(a string) {
938982
}
939983
}
940984

941-
// NthPreviousEntry returns the value passed to the nth previous call to Add.
985+
func (s *stRingBuffer) Len() int {
986+
return s.size
987+
}
988+
989+
// At returns the value passed to the nth previous call to Add.
942990
// If n is zero then the immediately prior value is returned, if one, then the
943991
// next most recent, and so on. If such an element doesn't exist then ok is
944992
// false.
945-
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) {
993+
func (s *stRingBuffer) At(n int) string {
946994
if n < 0 || n >= s.size {
947-
return "", false
995+
panic(fmt.Sprintf("term: history index [%d] out of range [0,%d)", n, s.size))
948996
}
949997
index := s.head - n
950998
if index < 0 {
951999
index += s.max
9521000
}
953-
return s.entries[index], true
1001+
return s.entries[index]
9541002
}
9551003

9561004
// readPasswordLine reads from reader until it finds \n or io.EOF.

0 commit comments

Comments
 (0)