From 1ca6c26d14bfec337f88633c03aa9d6f0109d696 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 09:43:09 -0700 Subject: [PATCH 1/7] 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 113a4d57915625193a1b0e1451ee8dae713adbb0 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 12:27:22 -0700 Subject: [PATCH 2/7] prep for release under fortio.org/term (#2) --- README.md | 15 ++++----------- go.mod | 2 +- term_test.go | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d03d0ae..5a66244 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,12 @@ # Go terminal/console support -[![Go Reference](https://pkg.go.dev/badge/golang.org/x/term.svg)](https://pkg.go.dev/golang.org/x/term) +[![Go Reference](https://pkg.go.dev/badge/fortio.org/term.svg)](https://pkg.go.dev/fortio.org/term) This repository provides Go terminal and console support packages. +It's a fork of the [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) pending approval of improvements upstream. ## Download/Install -The easiest way to install is to run `go get -u golang.org/x/term`. You can -also manually git clone the repository to `$GOPATH/src/golang.org/x/term`. +The easiest way to use is to run `go get -u fortio.org/term`. -## Report Issues / Send Patches - -This repository uses Gerrit for code changes. To learn how to submit changes to -this repository, see https://golang.org/doc/contribute.html. - -The main issue tracker for the term repository is located at -https://github.com/golang/go/issues. Prefix your issue with "x/term:" in the -subject line, so it is easy to find. +Rather than use this lowlevel library, use [fortio.org/terminal](https://github.com/fortio/terminal#terminal) which wraps this one into a higher level and simpler more powerful API. diff --git a/go.mod b/go.mod index de7ea0f..85f7984 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module golang.org/x/term +module fortio.org/term go 1.18 diff --git a/term_test.go b/term_test.go index 561f3d4..48e5d57 100644 --- a/term_test.go +++ b/term_test.go @@ -9,7 +9,7 @@ import ( "runtime" "testing" - "golang.org/x/term" + "fortio.org/term" ) func TestIsTerminalTempFile(t *testing.T) { From 7796a1325a98277b4653fccf9675b4105fbb4145 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 16:42:44 -0700 Subject: [PATCH 3/7] 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 e4cae6faeaf3e0446f835232645cca16da0efe29 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Fri, 9 Aug 2024 09:16:59 -0700 Subject: [PATCH 4/7] Add AutoHistory(false) option (default remains true) to not automatically add to the History (#4) * 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 ed77b0f4ebd133e4c497f0246f07b368bef71967 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Fri, 9 Aug 2024 11:29:25 -0700 Subject: [PATCH 5/7] Add ReplaceLatest() to mutate top of history. Add comments. Expose DefaultHistoryEntries as const (#5) * Add ReplaceLatest() to mutate top of history. Add comments. Expose DefaultHistoryEntries as const * Correct godoc for ReplaceLatest --- terminal.go | 32 +++++++++++++++++++++++++------- terminal_test.go | 5 +++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/terminal.go b/terminal.go index 0b2e0c3..1c35c6c 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() } +// 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 { + 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 07a4a9e10f59aee55d2e7298782213c93de66945 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sat, 10 Aug 2024 10:28:38 -0700 Subject: [PATCH 6/7] Fix windows not receiving arrow keys (golang/go#68830) (#6) --- term_windows.go | 1 + 1 file changed, 1 insertion(+) diff --git a/term_windows.go b/term_windows.go index 465f560..df6bf94 100644 --- a/term_windows.go +++ b/term_windows.go @@ -26,6 +26,7 @@ func makeRaw(fd int) (*State, error) { return nil, err } raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { return nil, err } From 70d7aeed17b80672b1c30879d37511fb44171bd9 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sat, 10 Aug 2024 12:55:39 -0700 Subject: [PATCH 7/7] 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 {