github.com/prattmic/llgo-embedded@v0.0.0-20150820070356-41cfecea0e1e/third_party/liner/common.go (about)

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