package liner import ( "bufio" "fmt" "io" "strings" "sync" "unicode/utf8" ) type HistoryProvider interface { ReadHistory(r io.Reader) (num int, err error) WriteHistory(w io.Writer) (num int, err error) AppendHistory(item string) ClearHistory() GetHistoryByPrefix(prefix string) (ph []string) GetHistoryByPattern(pattern string) (ph []string, pos []int) RLock() RUnlock() } type defaultHistoryProvider struct { history []string historyMutex sync.RWMutex } // RUnlock unlocks history for reading func (s *defaultHistoryProvider) RUnlock() { s.historyMutex.RUnlock() } // RUnlock locks history for reading func (s *defaultHistoryProvider) RLock() { s.historyMutex.RLock() } // ReadHistory reads scrollback history from r. Returns the number of lines // read, and any read error (except io.EOF). func (s *defaultHistoryProvider) 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 *defaultHistoryProvider) 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 *defaultHistoryProvider) 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 scrollback history. func (s *defaultHistoryProvider) ClearHistory() { s.historyMutex.Lock() defer s.historyMutex.Unlock() s.history = nil } // Returns the history lines starting with prefix func (s *defaultHistoryProvider) 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 intelligent search func (s *defaultHistoryProvider) 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 }