1/*
2Package liner implements a simple command line editor, inspired by linenoise
3(https://github.com/antirez/linenoise/). This package supports WIN32 in
4addition to the xterm codes supported by everything else.
5*/
6package liner
7
8import (
9	"bufio"
10	"container/ring"
11	"errors"
12	"fmt"
13	"io"
14	"strings"
15	"sync"
16	"unicode/utf8"
17)
18
19type commonState struct {
20	terminalSupported bool
21	outputRedirected  bool
22	inputRedirected   bool
23	history           []string
24	historyMutex      sync.RWMutex
25	completer         WordCompleter
26	columns           int
27	killRing          *ring.Ring
28	ctrlCAborts       bool
29	r                 *bufio.Reader
30	tabStyle          TabStyle
31	multiLineMode     bool
32	cursorRows        int
33	maxRows           int
34	shouldRestart     ShouldRestart
35	needRefresh       bool
36}
37
38// TabStyle is used to select how tab completions are displayed.
39type TabStyle int
40
41// Two tab styles are currently available:
42//
43// TabCircular cycles through each completion item and displays it directly on
44// the prompt
45//
46// TabPrints prints the list of completion items to the screen after a second
47// tab key is pressed. This behaves similar to GNU readline and BASH (which
48// uses readline)
49const (
50	TabCircular TabStyle = iota
51	TabPrints
52)
53
54// ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C
55// if SetCtrlCAborts(true) has been called on the State
56var ErrPromptAborted = errors.New("prompt aborted")
57
58// ErrNotTerminalOutput is returned from Prompt or PasswordPrompt if the
59// platform is normally supported, but stdout has been redirected
60var ErrNotTerminalOutput = errors.New("standard output is not a terminal")
61
62// ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the
63// prompt contains any unprintable runes (including substrings that could
64// be colour codes on some platforms).
65var ErrInvalidPrompt = errors.New("invalid prompt")
66
67// ErrInternal is returned when liner experiences an error that it cannot
68// handle. For example, if the number of colums becomes zero during an
69// active call to Prompt
70var ErrInternal = errors.New("liner: internal error")
71
72// KillRingMax is the max number of elements to save on the killring.
73const KillRingMax = 60
74
75// HistoryLimit is the maximum number of entries saved in the scrollback history.
76const HistoryLimit = 1000
77
78// ReadHistory reads scrollback history from r. Returns the number of lines
79// read, and any read error (except io.EOF).
80func (s *State) ReadHistory(r io.Reader) (num int, err error) {
81	s.historyMutex.Lock()
82	defer s.historyMutex.Unlock()
83
84	in := bufio.NewReader(r)
85	num = 0
86	for {
87		line, part, err := in.ReadLine()
88		if err == io.EOF {
89			break
90		}
91		if err != nil {
92			return num, err
93		}
94		if part {
95			return num, fmt.Errorf("line %d is too long", num+1)
96		}
97		if !utf8.Valid(line) {
98			return num, fmt.Errorf("invalid string at line %d", num+1)
99		}
100		num++
101		s.history = append(s.history, string(line))
102		if len(s.history) > HistoryLimit {
103			s.history = s.history[1:]
104		}
105	}
106	return num, nil
107}
108
109// WriteHistory writes scrollback history to w. Returns the number of lines
110// successfully written, and any write error.
111//
112// Unlike the rest of liner's API, WriteHistory is safe to call
113// from another goroutine while Prompt is in progress.
114// This exception is to facilitate the saving of the history buffer
115// during an unexpected exit (for example, due to Ctrl-C being invoked)
116func (s *State) WriteHistory(w io.Writer) (num int, err error) {
117	s.historyMutex.RLock()
118	defer s.historyMutex.RUnlock()
119
120	for _, item := range s.history {
121		_, err := fmt.Fprintln(w, item)
122		if err != nil {
123			return num, err
124		}
125		num++
126	}
127	return num, nil
128}
129
130// AppendHistory appends an entry to the scrollback history. AppendHistory
131// should be called iff Prompt returns a valid command.
132func (s *State) AppendHistory(item string) {
133	s.historyMutex.Lock()
134	defer s.historyMutex.Unlock()
135
136	if len(s.history) > 0 {
137		if item == s.history[len(s.history)-1] {
138			return
139		}
140	}
141	s.history = append(s.history, item)
142	if len(s.history) > HistoryLimit {
143		s.history = s.history[1:]
144	}
145}
146
147// ClearHistory clears the scroollback history.
148func (s *State) ClearHistory() {
149	s.historyMutex.Lock()
150	defer s.historyMutex.Unlock()
151	s.history = nil
152}
153
154// Returns the history lines starting with prefix
155func (s *State) getHistoryByPrefix(prefix string) (ph []string) {
156	for _, h := range s.history {
157		if strings.HasPrefix(h, prefix) {
158			ph = append(ph, h)
159		}
160	}
161	return
162}
163
164// Returns the history lines matching the intelligent search
165func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) {
166	if pattern == "" {
167		return
168	}
169	for _, h := range s.history {
170		if i := strings.Index(h, pattern); i >= 0 {
171			ph = append(ph, h)
172			pos = append(pos, i)
173		}
174	}
175	return
176}
177
178// Completer takes the currently edited line content at the left of the cursor
179// and returns a list of completion candidates.
180// If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed
181// to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!".
182type Completer func(line string) []string
183
184// WordCompleter takes the currently edited line with the cursor position and
185// returns the completion candidates for the partial word to be completed.
186// If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed
187// to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!".
188type WordCompleter func(line string, pos int) (head string, completions []string, tail string)
189
190// SetCompleter sets the completion function that Liner will call to
191// fetch completion candidates when the user presses tab.
192func (s *State) SetCompleter(f Completer) {
193	if f == nil {
194		s.completer = nil
195		return
196	}
197	s.completer = func(line string, pos int) (string, []string, string) {
198		return "", f(string([]rune(line)[:pos])), string([]rune(line)[pos:])
199	}
200}
201
202// SetWordCompleter sets the completion function that Liner will call to
203// fetch completion candidates when the user presses tab.
204func (s *State) SetWordCompleter(f WordCompleter) {
205	s.completer = f
206}
207
208// SetTabCompletionStyle sets the behvavior when the Tab key is pressed
209// for auto-completion.  TabCircular is the default behavior and cycles
210// through the list of candidates at the prompt.  TabPrints will print
211// the available completion candidates to the screen similar to BASH
212// and GNU Readline
213func (s *State) SetTabCompletionStyle(tabStyle TabStyle) {
214	s.tabStyle = tabStyle
215}
216
217// ModeApplier is the interface that wraps a representation of the terminal
218// mode. ApplyMode sets the terminal to this mode.
219type ModeApplier interface {
220	ApplyMode() error
221}
222
223// SetCtrlCAborts sets whether Prompt on a supported terminal will return an
224// ErrPromptAborted when Ctrl-C is pressed. The default is false (will not
225// return when Ctrl-C is pressed). Unsupported terminals typically raise SIGINT
226// (and Prompt does not return) regardless of the value passed to SetCtrlCAborts.
227func (s *State) SetCtrlCAborts(aborts bool) {
228	s.ctrlCAborts = aborts
229}
230
231// SetMultiLineMode sets whether line is auto-wrapped. The default is false (single line).
232func (s *State) SetMultiLineMode(mlmode bool) {
233	s.multiLineMode = mlmode
234}
235
236// ShouldRestart is passed the error generated by readNext and returns true if
237// the the read should be restarted or false if the error should be returned.
238type ShouldRestart func(err error) bool
239
240// SetShouldRestart sets the restart function that Liner will call to determine
241// whether to retry the call to, or return the error returned by, readNext.
242func (s *State) SetShouldRestart(f ShouldRestart) {
243	s.shouldRestart = f
244}
245
246func (s *State) promptUnsupported(p string) (string, error) {
247	if !s.inputRedirected || !s.terminalSupported {
248		fmt.Print(p)
249	}
250	linebuf, _, err := s.r.ReadLine()
251	if err != nil {
252		return "", err
253	}
254	return string(linebuf), nil
255}
256