Skip to content
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

Rebase from x/term master (includes my windows fix) #9

Merged
merged 13 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module golang.org/x/term
module fortio.org/term

go 1.18

require golang.org/x/sys v0.23.0
require golang.org/x/sys v0.29.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.
// Use 99 (and not 100) so printing History's index 1-99 using %02d looks good.
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)
}
}