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