Skip to content

Use 99 instead of 100 #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module golang.org/x/term
module fortio.org/term

go 1.18

Expand Down
2 changes: 1 addition & 1 deletion term_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"runtime"
"testing"

"golang.org/x/term"
"fortio.org/term"
)

func TestIsTerminalTempFile(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions term_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
92 changes: 84 additions & 8 deletions terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,18 @@ 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
// When navigating up and down the history it's possible to return to
// 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
Expand All @@ -109,6 +113,8 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
termHeight: 24,
echo: true,
historyIndex: -1,
history: NewHistory(DefaultHistoryEntries),
autoHistory: true,
}
}

Expand Down Expand Up @@ -771,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
Expand All @@ -797,6 +805,54 @@ 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)
}
}

// 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()
}

// 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()
Expand Down Expand Up @@ -915,20 +971,40 @@ type stRingBuffer struct {
size int
}

func (s *stRingBuffer) Add(a string) {
if s.entries == nil {
const defaultNumEntries = 100
s.entries = make([]string, defaultNumEntries)
s.max = defaultNumEntries
// Creates a new ring buffer of strings with the given capacity.
func NewHistory(capacity int) *stRingBuffer {
return &stRingBuffer{
entries: make([]string, capacity),
max: capacity,
}
}

// DefaultHistoryEntries is the default number of entries in the history.
// History index 1-99 prints using %02d.
const DefaultHistoryEntries = 99

func (s *stRingBuffer) Add(a string) {
if s.entries[s.head] == a {
// 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
if s.size < s.max {
s.size++
}
}

// 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
Expand Down
33 changes: 33 additions & 0 deletions terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package term

import (
"bytes"
"errors"
"io"
"os"
"reflect"
"runtime"
"testing"
)
Expand Down Expand Up @@ -435,3 +437,34 @@ 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)
}
ss.ReplaceLatest("x")
h = ss.History()
if !reflect.DeepEqual(h, []string{"x", "b", "a"}) {
t.Errorf("history unexpected: %v", h)
}
}