From 1ca6c26d14bfec337f88633c03aa9d6f0109d696 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 09:43:09 -0700 Subject: [PATCH 1/6] Expose History() AddToHistory() and NewHistory() so users of the library can save and restore the history --- terminal.go | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/terminal.go b/terminal.go index f636667..b4a79d1 100644 --- a/terminal.go +++ b/terminal.go @@ -86,7 +86,7 @@ type Terminal struct { // history contains previously entered commands so that they can be // accessed with the up and down keys. - history stRingBuffer + history *stRingBuffer // historyIndex stores the currently accessed history entry, where zero // means the immediately previous entry. historyIndex int @@ -109,6 +109,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal { termHeight: 24, echo: true, historyIndex: -1, + history: NewHistory(defaultNumEntries), } } @@ -797,6 +798,37 @@ func (t *Terminal) readLine() (line string, err error) { } } +// History returns a slice of strings containing the history of entered commands so far. +func (t *Terminal) History() []string { + t.lock.Lock() + defer t.lock.Unlock() + res := []string{} + for i := 0; ; i++ { + c, ok := t.history.NthPreviousEntry(i) + if !ok { + break + } + res = append(res, c) + } + return res +} + +// NewHistory resets the history to one of a given capacity. +func (t *Terminal) NewHistory(capacity int) { + t.lock.Lock() + defer t.lock.Unlock() + t.history = NewHistory(capacity) +} + +// AddToHistory populates history. +func (t *Terminal) AddToHistory(entry ...string) { + t.lock.Lock() + defer t.lock.Unlock() + for _, e := range entry { + t.history.Add(e) + } +} + // SetPrompt sets the prompt to be used when reading subsequent lines. func (t *Terminal) SetPrompt(prompt string) { t.lock.Lock() @@ -915,9 +947,17 @@ type stRingBuffer struct { size int } +func NewHistory(capacity int) *stRingBuffer { + return &stRingBuffer{ + entries: make([]string, capacity), + max: capacity, + } +} + +const defaultNumEntries = 100 + func (s *stRingBuffer) Add(a string) { if s.entries == nil { - const defaultNumEntries = 100 s.entries = make([]string, defaultNumEntries) s.max = defaultNumEntries } From b0cce21e5abf64d493c8d202b15c89baba4d3550 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 16:42:44 -0700 Subject: [PATCH 2/6] Don't add adjacent duplicates to history (#3) --- terminal.go | 4 +++- terminal_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index b4a79d1..74ce989 100644 --- a/terminal.go +++ b/terminal.go @@ -961,7 +961,9 @@ func (s *stRingBuffer) Add(a string) { s.entries = make([]string, defaultNumEntries) s.max = defaultNumEntries } - + if s.entries[s.head] == a { + return // already there at the top + } s.head = (s.head + 1) % s.max s.entries[s.head] = a if s.size < s.max { diff --git a/terminal_test.go b/terminal_test.go index d5c1794..97a5e5f 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -6,8 +6,10 @@ package term import ( "bytes" + "errors" "io" "os" + "reflect" "runtime" "testing" ) @@ -435,3 +437,29 @@ func TestOutputNewlines(t *testing.T) { t.Errorf("incorrect output: was %q, expected %q", output, expected) } } + +func TestHistoryNoDuplicates(t *testing.T) { + c := &MockTerminal{ + toSend: []byte("a\rb\rb\rb\rc\r"), // 5 with 3 duplicate "b" + bytesPerRead: 1, + } + ss := NewTerminal(c, "> ") + count := 0 + for { + _, err := ss.ReadLine() + if errors.Is(err, io.EOF) { + break + } + count++ + } + if count != 5 { + t.Errorf("expected 5 lines, got %d", count) + } + h := ss.History() + if len(h) != 3 { + t.Errorf("history length should be 3, got %d", len(h)) + } + if !reflect.DeepEqual(h, []string{"c", "b", "a"}) { + t.Errorf("history unexpected: %v", h) + } +} From 972077b7c1380725c41f6b78c5b2474255560cb3 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Fri, 9 Aug 2024 09:04:33 -0700 Subject: [PATCH 3/6] Add AutoHistory(false) option (default remains true) to not automatically add to the History and let the caller of ReadLine() decide, for instance to only add validated commands --- terminal.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/terminal.go b/terminal.go index 74ce989..0b2e0c3 100644 --- a/terminal.go +++ b/terminal.go @@ -94,6 +94,10 @@ type Terminal struct { // the incomplete, initial line. That value is stored in // historyPending. historyPending string + // autoHistory, if true, causes lines to be automatically added to the history. + // If false, call AddToHistory to add lines to the history for instance only adding + // successful commands. Defaults to true. This is controlled through AutoHistory(bool). + autoHistory bool } // NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is @@ -110,6 +114,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal { echo: true, historyIndex: -1, history: NewHistory(defaultNumEntries), + autoHistory: true, } } @@ -772,8 +777,10 @@ func (t *Terminal) readLine() (line string, err error) { t.outBuf = t.outBuf[:0] if lineOk { if t.echo { - t.historyIndex = -1 - t.history.Add(line) + t.historyIndex = -1 // Resets the key up behavior/historyPending. + if t.autoHistory { + t.history.Add(line) + } } if lineIsPasted { err = ErrPasteIndicator @@ -829,6 +836,14 @@ func (t *Terminal) AddToHistory(entry ...string) { } } +// AutoHistory sets whether lines are automatically added to the history +// before being returned by ReadLine() or not. Defaults to true. +func (t *Terminal) AutoHistory(onOff bool) { + t.lock.Lock() + t.autoHistory = onOff + t.lock.Unlock() +} + // SetPrompt sets the prompt to be used when reading subsequent lines. func (t *Terminal) SetPrompt(prompt string) { t.lock.Lock() From 9c512d50670f809ed0daa9e6e40929211061de9c Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Fri, 9 Aug 2024 11:19:26 -0700 Subject: [PATCH 4/6] Add ReplaceLatest() to mutate top of history. Add comments. Expose DefaultHistoryEntries as const --- terminal.go | 32 +++++++++++++++++++++++++------- terminal_test.go | 5 +++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/terminal.go b/terminal.go index 0b2e0c3..b26197f 100644 --- a/terminal.go +++ b/terminal.go @@ -113,7 +113,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal { termHeight: 24, echo: true, historyIndex: -1, - history: NewHistory(defaultNumEntries), + history: NewHistory(DefaultHistoryEntries), autoHistory: true, } } @@ -844,6 +844,15 @@ func (t *Terminal) AutoHistory(onOff bool) { t.lock.Unlock() } +// ReplaceLast replaces the most recent history entry with the given string. +// Enables to add invalid commands to the history for editing purpose and +// replace them with the corrected version. Returns the replaced entry. +func (t *Terminal) ReplaceLatest(entry string) string { + t.lock.Lock() + defer t.lock.Unlock() + return t.history.Replace(entry) +} + // SetPrompt sets the prompt to be used when reading subsequent lines. func (t *Terminal) SetPrompt(prompt string) { t.lock.Lock() @@ -962,6 +971,7 @@ type stRingBuffer struct { size int } +// Creates a new ring buffer of strings with the given capacity. func NewHistory(capacity int) *stRingBuffer { return &stRingBuffer{ entries: make([]string, capacity), @@ -969,15 +979,15 @@ func NewHistory(capacity int) *stRingBuffer { } } -const defaultNumEntries = 100 +// DefaultHistoryEntries is the default number of entries in the history. +const DefaultHistoryEntries = 100 func (s *stRingBuffer) Add(a string) { - if s.entries == nil { - s.entries = make([]string, defaultNumEntries) - s.max = defaultNumEntries - } if s.entries[s.head] == a { - return // already there at the top + // Already there at the top, so don't add. + // Also has the nice side effect of ignoring empty strings, + // no s.size check on purpose. + return } s.head = (s.head + 1) % s.max s.entries[s.head] = a @@ -986,6 +996,14 @@ func (s *stRingBuffer) Add(a string) { } } +// Replace theoretically could panic on an empty ring buffer but +// it's harmless on strings. +func (s *stRingBuffer) Replace(a string) string { + previous := s.entries[s.head] + s.entries[s.head] = a + return previous +} + // NthPreviousEntry returns the value passed to the nth previous call to Add. // If n is zero then the immediately prior value is returned, if one, then the // next most recent, and so on. If such an element doesn't exist then ok is diff --git a/terminal_test.go b/terminal_test.go index 97a5e5f..4008cdf 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -462,4 +462,9 @@ func TestHistoryNoDuplicates(t *testing.T) { if !reflect.DeepEqual(h, []string{"c", "b", "a"}) { t.Errorf("history unexpected: %v", h) } + ss.ReplaceLatest("x") + h = ss.History() + if !reflect.DeepEqual(h, []string{"x", "b", "a"}) { + t.Errorf("history unexpected: %v", h) + } } From 68dd89eaaf9edde50bb1c0a5c05ede077220f94b Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Fri, 9 Aug 2024 11:26:30 -0700 Subject: [PATCH 5/6] Correct godoc for ReplaceLatest --- terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index b26197f..1c35c6c 100644 --- a/terminal.go +++ b/terminal.go @@ -844,7 +844,7 @@ func (t *Terminal) AutoHistory(onOff bool) { t.lock.Unlock() } -// ReplaceLast replaces the most recent history entry with the given string. +// ReplaceLatest replaces the most recent history entry with the given string. // Enables to add invalid commands to the history for editing purpose and // replace them with the corrected version. Returns the replaced entry. func (t *Terminal) ReplaceLatest(entry string) string { From 26ecfd17e9914b5aba4ca1e1c4ea9d99c8b60a31 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sat, 10 Aug 2024 12:55:39 -0700 Subject: [PATCH 6/6] Change history default to 99 from 100 so history printed with %02d looks good --- terminal.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index 1c35c6c..8cb2aae 100644 --- a/terminal.go +++ b/terminal.go @@ -980,7 +980,8 @@ func NewHistory(capacity int) *stRingBuffer { } // DefaultHistoryEntries is the default number of entries in the history. -const DefaultHistoryEntries = 100 +// History index 1-99 prints using %02d. +const DefaultHistoryEntries = 99 func (s *stRingBuffer) Add(a string) { if s.entries[s.head] == a {