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