251 lines
7.5 KiB
Go
251 lines
7.5 KiB
Go
/*
|
|
Package liner implements a simple command line editor, inspired by linenoise
|
|
(https://github.com/antirez/linenoise/). This package supports WIN32 in
|
|
addition to the xterm codes supported by everything else.
|
|
*/
|
|
package liner
|
|
|
|
import (
|
|
"bufio"
|
|
"container/ring"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type commonState struct {
|
|
terminalSupported bool
|
|
outputRedirected bool
|
|
inputRedirected bool
|
|
history []string
|
|
historyMutex sync.RWMutex
|
|
completer WordCompleter
|
|
columns int
|
|
killRing *ring.Ring
|
|
ctrlCAborts bool
|
|
r *bufio.Reader
|
|
tabStyle TabStyle
|
|
multiLineMode bool
|
|
cursorRows int
|
|
maxRows int
|
|
shouldRestart ShouldRestart
|
|
needRefresh bool
|
|
}
|
|
|
|
// TabStyle is used to select how tab completions are displayed.
|
|
type TabStyle int
|
|
|
|
// Two tab styles are currently available:
|
|
//
|
|
// TabCircular cycles through each completion item and displays it directly on
|
|
// the prompt
|
|
//
|
|
// TabPrints prints the list of completion items to the screen after a second
|
|
// tab key is pressed. This behaves similar to GNU readline and BASH (which
|
|
// uses readline)
|
|
const (
|
|
TabCircular TabStyle = iota
|
|
TabPrints
|
|
)
|
|
|
|
// ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C
|
|
// if SetCtrlCAborts(true) has been called on the State
|
|
var ErrPromptAborted = errors.New("prompt aborted")
|
|
|
|
// ErrNotTerminalOutput is returned from Prompt or PasswordPrompt if the
|
|
// platform is normally supported, but stdout has been redirected
|
|
var ErrNotTerminalOutput = errors.New("standard output is not a terminal")
|
|
|
|
// ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the
|
|
// prompt contains any unprintable runes (including substrings that could
|
|
// be colour codes on some platforms).
|
|
var ErrInvalidPrompt = errors.New("invalid prompt")
|
|
|
|
// KillRingMax is the max number of elements to save on the killring.
|
|
const KillRingMax = 60
|
|
|
|
// HistoryLimit is the maximum number of entries saved in the scrollback history.
|
|
const HistoryLimit = 1000
|
|
|
|
// ReadHistory reads scrollback history from r. Returns the number of lines
|
|
// read, and any read error (except io.EOF).
|
|
func (s *State) ReadHistory(r io.Reader) (num int, err error) {
|
|
s.historyMutex.Lock()
|
|
defer s.historyMutex.Unlock()
|
|
|
|
in := bufio.NewReader(r)
|
|
num = 0
|
|
for {
|
|
line, part, err := in.ReadLine()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return num, err
|
|
}
|
|
if part {
|
|
return num, fmt.Errorf("line %d is too long", num+1)
|
|
}
|
|
if !utf8.Valid(line) {
|
|
return num, fmt.Errorf("invalid string at line %d", num+1)
|
|
}
|
|
num++
|
|
s.history = append(s.history, string(line))
|
|
if len(s.history) > HistoryLimit {
|
|
s.history = s.history[1:]
|
|
}
|
|
}
|
|
return num, nil
|
|
}
|
|
|
|
// WriteHistory writes scrollback history to w. Returns the number of lines
|
|
// successfully written, and any write error.
|
|
//
|
|
// Unlike the rest of liner's API, WriteHistory is safe to call
|
|
// from another goroutine while Prompt is in progress.
|
|
// This exception is to facilitate the saving of the history buffer
|
|
// during an unexpected exit (for example, due to Ctrl-C being invoked)
|
|
func (s *State) WriteHistory(w io.Writer) (num int, err error) {
|
|
s.historyMutex.RLock()
|
|
defer s.historyMutex.RUnlock()
|
|
|
|
for _, item := range s.history {
|
|
_, err := fmt.Fprintln(w, item)
|
|
if err != nil {
|
|
return num, err
|
|
}
|
|
num++
|
|
}
|
|
return num, nil
|
|
}
|
|
|
|
// AppendHistory appends an entry to the scrollback history. AppendHistory
|
|
// should be called iff Prompt returns a valid command.
|
|
func (s *State) AppendHistory(item string) {
|
|
s.historyMutex.Lock()
|
|
defer s.historyMutex.Unlock()
|
|
|
|
if len(s.history) > 0 {
|
|
if item == s.history[len(s.history)-1] {
|
|
return
|
|
}
|
|
}
|
|
s.history = append(s.history, item)
|
|
if len(s.history) > HistoryLimit {
|
|
s.history = s.history[1:]
|
|
}
|
|
}
|
|
|
|
// ClearHistory clears the scroollback history.
|
|
func (s *State) ClearHistory() {
|
|
s.historyMutex.Lock()
|
|
defer s.historyMutex.Unlock()
|
|
s.history = nil
|
|
}
|
|
|
|
// Returns the history lines starting with prefix
|
|
func (s *State) getHistoryByPrefix(prefix string) (ph []string) {
|
|
for _, h := range s.history {
|
|
if strings.HasPrefix(h, prefix) {
|
|
ph = append(ph, h)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Returns the history lines matching the inteligent search
|
|
func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) {
|
|
if pattern == "" {
|
|
return
|
|
}
|
|
for _, h := range s.history {
|
|
if i := strings.Index(h, pattern); i >= 0 {
|
|
ph = append(ph, h)
|
|
pos = append(pos, i)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Completer takes the currently edited line content at the left of the cursor
|
|
// and returns a list of completion candidates.
|
|
// If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed
|
|
// to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!".
|
|
type Completer func(line string) []string
|
|
|
|
// WordCompleter takes the currently edited line with the cursor position and
|
|
// returns the completion candidates for the partial word to be completed.
|
|
// If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed
|
|
// to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!".
|
|
type WordCompleter func(line string, pos int) (head string, completions []string, tail string)
|
|
|
|
// SetCompleter sets the completion function that Liner will call to
|
|
// fetch completion candidates when the user presses tab.
|
|
func (s *State) SetCompleter(f Completer) {
|
|
if f == nil {
|
|
s.completer = nil
|
|
return
|
|
}
|
|
s.completer = func(line string, pos int) (string, []string, string) {
|
|
return "", f(string([]rune(line)[:pos])), string([]rune(line)[pos:])
|
|
}
|
|
}
|
|
|
|
// SetWordCompleter sets the completion function that Liner will call to
|
|
// fetch completion candidates when the user presses tab.
|
|
func (s *State) SetWordCompleter(f WordCompleter) {
|
|
s.completer = f
|
|
}
|
|
|
|
// SetTabCompletionStyle sets the behvavior when the Tab key is pressed
|
|
// for auto-completion. TabCircular is the default behavior and cycles
|
|
// through the list of candidates at the prompt. TabPrints will print
|
|
// the available completion candidates to the screen similar to BASH
|
|
// and GNU Readline
|
|
func (s *State) SetTabCompletionStyle(tabStyle TabStyle) {
|
|
s.tabStyle = tabStyle
|
|
}
|
|
|
|
// ModeApplier is the interface that wraps a representation of the terminal
|
|
// mode. ApplyMode sets the terminal to this mode.
|
|
type ModeApplier interface {
|
|
ApplyMode() error
|
|
}
|
|
|
|
// SetCtrlCAborts sets whether Prompt on a supported terminal will return an
|
|
// ErrPromptAborted when Ctrl-C is pressed. The default is false (will not
|
|
// return when Ctrl-C is pressed). Unsupported terminals typically raise SIGINT
|
|
// (and Prompt does not return) regardless of the value passed to SetCtrlCAborts.
|
|
func (s *State) SetCtrlCAborts(aborts bool) {
|
|
s.ctrlCAborts = aborts
|
|
}
|
|
|
|
// SetMultiLineMode sets whether line is auto-wrapped. The default is false (single line).
|
|
func (s *State) SetMultiLineMode(mlmode bool) {
|
|
s.multiLineMode = mlmode
|
|
}
|
|
|
|
// ShouldRestart is passed the error generated by readNext and returns true if
|
|
// the the read should be restarted or false if the error should be returned.
|
|
type ShouldRestart func(err error) bool
|
|
|
|
// SetShouldRestart sets the restart function that Liner will call to determine
|
|
// whether to retry the call to, or return the error returned by, readNext.
|
|
func (s *State) SetShouldRestart(f ShouldRestart) {
|
|
s.shouldRestart = f
|
|
}
|
|
|
|
func (s *State) promptUnsupported(p string) (string, error) {
|
|
if !s.inputRedirected || !s.terminalSupported {
|
|
fmt.Print(p)
|
|
}
|
|
linebuf, _, err := s.r.ReadLine()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(linebuf), nil
|
|
}
|