mirror of https://github.com/ethereum/go-ethereum
parent
38f6d60e6e
commit
2393de5d6b
@ -0,0 +1,21 @@ |
||||
Copyright © 2012 Peter Harris |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a |
||||
copy of this software and associated documentation files (the "Software"), |
||||
to deal in the Software without restriction, including without limitation |
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
and/or sell copies of the Software, and to permit persons to whom the |
||||
Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice (including the next |
||||
paragraph) shall be included in all copies or substantial portions of the |
||||
Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
DEALINGS IN THE SOFTWARE. |
||||
|
@ -0,0 +1,95 @@ |
||||
Liner |
||||
===== |
||||
|
||||
Liner is a command line editor with history. It was inspired by linenoise; |
||||
everything Unix-like is a VT100 (or is trying very hard to be). If your |
||||
terminal is not pretending to be a VT100, change it. Liner also support |
||||
Windows. |
||||
|
||||
Liner is released under the X11 license (which is similar to the new BSD |
||||
license). |
||||
|
||||
Line Editing |
||||
------------ |
||||
|
||||
The following line editing commands are supported on platforms and terminals |
||||
that Liner supports: |
||||
|
||||
Keystroke | Action |
||||
--------- | ------ |
||||
Ctrl-A, Home | Move cursor to beginning of line |
||||
Ctrl-E, End | Move cursor to end of line |
||||
Ctrl-B, Left | Move cursor one character left |
||||
Ctrl-F, Right| Move cursor one character right |
||||
Ctrl-Left | Move cursor to previous word |
||||
Ctrl-Right | Move cursor to next word |
||||
Ctrl-D, Del | (if line is *not* empty) Delete character under cursor |
||||
Ctrl-D | (if line *is* empty) End of File - usually quits application |
||||
Ctrl-C | Reset input (create new empty prompt) |
||||
Ctrl-L | Clear screen (line is unmodified) |
||||
Ctrl-T | Transpose previous character with current character |
||||
Ctrl-H, BackSpace | Delete character before cursor |
||||
Ctrl-W | Delete word leading up to cursor |
||||
Ctrl-K | Delete from cursor to end of line |
||||
Ctrl-U | Delete from start of line to cursor |
||||
Ctrl-P, Up | Previous match from history |
||||
Ctrl-N, Down | Next match from history |
||||
Ctrl-R | Reverse Search history (Ctrl-S forward, Ctrl-G cancel) |
||||
Ctrl-Y | Paste from Yank buffer (Alt-Y to paste next yank instead) |
||||
Tab | Next completion |
||||
Shift-Tab | (after Tab) Previous completion |
||||
|
||||
Getting started |
||||
----------------- |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import ( |
||||
"log" |
||||
"os" |
||||
"strings" |
||||
|
||||
"github.com/peterh/liner" |
||||
) |
||||
|
||||
var ( |
||||
history_fn = "/tmp/.liner_history" |
||||
names = []string{"john", "james", "mary", "nancy"} |
||||
) |
||||
|
||||
func main() { |
||||
line := liner.NewLiner() |
||||
defer line.Close() |
||||
|
||||
line.SetCompleter(func(line string) (c []string) { |
||||
for _, n := range names { |
||||
if strings.HasPrefix(n, strings.ToLower(line)) { |
||||
c = append(c, n) |
||||
} |
||||
} |
||||
return |
||||
}) |
||||
|
||||
if f, err := os.Open(history_fn); err == nil { |
||||
line.ReadHistory(f) |
||||
f.Close() |
||||
} |
||||
|
||||
if name, err := line.Prompt("What is your name? "); err != nil { |
||||
log.Print("Error reading line: ", err) |
||||
} else { |
||||
log.Print("Got: ", name) |
||||
line.AppendHistory(name) |
||||
} |
||||
|
||||
if f, err := os.Create(history_fn); err != nil { |
||||
log.Print("Error writing history file: ", err) |
||||
} else { |
||||
line.WriteHistory(f) |
||||
f.Close() |
||||
} |
||||
} |
||||
``` |
||||
|
||||
For documentation, see http://godoc.org/github.com/peterh/liner |
@ -0,0 +1,39 @@ |
||||
// +build openbsd freebsd netbsd
|
||||
|
||||
package liner |
||||
|
||||
import "syscall" |
||||
|
||||
const ( |
||||
getTermios = syscall.TIOCGETA |
||||
setTermios = syscall.TIOCSETA |
||||
) |
||||
|
||||
const ( |
||||
// Input flags
|
||||
inpck = 0x010 |
||||
istrip = 0x020 |
||||
icrnl = 0x100 |
||||
ixon = 0x200 |
||||
|
||||
// Output flags
|
||||
opost = 0x1 |
||||
|
||||
// Control flags
|
||||
cs8 = 0x300 |
||||
|
||||
// Local flags
|
||||
isig = 0x080 |
||||
icanon = 0x100 |
||||
iexten = 0x400 |
||||
) |
||||
|
||||
type termios struct { |
||||
Iflag uint32 |
||||
Oflag uint32 |
||||
Cflag uint32 |
||||
Lflag uint32 |
||||
Cc [20]byte |
||||
Ispeed int32 |
||||
Ospeed int32 |
||||
} |
@ -0,0 +1,219 @@ |
||||
/* |
||||
Package liner implements a simple command line editor, inspired by linenoise |
||||
(https://github.com/antirez/linenoise/). This package supports WIN32 in
|
||||
addition to the xterm codes supported by everything else. |
||||
*/ |
||||
package liner |
||||
|
||||
import ( |
||||
"bufio" |
||||
"bytes" |
||||
"container/ring" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"strings" |
||||
"sync" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
type commonState struct { |
||||
terminalSupported bool |
||||
outputRedirected bool |
||||
inputRedirected bool |
||||
history []string |
||||
historyMutex sync.RWMutex |
||||
completer WordCompleter |
||||
columns int |
||||
killRing *ring.Ring |
||||
ctrlCAborts bool |
||||
r *bufio.Reader |
||||
tabStyle TabStyle |
||||
} |
||||
|
||||
// TabStyle is used to select how tab completions are displayed.
|
||||
type TabStyle int |
||||
|
||||
// Two tab styles are currently available:
|
||||
//
|
||||
// TabCircular cycles through each completion item and displays it directly on
|
||||
// the prompt
|
||||
//
|
||||
// TabPrints prints the list of completion items to the screen after a second
|
||||
// tab key is pressed. This behaves similar to GNU readline and BASH (which
|
||||
// uses readline)
|
||||
const ( |
||||
TabCircular TabStyle = iota |
||||
TabPrints |
||||
) |
||||
|
||||
// ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C
|
||||
// if SetCtrlCAborts(true) has been called on the State
|
||||
var ErrPromptAborted = errors.New("prompt aborted") |
||||
|
||||
// ErrNotTerminalOutput is returned from Prompt or PasswordPrompt if the
|
||||
// platform is normally supported, but stdout has been redirected
|
||||
var ErrNotTerminalOutput = errors.New("standard output is not a terminal") |
||||
|
||||
// Max elements to save on the killring
|
||||
const KillRingMax = 60 |
||||
|
||||
// HistoryLimit is the maximum number of entries saved in the scrollback history.
|
||||
const HistoryLimit = 1000 |
||||
|
||||
// ReadHistory reads scrollback history from r. Returns the number of lines
|
||||
// read, and any read error (except io.EOF).
|
||||
func (s *State) 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 *State) 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 *State) 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:] |
||||
} |
||||
} |
||||
|
||||
// Returns the history lines starting with prefix
|
||||
func (s *State) 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 inteligent search
|
||||
func (s *State) 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 |
||||
} |
||||
|
||||
// Completer takes the currently edited line content at the left of the cursor
|
||||
// and returns a list of completion candidates.
|
||||
// If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed
|
||||
// to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!".
|
||||
type Completer func(line string) []string |
||||
|
||||
// WordCompleter takes the currently edited line with the cursor position and
|
||||
// returns the completion candidates for the partial word to be completed.
|
||||
// If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed
|
||||
// to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!".
|
||||
type WordCompleter func(line string, pos int) (head string, completions []string, tail string) |
||||
|
||||
// SetCompleter sets the completion function that Liner will call to
|
||||
// fetch completion candidates when the user presses tab.
|
||||
func (s *State) SetCompleter(f Completer) { |
||||
if f == nil { |
||||
s.completer = nil |
||||
return |
||||
} |
||||
s.completer = func(line string, pos int) (string, []string, string) { |
||||
return "", f(line[:pos]), line[pos:] |
||||
} |
||||
} |
||||
|
||||
// SetWordCompleter sets the completion function that Liner will call to
|
||||
// fetch completion candidates when the user presses tab.
|
||||
func (s *State) SetWordCompleter(f WordCompleter) { |
||||
s.completer = f |
||||
} |
||||
|
||||
// SetTabCompletionStyle sets the behvavior when the Tab key is pressed
|
||||
// for auto-completion. TabCircular is the default behavior and cycles
|
||||
// through the list of candidates at the prompt. TabPrints will print
|
||||
// the available completion candidates to the screen similar to BASH
|
||||
// and GNU Readline
|
||||
func (s *State) SetTabCompletionStyle(tabStyle TabStyle) { |
||||
s.tabStyle = tabStyle |
||||
} |
||||
|
||||
// ModeApplier is the interface that wraps a representation of the terminal
|
||||
// mode. ApplyMode sets the terminal to this mode.
|
||||
type ModeApplier interface { |
||||
ApplyMode() error |
||||
} |
||||
|
||||
// SetCtrlCAborts sets whether Prompt on a supported terminal will return an
|
||||
// ErrPromptAborted when Ctrl-C is pressed. The default is false (will not
|
||||
// return when Ctrl-C is pressed). Unsupported terminals typically raise SIGINT
|
||||
// (and Prompt does not return) regardless of the value passed to SetCtrlCAborts.
|
||||
func (s *State) SetCtrlCAborts(aborts bool) { |
||||
s.ctrlCAborts = aborts |
||||
} |
||||
|
||||
func (s *State) promptUnsupported(p string) (string, error) { |
||||
if !s.inputRedirected { |
||||
fmt.Print(p) |
||||
} |
||||
linebuf, _, err := s.r.ReadLine() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return string(bytes.TrimSpace(linebuf)), nil |
||||
} |
@ -0,0 +1,57 @@ |
||||
// +build !windows,!linux,!darwin,!openbsd,!freebsd,!netbsd
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"bufio" |
||||
"errors" |
||||
"os" |
||||
) |
||||
|
||||
// State represents an open terminal
|
||||
type State struct { |
||||
commonState |
||||
} |
||||
|
||||
// Prompt displays p, and then waits for user input. Prompt does not support
|
||||
// line editing on this operating system.
|
||||
func (s *State) Prompt(p string) (string, error) { |
||||
return s.promptUnsupported(p) |
||||
} |
||||
|
||||
// PasswordPrompt is not supported in this OS.
|
||||
func (s *State) PasswordPrompt(p string) (string, error) { |
||||
return "", errors.New("liner: function not supported in this terminal") |
||||
} |
||||
|
||||
// NewLiner initializes a new *State
|
||||
//
|
||||
// Note that this operating system uses a fallback mode without line
|
||||
// editing. Patches welcome.
|
||||
func NewLiner() *State { |
||||
var s State |
||||
s.r = bufio.NewReader(os.Stdin) |
||||
return &s |
||||
} |
||||
|
||||
// Close returns the terminal to its previous mode
|
||||
func (s *State) Close() error { |
||||
return nil |
||||
} |
||||
|
||||
// TerminalSupported returns false because line editing is not
|
||||
// supported on this platform.
|
||||
func TerminalSupported() bool { |
||||
return false |
||||
} |
||||
|
||||
type noopMode struct{} |
||||
|
||||
func (n noopMode) ApplyMode() error { |
||||
return nil |
||||
} |
||||
|
||||
// TerminalMode returns a noop InputModeSetter on this platform.
|
||||
func TerminalMode() (ModeApplier, error) { |
||||
return noopMode{}, nil |
||||
} |
@ -0,0 +1,359 @@ |
||||
// +build linux darwin openbsd freebsd netbsd
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"bufio" |
||||
"errors" |
||||
"os" |
||||
"os/signal" |
||||
"strconv" |
||||
"strings" |
||||
"syscall" |
||||
"time" |
||||
) |
||||
|
||||
type nexter struct { |
||||
r rune |
||||
err error |
||||
} |
||||
|
||||
// State represents an open terminal
|
||||
type State struct { |
||||
commonState |
||||
origMode termios |
||||
defaultMode termios |
||||
next <-chan nexter |
||||
winch chan os.Signal |
||||
pending []rune |
||||
useCHA bool |
||||
} |
||||
|
||||
// NewLiner initializes a new *State, and sets the terminal into raw mode. To
|
||||
// restore the terminal to its previous state, call State.Close().
|
||||
//
|
||||
// Note if you are still using Go 1.0: NewLiner handles SIGWINCH, so it will
|
||||
// leak a channel every time you call it. Therefore, it is recommened that you
|
||||
// upgrade to a newer release of Go, or ensure that NewLiner is only called
|
||||
// once.
|
||||
func NewLiner() *State { |
||||
var s State |
||||
s.r = bufio.NewReader(os.Stdin) |
||||
|
||||
s.terminalSupported = TerminalSupported() |
||||
if m, err := TerminalMode(); err == nil { |
||||
s.origMode = *m.(*termios) |
||||
} else { |
||||
s.terminalSupported = false |
||||
s.inputRedirected = true |
||||
} |
||||
if _, err := getMode(syscall.Stdout); err != 0 { |
||||
s.terminalSupported = false |
||||
s.outputRedirected = true |
||||
} |
||||
if s.terminalSupported { |
||||
mode := s.origMode |
||||
mode.Iflag &^= icrnl | inpck | istrip | ixon |
||||
mode.Cflag |= cs8 |
||||
mode.Lflag &^= syscall.ECHO | icanon | iexten |
||||
mode.ApplyMode() |
||||
|
||||
winch := make(chan os.Signal, 1) |
||||
signal.Notify(winch, syscall.SIGWINCH) |
||||
s.winch = winch |
||||
|
||||
s.checkOutput() |
||||
} |
||||
|
||||
if !s.outputRedirected { |
||||
s.getColumns() |
||||
s.outputRedirected = s.columns <= 0 |
||||
} |
||||
|
||||
return &s |
||||
} |
||||
|
||||
var errTimedOut = errors.New("timeout") |
||||
|
||||
func (s *State) startPrompt() { |
||||
if s.terminalSupported { |
||||
if m, err := TerminalMode(); err == nil { |
||||
s.defaultMode = *m.(*termios) |
||||
mode := s.defaultMode |
||||
mode.Lflag &^= isig |
||||
mode.ApplyMode() |
||||
} |
||||
} |
||||
s.restartPrompt() |
||||
} |
||||
|
||||
func (s *State) restartPrompt() { |
||||
next := make(chan nexter) |
||||
go func() { |
||||
for { |
||||
var n nexter |
||||
n.r, _, n.err = s.r.ReadRune() |
||||
next <- n |
||||
// Shut down nexter loop when an end condition has been reached
|
||||
if n.err != nil || n.r == '\n' || n.r == '\r' || n.r == ctrlC || n.r == ctrlD { |
||||
close(next) |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
s.next = next |
||||
} |
||||
|
||||
func (s *State) stopPrompt() { |
||||
if s.terminalSupported { |
||||
s.defaultMode.ApplyMode() |
||||
} |
||||
} |
||||
|
||||
func (s *State) nextPending(timeout <-chan time.Time) (rune, error) { |
||||
select { |
||||
case thing, ok := <-s.next: |
||||
if !ok { |
||||
return 0, errors.New("liner: internal error") |
||||
} |
||||
if thing.err != nil { |
||||
return 0, thing.err |
||||
} |
||||
s.pending = append(s.pending, thing.r) |
||||
return thing.r, nil |
||||
case <-timeout: |
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, errTimedOut |
||||
} |
||||
// not reached
|
||||
return 0, nil |
||||
} |
||||
|
||||
func (s *State) readNext() (interface{}, error) { |
||||
if len(s.pending) > 0 { |
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, nil |
||||
} |
||||
var r rune |
||||
select { |
||||
case thing, ok := <-s.next: |
||||
if !ok { |
||||
return 0, errors.New("liner: internal error") |
||||
} |
||||
if thing.err != nil { |
||||
return nil, thing.err |
||||
} |
||||
r = thing.r |
||||
case <-s.winch: |
||||
s.getColumns() |
||||
return winch, nil |
||||
} |
||||
if r != esc { |
||||
return r, nil |
||||
} |
||||
s.pending = append(s.pending, r) |
||||
|
||||
// Wait at most 50 ms for the rest of the escape sequence
|
||||
// If nothing else arrives, it was an actual press of the esc key
|
||||
timeout := time.After(50 * time.Millisecond) |
||||
flag, err := s.nextPending(timeout) |
||||
if err != nil { |
||||
if err == errTimedOut { |
||||
return flag, nil |
||||
} |
||||
return unknown, err |
||||
} |
||||
|
||||
switch flag { |
||||
case '[': |
||||
code, err := s.nextPending(timeout) |
||||
if err != nil { |
||||
if err == errTimedOut { |
||||
return code, nil |
||||
} |
||||
return unknown, err |
||||
} |
||||
switch code { |
||||
case 'A': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return up, nil |
||||
case 'B': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return down, nil |
||||
case 'C': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return right, nil |
||||
case 'D': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return left, nil |
||||
case 'F': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return end, nil |
||||
case 'H': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return home, nil |
||||
case 'Z': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return shiftTab, nil |
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||
num := []rune{code} |
||||
for { |
||||
code, err := s.nextPending(timeout) |
||||
if err != nil { |
||||
if err == errTimedOut { |
||||
return code, nil |
||||
} |
||||
return nil, err |
||||
} |
||||
switch code { |
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||
num = append(num, code) |
||||
case ';': |
||||
// Modifier code to follow
|
||||
// This only supports Ctrl-left and Ctrl-right for now
|
||||
x, _ := strconv.ParseInt(string(num), 10, 32) |
||||
if x != 1 { |
||||
// Can't be left or right
|
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, nil |
||||
} |
||||
num = num[:0] |
||||
for { |
||||
code, err = s.nextPending(timeout) |
||||
if err != nil { |
||||
if err == errTimedOut { |
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, nil |
||||
} |
||||
return nil, err |
||||
} |
||||
switch code { |
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||
num = append(num, code) |
||||
case 'C', 'D': |
||||
// right, left
|
||||
mod, _ := strconv.ParseInt(string(num), 10, 32) |
||||
if mod != 5 { |
||||
// Not bare Ctrl
|
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, nil |
||||
} |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
if code == 'C' { |
||||
return wordRight, nil |
||||
} |
||||
return wordLeft, nil |
||||
default: |
||||
// Not left or right
|
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, nil |
||||
} |
||||
} |
||||
case '~': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
x, _ := strconv.ParseInt(string(num), 10, 32) |
||||
switch x { |
||||
case 2: |
||||
return insert, nil |
||||
case 3: |
||||
return del, nil |
||||
case 5: |
||||
return pageUp, nil |
||||
case 6: |
||||
return pageDown, nil |
||||
case 7: |
||||
return home, nil |
||||
case 8: |
||||
return end, nil |
||||
case 15: |
||||
return f5, nil |
||||
case 17: |
||||
return f6, nil |
||||
case 18: |
||||
return f7, nil |
||||
case 19: |
||||
return f8, nil |
||||
case 20: |
||||
return f9, nil |
||||
case 21: |
||||
return f10, nil |
||||
case 23: |
||||
return f11, nil |
||||
case 24: |
||||
return f12, nil |
||||
default: |
||||
return unknown, nil |
||||
} |
||||
default: |
||||
// unrecognized escape code
|
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
case 'O': |
||||
code, err := s.nextPending(timeout) |
||||
if err != nil { |
||||
if err == errTimedOut { |
||||
return code, nil |
||||
} |
||||
return nil, err |
||||
} |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
switch code { |
||||
case 'c': |
||||
return wordRight, nil |
||||
case 'd': |
||||
return wordLeft, nil |
||||
case 'H': |
||||
return home, nil |
||||
case 'F': |
||||
return end, nil |
||||
case 'P': |
||||
return f1, nil |
||||
case 'Q': |
||||
return f2, nil |
||||
case 'R': |
||||
return f3, nil |
||||
case 'S': |
||||
return f4, nil |
||||
default: |
||||
return unknown, nil |
||||
} |
||||
case 'y': |
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return altY, nil |
||||
default: |
||||
rv := s.pending[0] |
||||
s.pending = s.pending[1:] |
||||
return rv, nil |
||||
} |
||||
|
||||
// not reached
|
||||
return r, nil |
||||
} |
||||
|
||||
// Close returns the terminal to its previous mode
|
||||
func (s *State) Close() error { |
||||
stopSignal(s.winch) |
||||
if s.terminalSupported { |
||||
s.origMode.ApplyMode() |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// TerminalSupported returns true if the current terminal supports
|
||||
// line editing features, and false if liner will use the 'dumb'
|
||||
// fallback for input.
|
||||
func TerminalSupported() bool { |
||||
bad := map[string]bool{"": true, "dumb": true, "cons25": true} |
||||
return !bad[strings.ToLower(os.Getenv("TERM"))] |
||||
} |
@ -0,0 +1,39 @@ |
||||
// +build darwin
|
||||
|
||||
package liner |
||||
|
||||
import "syscall" |
||||
|
||||
const ( |
||||
getTermios = syscall.TIOCGETA |
||||
setTermios = syscall.TIOCSETA |
||||
) |
||||
|
||||
const ( |
||||
// Input flags
|
||||
inpck = 0x010 |
||||
istrip = 0x020 |
||||
icrnl = 0x100 |
||||
ixon = 0x200 |
||||
|
||||
// Output flags
|
||||
opost = 0x1 |
||||
|
||||
// Control flags
|
||||
cs8 = 0x300 |
||||
|
||||
// Local flags
|
||||
isig = 0x080 |
||||
icanon = 0x100 |
||||
iexten = 0x400 |
||||
) |
||||
|
||||
type termios struct { |
||||
Iflag uintptr |
||||
Oflag uintptr |
||||
Cflag uintptr |
||||
Lflag uintptr |
||||
Cc [20]byte |
||||
Ispeed uintptr |
||||
Ospeed uintptr |
||||
} |
@ -0,0 +1,26 @@ |
||||
// +build linux
|
||||
|
||||
package liner |
||||
|
||||
import "syscall" |
||||
|
||||
const ( |
||||
getTermios = syscall.TCGETS |
||||
setTermios = syscall.TCSETS |
||||
) |
||||
|
||||
const ( |
||||
icrnl = syscall.ICRNL |
||||
inpck = syscall.INPCK |
||||
istrip = syscall.ISTRIP |
||||
ixon = syscall.IXON |
||||
opost = syscall.OPOST |
||||
cs8 = syscall.CS8 |
||||
isig = syscall.ISIG |
||||
icanon = syscall.ICANON |
||||
iexten = syscall.IEXTEN |
||||
) |
||||
|
||||
type termios struct { |
||||
syscall.Termios |
||||
} |
@ -0,0 +1,61 @@ |
||||
// +build !windows
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"bufio" |
||||
"bytes" |
||||
"testing" |
||||
) |
||||
|
||||
func (s *State) expectRune(t *testing.T, r rune) { |
||||
item, err := s.readNext() |
||||
if err != nil { |
||||
t.Fatalf("Expected rune '%c', got error %s\n", r, err) |
||||
} |
||||
if v, ok := item.(rune); !ok { |
||||
t.Fatalf("Expected rune '%c', got non-rune %v\n", r, v) |
||||
} else { |
||||
if v != r { |
||||
t.Fatalf("Expected rune '%c', got rune '%c'\n", r, v) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *State) expectAction(t *testing.T, a action) { |
||||
item, err := s.readNext() |
||||
if err != nil { |
||||
t.Fatalf("Expected Action %d, got error %s\n", a, err) |
||||
} |
||||
if v, ok := item.(action); !ok { |
||||
t.Fatalf("Expected Action %d, got non-Action %v\n", a, v) |
||||
} else { |
||||
if v != a { |
||||
t.Fatalf("Expected Action %d, got Action %d\n", a, v) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestTypes(t *testing.T) { |
||||
input := []byte{'A', 27, 'B', 27, 91, 68, 27, '[', '1', ';', '5', 'D', 'e'} |
||||
var s State |
||||
s.r = bufio.NewReader(bytes.NewBuffer(input)) |
||||
|
||||
next := make(chan nexter) |
||||
go func() { |
||||
for { |
||||
var n nexter |
||||
n.r, _, n.err = s.r.ReadRune() |
||||
next <- n |
||||
} |
||||
}() |
||||
s.next = next |
||||
|
||||
s.expectRune(t, 'A') |
||||
s.expectRune(t, 27) |
||||
s.expectRune(t, 'B') |
||||
s.expectAction(t, left) |
||||
s.expectAction(t, wordLeft) |
||||
|
||||
s.expectRune(t, 'e') |
||||
} |
@ -0,0 +1,313 @@ |
||||
package liner |
||||
|
||||
import ( |
||||
"bufio" |
||||
"os" |
||||
"syscall" |
||||
"unsafe" |
||||
) |
||||
|
||||
var ( |
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll") |
||||
|
||||
procGetStdHandle = kernel32.NewProc("GetStdHandle") |
||||
procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW") |
||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode") |
||||
procSetConsoleMode = kernel32.NewProc("SetConsoleMode") |
||||
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") |
||||
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") |
||||
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") |
||||
) |
||||
|
||||
// These names are from the Win32 api, so they use underscores (contrary to
|
||||
// what golint suggests)
|
||||
const ( |
||||
std_input_handle = uint32(-10 & 0xFFFFFFFF) |
||||
std_output_handle = uint32(-11 & 0xFFFFFFFF) |
||||
std_error_handle = uint32(-12 & 0xFFFFFFFF) |
||||
invalid_handle_value = ^uintptr(0) |
||||
) |
||||
|
||||
type inputMode uint32 |
||||
|
||||
// State represents an open terminal
|
||||
type State struct { |
||||
commonState |
||||
handle syscall.Handle |
||||
hOut syscall.Handle |
||||
origMode inputMode |
||||
defaultMode inputMode |
||||
key interface{} |
||||
repeat uint16 |
||||
} |
||||
|
||||
const ( |
||||
enableEchoInput = 0x4 |
||||
enableInsertMode = 0x20 |
||||
enableLineInput = 0x2 |
||||
enableMouseInput = 0x10 |
||||
enableProcessedInput = 0x1 |
||||
enableQuickEditMode = 0x40 |
||||
enableWindowInput = 0x8 |
||||
) |
||||
|
||||
// NewLiner initializes a new *State, and sets the terminal into raw mode. To
|
||||
// restore the terminal to its previous state, call State.Close().
|
||||
func NewLiner() *State { |
||||
var s State |
||||
hIn, _, _ := procGetStdHandle.Call(uintptr(std_input_handle)) |
||||
s.handle = syscall.Handle(hIn) |
||||
hOut, _, _ := procGetStdHandle.Call(uintptr(std_output_handle)) |
||||
s.hOut = syscall.Handle(hOut) |
||||
|
||||
s.terminalSupported = true |
||||
if m, err := TerminalMode(); err == nil { |
||||
s.origMode = m.(inputMode) |
||||
mode := s.origMode |
||||
mode &^= enableEchoInput |
||||
mode &^= enableInsertMode |
||||
mode &^= enableLineInput |
||||
mode &^= enableMouseInput |
||||
mode |= enableWindowInput |
||||
mode.ApplyMode() |
||||
} else { |
||||
s.inputRedirected = true |
||||
s.r = bufio.NewReader(os.Stdin) |
||||
} |
||||
|
||||
s.getColumns() |
||||
s.outputRedirected = s.columns <= 0 |
||||
|
||||
return &s |
||||
} |
||||
|
||||
// These names are from the Win32 api, so they use underscores (contrary to
|
||||
// what golint suggests)
|
||||
const ( |
||||
focus_event = 0x0010 |
||||
key_event = 0x0001 |
||||
menu_event = 0x0008 |
||||
mouse_event = 0x0002 |
||||
window_buffer_size_event = 0x0004 |
||||
) |
||||
|
||||
type input_record struct { |
||||
eventType uint16 |
||||
pad uint16 |
||||
blob [16]byte |
||||
} |
||||
|
||||
type key_event_record struct { |
||||
KeyDown int32 |
||||
RepeatCount uint16 |
||||
VirtualKeyCode uint16 |
||||
VirtualScanCode uint16 |
||||
Char int16 |
||||
ControlKeyState uint32 |
||||
} |
||||
|
||||
// These names are from the Win32 api, so they use underscores (contrary to
|
||||
// what golint suggests)
|
||||
const ( |
||||
vk_tab = 0x09 |
||||
vk_prior = 0x21 |
||||
vk_next = 0x22 |
||||
vk_end = 0x23 |
||||
vk_home = 0x24 |
||||
vk_left = 0x25 |
||||
vk_up = 0x26 |
||||
vk_right = 0x27 |
||||
vk_down = 0x28 |
||||
vk_insert = 0x2d |
||||
vk_delete = 0x2e |
||||
vk_f1 = 0x70 |
||||
vk_f2 = 0x71 |
||||
vk_f3 = 0x72 |
||||
vk_f4 = 0x73 |
||||
vk_f5 = 0x74 |
||||
vk_f6 = 0x75 |
||||
vk_f7 = 0x76 |
||||
vk_f8 = 0x77 |
||||
vk_f9 = 0x78 |
||||
vk_f10 = 0x79 |
||||
vk_f11 = 0x7a |
||||
vk_f12 = 0x7b |
||||
yKey = 0x59 |
||||
) |
||||
|
||||
const ( |
||||
shiftPressed = 0x0010 |
||||
leftAltPressed = 0x0002 |
||||
leftCtrlPressed = 0x0008 |
||||
rightAltPressed = 0x0001 |
||||
rightCtrlPressed = 0x0004 |
||||
|
||||
modKeys = shiftPressed | leftAltPressed | rightAltPressed | leftCtrlPressed | rightCtrlPressed |
||||
) |
||||
|
||||
func (s *State) readNext() (interface{}, error) { |
||||
if s.repeat > 0 { |
||||
s.repeat-- |
||||
return s.key, nil |
||||
} |
||||
|
||||
var input input_record |
||||
pbuf := uintptr(unsafe.Pointer(&input)) |
||||
var rv uint32 |
||||
prv := uintptr(unsafe.Pointer(&rv)) |
||||
|
||||
for { |
||||
ok, _, err := procReadConsoleInput.Call(uintptr(s.handle), pbuf, 1, prv) |
||||
|
||||
if ok == 0 { |
||||
return nil, err |
||||
} |
||||
|
||||
if input.eventType == window_buffer_size_event { |
||||
xy := (*coord)(unsafe.Pointer(&input.blob[0])) |
||||
s.columns = int(xy.x) |
||||
return winch, nil |
||||
} |
||||
if input.eventType != key_event { |
||||
continue |
||||
} |
||||
ke := (*key_event_record)(unsafe.Pointer(&input.blob[0])) |
||||
if ke.KeyDown == 0 { |
||||
continue |
||||
} |
||||
|
||||
if ke.VirtualKeyCode == vk_tab && ke.ControlKeyState&modKeys == shiftPressed { |
||||
s.key = shiftTab |
||||
} else if ke.VirtualKeyCode == yKey && (ke.ControlKeyState&modKeys == leftAltPressed || |
||||
ke.ControlKeyState&modKeys == rightAltPressed) { |
||||
s.key = altY |
||||
} else if ke.Char > 0 { |
||||
s.key = rune(ke.Char) |
||||
} else { |
||||
switch ke.VirtualKeyCode { |
||||
case vk_prior: |
||||
s.key = pageUp |
||||
case vk_next: |
||||
s.key = pageDown |
||||
case vk_end: |
||||
s.key = end |
||||
case vk_home: |
||||
s.key = home |
||||
case vk_left: |
||||
s.key = left |
||||
if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 { |
||||
if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) { |
||||
s.key = wordLeft |
||||
} |
||||
} |
||||
case vk_right: |
||||
s.key = right |
||||
if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 { |
||||
if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) { |
||||
s.key = wordRight |
||||
} |
||||
} |
||||
case vk_up: |
||||
s.key = up |
||||
case vk_down: |
||||
s.key = down |
||||
case vk_insert: |
||||
s.key = insert |
||||
case vk_delete: |
||||
s.key = del |
||||
case vk_f1: |
||||
s.key = f1 |
||||
case vk_f2: |
||||
s.key = f2 |
||||
case vk_f3: |
||||
s.key = f3 |
||||
case vk_f4: |
||||
s.key = f4 |
||||
case vk_f5: |
||||
s.key = f5 |
||||
case vk_f6: |
||||
s.key = f6 |
||||
case vk_f7: |
||||
s.key = f7 |
||||
case vk_f8: |
||||
s.key = f8 |
||||
case vk_f9: |
||||
s.key = f9 |
||||
case vk_f10: |
||||
s.key = f10 |
||||
case vk_f11: |
||||
s.key = f11 |
||||
case vk_f12: |
||||
s.key = f12 |
||||
default: |
||||
// Eat modifier keys
|
||||
// TODO: return Action(Unknown) if the key isn't a
|
||||
// modifier.
|
||||
continue |
||||
} |
||||
} |
||||
|
||||
if ke.RepeatCount > 1 { |
||||
s.repeat = ke.RepeatCount - 1 |
||||
} |
||||
return s.key, nil |
||||
} |
||||
return unknown, nil |
||||
} |
||||
|
||||
// Close returns the terminal to its previous mode
|
||||
func (s *State) Close() error { |
||||
s.origMode.ApplyMode() |
||||
return nil |
||||
} |
||||
|
||||
func (s *State) startPrompt() { |
||||
if m, err := TerminalMode(); err == nil { |
||||
s.defaultMode = m.(inputMode) |
||||
mode := s.defaultMode |
||||
mode &^= enableProcessedInput |
||||
mode.ApplyMode() |
||||
} |
||||
} |
||||
|
||||
func (s *State) restartPrompt() { |
||||
} |
||||
|
||||
func (s *State) stopPrompt() { |
||||
s.defaultMode.ApplyMode() |
||||
} |
||||
|
||||
// TerminalSupported returns true because line editing is always
|
||||
// supported on Windows.
|
||||
func TerminalSupported() bool { |
||||
return true |
||||
} |
||||
|
||||
func (mode inputMode) ApplyMode() error { |
||||
hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle)) |
||||
if hIn == invalid_handle_value || hIn == 0 { |
||||
return err |
||||
} |
||||
ok, _, err := procSetConsoleMode.Call(hIn, uintptr(mode)) |
||||
if ok != 0 { |
||||
err = nil |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// TerminalMode returns the current terminal input mode as an InputModeSetter.
|
||||
//
|
||||
// This function is provided for convenience, and should
|
||||
// not be necessary for most users of liner.
|
||||
func TerminalMode() (ModeApplier, error) { |
||||
var mode inputMode |
||||
hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle)) |
||||
if hIn == invalid_handle_value || hIn == 0 { |
||||
return nil, err |
||||
} |
||||
ok, _, err := procGetConsoleMode.Call(hIn, uintptr(unsafe.Pointer(&mode))) |
||||
if ok != 0 { |
||||
err = nil |
||||
} |
||||
return mode, err |
||||
} |
@ -0,0 +1,864 @@ |
||||
// +build windows linux darwin openbsd freebsd netbsd
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"container/ring" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"strings" |
||||
"unicode" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
type action int |
||||
|
||||
const ( |
||||
left action = iota |
||||
right |
||||
up |
||||
down |
||||
home |
||||
end |
||||
insert |
||||
del |
||||
pageUp |
||||
pageDown |
||||
f1 |
||||
f2 |
||||
f3 |
||||
f4 |
||||
f5 |
||||
f6 |
||||
f7 |
||||
f8 |
||||
f9 |
||||
f10 |
||||
f11 |
||||
f12 |
||||
altY |
||||
shiftTab |
||||
wordLeft |
||||
wordRight |
||||
winch |
||||
unknown |
||||
) |
||||
|
||||
const ( |
||||
ctrlA = 1 |
||||
ctrlB = 2 |
||||
ctrlC = 3 |
||||
ctrlD = 4 |
||||
ctrlE = 5 |
||||
ctrlF = 6 |
||||
ctrlG = 7 |
||||
ctrlH = 8 |
||||
tab = 9 |
||||
lf = 10 |
||||
ctrlK = 11 |
||||
ctrlL = 12 |
||||
cr = 13 |
||||
ctrlN = 14 |
||||
ctrlO = 15 |
||||
ctrlP = 16 |
||||
ctrlQ = 17 |
||||
ctrlR = 18 |
||||
ctrlS = 19 |
||||
ctrlT = 20 |
||||
ctrlU = 21 |
||||
ctrlV = 22 |
||||
ctrlW = 23 |
||||
ctrlX = 24 |
||||
ctrlY = 25 |
||||
ctrlZ = 26 |
||||
esc = 27 |
||||
bs = 127 |
||||
) |
||||
|
||||
const ( |
||||
beep = "\a" |
||||
) |
||||
|
||||
type tabDirection int |
||||
|
||||
const ( |
||||
tabForward tabDirection = iota |
||||
tabReverse |
||||
) |
||||
|
||||
func (s *State) refresh(prompt []rune, buf []rune, pos int) error { |
||||
s.cursorPos(0) |
||||
_, err := fmt.Print(string(prompt)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pLen := countGlyphs(prompt) |
||||
bLen := countGlyphs(buf) |
||||
pos = countGlyphs(buf[:pos]) |
||||
if pLen+bLen < s.columns { |
||||
_, err = fmt.Print(string(buf)) |
||||
s.eraseLine() |
||||
s.cursorPos(pLen + pos) |
||||
} else { |
||||
// Find space available
|
||||
space := s.columns - pLen |
||||
space-- // space for cursor
|
||||
start := pos - space/2 |
||||
end := start + space |
||||
if end > bLen { |
||||
end = bLen |
||||
start = end - space |
||||
} |
||||
if start < 0 { |
||||
start = 0 |
||||
end = space |
||||
} |
||||
pos -= start |
||||
|
||||
// Leave space for markers
|
||||
if start > 0 { |
||||
start++ |
||||
} |
||||
if end < bLen { |
||||
end-- |
||||
} |
||||
startRune := len(getPrefixGlyphs(buf, start)) |
||||
line := getPrefixGlyphs(buf[startRune:], end-start) |
||||
|
||||
// Output
|
||||
if start > 0 { |
||||
fmt.Print("{") |
||||
} |
||||
fmt.Print(string(line)) |
||||
if end < bLen { |
||||
fmt.Print("}") |
||||
} |
||||
|
||||
// Set cursor position
|
||||
s.eraseLine() |
||||
s.cursorPos(pLen + pos) |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func longestCommonPrefix(strs []string) string { |
||||
if len(strs) == 0 { |
||||
return "" |
||||
} |
||||
longest := strs[0] |
||||
|
||||
for _, str := range strs[1:] { |
||||
for !strings.HasPrefix(str, longest) { |
||||
longest = longest[:len(longest)-1] |
||||
} |
||||
} |
||||
// Remove trailing partial runes
|
||||
longest = strings.TrimRight(longest, "\uFFFD") |
||||
return longest |
||||
} |
||||
|
||||
func (s *State) circularTabs(items []string) func(tabDirection) (string, error) { |
||||
item := -1 |
||||
return func(direction tabDirection) (string, error) { |
||||
if direction == tabForward { |
||||
if item < len(items)-1 { |
||||
item++ |
||||
} else { |
||||
item = 0 |
||||
} |
||||
} else if direction == tabReverse { |
||||
if item > 0 { |
||||
item-- |
||||
} else { |
||||
item = len(items) - 1 |
||||
} |
||||
} |
||||
return items[item], nil |
||||
} |
||||
} |
||||
|
||||
func (s *State) printedTabs(items []string) func(tabDirection) (string, error) { |
||||
numTabs := 1 |
||||
prefix := longestCommonPrefix(items) |
||||
return func(direction tabDirection) (string, error) { |
||||
if len(items) == 1 { |
||||
return items[0], nil |
||||
} |
||||
|
||||
if numTabs == 2 { |
||||
if len(items) > 100 { |
||||
fmt.Printf("\nDisplay all %d possibilities? (y or n) ", len(items)) |
||||
for { |
||||
next, err := s.readNext() |
||||
if err != nil { |
||||
return prefix, err |
||||
} |
||||
|
||||
if key, ok := next.(rune); ok { |
||||
if unicode.ToLower(key) == 'n' { |
||||
return prefix, nil |
||||
} else if unicode.ToLower(key) == 'y' { |
||||
break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
fmt.Println("") |
||||
maxWidth := 0 |
||||
for _, item := range items { |
||||
if len(item) >= maxWidth { |
||||
maxWidth = len(item) + 1 |
||||
} |
||||
} |
||||
|
||||
numColumns := s.columns / maxWidth |
||||
numRows := len(items) / numColumns |
||||
if len(items)%numColumns > 0 { |
||||
numRows++ |
||||
} |
||||
|
||||
if len(items) <= numColumns { |
||||
maxWidth = 0 |
||||
} |
||||
for i := 0; i < numRows; i++ { |
||||
for j := 0; j < numColumns*numRows; j += numRows { |
||||
if i+j < len(items) { |
||||
if maxWidth > 0 { |
||||
fmt.Printf("%-*s", maxWidth, items[i+j]) |
||||
} else { |
||||
fmt.Printf("%v ", items[i+j]) |
||||
} |
||||
} |
||||
} |
||||
fmt.Println("") |
||||
} |
||||
} else { |
||||
numTabs++ |
||||
} |
||||
return prefix, nil |
||||
} |
||||
} |
||||
|
||||
func (s *State) tabComplete(p []rune, line []rune, pos int) ([]rune, int, interface{}, error) { |
||||
if s.completer == nil { |
||||
return line, pos, rune(esc), nil |
||||
} |
||||
head, list, tail := s.completer(string(line), pos) |
||||
if len(list) <= 0 { |
||||
return line, pos, rune(esc), nil |
||||
} |
||||
hl := utf8.RuneCountInString(head) |
||||
if len(list) == 1 { |
||||
s.refresh(p, []rune(head+list[0]+tail), hl+utf8.RuneCountInString(list[0])) |
||||
return []rune(head + list[0] + tail), hl + utf8.RuneCountInString(list[0]), rune(esc), nil |
||||
} |
||||
|
||||
direction := tabForward |
||||
tabPrinter := s.circularTabs(list) |
||||
if s.tabStyle == TabPrints { |
||||
tabPrinter = s.printedTabs(list) |
||||
} |
||||
|
||||
for { |
||||
pick, err := tabPrinter(direction) |
||||
if err != nil { |
||||
return line, pos, rune(esc), err |
||||
} |
||||
s.refresh(p, []rune(head+pick+tail), hl+utf8.RuneCountInString(pick)) |
||||
|
||||
next, err := s.readNext() |
||||
if err != nil { |
||||
return line, pos, rune(esc), err |
||||
} |
||||
if key, ok := next.(rune); ok { |
||||
if key == tab { |
||||
direction = tabForward |
||||
continue |
||||
} |
||||
if key == esc { |
||||
return line, pos, rune(esc), nil |
||||
} |
||||
} |
||||
if a, ok := next.(action); ok && a == shiftTab { |
||||
direction = tabReverse |
||||
continue |
||||
} |
||||
return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil |
||||
} |
||||
// Not reached
|
||||
return line, pos, rune(esc), nil |
||||
} |
||||
|
||||
// reverse intelligent search, implements a bash-like history search.
|
||||
func (s *State) reverseISearch(origLine []rune, origPos int) ([]rune, int, interface{}, error) { |
||||
p := "(reverse-i-search)`': " |
||||
s.refresh([]rune(p), origLine, origPos) |
||||
|
||||
line := []rune{} |
||||
pos := 0 |
||||
foundLine := string(origLine) |
||||
foundPos := origPos |
||||
|
||||
getLine := func() ([]rune, []rune, int) { |
||||
search := string(line) |
||||
prompt := "(reverse-i-search)`%s': " |
||||
return []rune(fmt.Sprintf(prompt, search)), []rune(foundLine), foundPos |
||||
} |
||||
|
||||
history, positions := s.getHistoryByPattern(string(line)) |
||||
historyPos := len(history) - 1 |
||||
|
||||
for { |
||||
next, err := s.readNext() |
||||
if err != nil { |
||||
return []rune(foundLine), foundPos, rune(esc), err |
||||
} |
||||
|
||||
switch v := next.(type) { |
||||
case rune: |
||||
switch v { |
||||
case ctrlR: // Search backwards
|
||||
if historyPos > 0 && historyPos < len(history) { |
||||
historyPos-- |
||||
foundLine = history[historyPos] |
||||
foundPos = positions[historyPos] |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case ctrlS: // Search forward
|
||||
if historyPos < len(history)-1 && historyPos >= 0 { |
||||
historyPos++ |
||||
foundLine = history[historyPos] |
||||
foundPos = positions[historyPos] |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case ctrlH, bs: // Backspace
|
||||
if pos <= 0 { |
||||
fmt.Print(beep) |
||||
} else { |
||||
n := len(getSuffixGlyphs(line[:pos], 1)) |
||||
line = append(line[:pos-n], line[pos:]...) |
||||
pos -= n |
||||
|
||||
// For each char deleted, display the last matching line of history
|
||||
history, positions := s.getHistoryByPattern(string(line)) |
||||
historyPos = len(history) - 1 |
||||
if len(history) > 0 { |
||||
foundLine = history[historyPos] |
||||
foundPos = positions[historyPos] |
||||
} else { |
||||
foundLine = "" |
||||
foundPos = 0 |
||||
} |
||||
} |
||||
case ctrlG: // Cancel
|
||||
return origLine, origPos, rune(esc), err |
||||
|
||||
case tab, cr, lf, ctrlA, ctrlB, ctrlD, ctrlE, ctrlF, ctrlK, |
||||
ctrlL, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: |
||||
fallthrough |
||||
case 0, ctrlC, esc, 28, 29, 30, 31: |
||||
return []rune(foundLine), foundPos, next, err |
||||
default: |
||||
line = append(line[:pos], append([]rune{v}, line[pos:]...)...) |
||||
pos++ |
||||
|
||||
// For each keystroke typed, display the last matching line of history
|
||||
history, positions = s.getHistoryByPattern(string(line)) |
||||
historyPos = len(history) - 1 |
||||
if len(history) > 0 { |
||||
foundLine = history[historyPos] |
||||
foundPos = positions[historyPos] |
||||
} else { |
||||
foundLine = "" |
||||
foundPos = 0 |
||||
} |
||||
} |
||||
case action: |
||||
return []rune(foundLine), foundPos, next, err |
||||
} |
||||
s.refresh(getLine()) |
||||
} |
||||
} |
||||
|
||||
// addToKillRing adds some text to the kill ring. If mode is 0 it adds it to a
|
||||
// new node in the end of the kill ring, and move the current pointer to the new
|
||||
// node. If mode is 1 or 2 it appends or prepends the text to the current entry
|
||||
// of the killRing.
|
||||
func (s *State) addToKillRing(text []rune, mode int) { |
||||
// Don't use the same underlying array as text
|
||||
killLine := make([]rune, len(text)) |
||||
copy(killLine, text) |
||||
|
||||
// Point killRing to a newNode, procedure depends on the killring state and
|
||||
// append mode.
|
||||
if mode == 0 { // Add new node to killRing
|
||||
if s.killRing == nil { // if killring is empty, create a new one
|
||||
s.killRing = ring.New(1) |
||||
} else if s.killRing.Len() >= KillRingMax { // if killring is "full"
|
||||
s.killRing = s.killRing.Next() |
||||
} else { // Normal case
|
||||
s.killRing.Link(ring.New(1)) |
||||
s.killRing = s.killRing.Next() |
||||
} |
||||
} else { |
||||
if s.killRing == nil { // if killring is empty, create a new one
|
||||
s.killRing = ring.New(1) |
||||
s.killRing.Value = []rune{} |
||||
} |
||||
if mode == 1 { // Append to last entry
|
||||
killLine = append(s.killRing.Value.([]rune), killLine...) |
||||
} else if mode == 2 { // Prepend to last entry
|
||||
killLine = append(killLine, s.killRing.Value.([]rune)...) |
||||
} |
||||
} |
||||
|
||||
// Save text in the current killring node
|
||||
s.killRing.Value = killLine |
||||
} |
||||
|
||||
func (s *State) yank(p []rune, text []rune, pos int) ([]rune, int, interface{}, error) { |
||||
if s.killRing == nil { |
||||
return text, pos, rune(esc), nil |
||||
} |
||||
|
||||
lineStart := text[:pos] |
||||
lineEnd := text[pos:] |
||||
var line []rune |
||||
|
||||
for { |
||||
value := s.killRing.Value.([]rune) |
||||
line = make([]rune, 0) |
||||
line = append(line, lineStart...) |
||||
line = append(line, value...) |
||||
line = append(line, lineEnd...) |
||||
|
||||
pos = len(lineStart) + len(value) |
||||
s.refresh(p, line, pos) |
||||
|
||||
next, err := s.readNext() |
||||
if err != nil { |
||||
return line, pos, next, err |
||||
} |
||||
|
||||
switch v := next.(type) { |
||||
case rune: |
||||
return line, pos, next, nil |
||||
case action: |
||||
switch v { |
||||
case altY: |
||||
s.killRing = s.killRing.Prev() |
||||
default: |
||||
return line, pos, next, nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
return line, pos, esc, nil |
||||
} |
||||
|
||||
// Prompt displays p, and then waits for user input. Prompt allows line editing
|
||||
// if the terminal supports it.
|
||||
func (s *State) Prompt(prompt string) (string, error) { |
||||
if s.inputRedirected { |
||||
return s.promptUnsupported(prompt) |
||||
} |
||||
if s.outputRedirected { |
||||
return "", ErrNotTerminalOutput |
||||
} |
||||
if !s.terminalSupported { |
||||
return s.promptUnsupported(prompt) |
||||
} |
||||
|
||||
s.historyMutex.RLock() |
||||
defer s.historyMutex.RUnlock() |
||||
|
||||
s.startPrompt() |
||||
defer s.stopPrompt() |
||||
s.getColumns() |
||||
|
||||
fmt.Print(prompt) |
||||
p := []rune(prompt) |
||||
var line []rune |
||||
pos := 0 |
||||
historyEnd := "" |
||||
prefixHistory := s.getHistoryByPrefix(string(line)) |
||||
historyPos := len(prefixHistory) |
||||
historyAction := false // used to mark history related actions
|
||||
killAction := 0 // used to mark kill related actions
|
||||
mainLoop: |
||||
for { |
||||
next, err := s.readNext() |
||||
haveNext: |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
historyAction = false |
||||
switch v := next.(type) { |
||||
case rune: |
||||
switch v { |
||||
case cr, lf: |
||||
fmt.Println() |
||||
break mainLoop |
||||
case ctrlA: // Start of line
|
||||
pos = 0 |
||||
s.refresh(p, line, pos) |
||||
case ctrlE: // End of line
|
||||
pos = len(line) |
||||
s.refresh(p, line, pos) |
||||
case ctrlB: // left
|
||||
if pos > 0 { |
||||
pos -= len(getSuffixGlyphs(line[:pos], 1)) |
||||
s.refresh(p, line, pos) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case ctrlF: // right
|
||||
if pos < len(line) { |
||||
pos += len(getPrefixGlyphs(line[pos:], 1)) |
||||
s.refresh(p, line, pos) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case ctrlD: // del
|
||||
if pos == 0 && len(line) == 0 { |
||||
// exit
|
||||
return "", io.EOF |
||||
} |
||||
|
||||
// ctrlD is a potential EOF, so the rune reader shuts down.
|
||||
// Therefore, if it isn't actually an EOF, we must re-startPrompt.
|
||||
s.restartPrompt() |
||||
|
||||
if pos >= len(line) { |
||||
fmt.Print(beep) |
||||
} else { |
||||
n := len(getPrefixGlyphs(line[pos:], 1)) |
||||
line = append(line[:pos], line[pos+n:]...) |
||||
s.refresh(p, line, pos) |
||||
} |
||||
case ctrlK: // delete remainder of line
|
||||
if pos >= len(line) { |
||||
fmt.Print(beep) |
||||
} else { |
||||
if killAction > 0 { |
||||
s.addToKillRing(line[pos:], 1) // Add in apend mode
|
||||
} else { |
||||
s.addToKillRing(line[pos:], 0) // Add in normal mode
|
||||
} |
||||
|
||||
killAction = 2 // Mark that there was a kill action
|
||||
line = line[:pos] |
||||
s.refresh(p, line, pos) |
||||
} |
||||
case ctrlP: // up
|
||||
historyAction = true |
||||
if historyPos > 0 { |
||||
if historyPos == len(prefixHistory) { |
||||
historyEnd = string(line) |
||||
} |
||||
historyPos-- |
||||
line = []rune(prefixHistory[historyPos]) |
||||
pos = len(line) |
||||
s.refresh(p, line, pos) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case ctrlN: // down
|
||||
historyAction = true |
||||
if historyPos < len(prefixHistory) { |
||||
historyPos++ |
||||
if historyPos == len(prefixHistory) { |
||||
line = []rune(historyEnd) |
||||
} else { |
||||
line = []rune(prefixHistory[historyPos]) |
||||
} |
||||
pos = len(line) |
||||
s.refresh(p, line, pos) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case ctrlT: // transpose prev glyph with glyph under cursor
|
||||
if len(line) < 2 || pos < 1 { |
||||
fmt.Print(beep) |
||||
} else { |
||||
if pos == len(line) { |
||||
pos -= len(getSuffixGlyphs(line, 1)) |
||||
} |
||||
prev := getSuffixGlyphs(line[:pos], 1) |
||||
next := getPrefixGlyphs(line[pos:], 1) |
||||
scratch := make([]rune, len(prev)) |
||||
copy(scratch, prev) |
||||
copy(line[pos-len(prev):], next) |
||||
copy(line[pos-len(prev)+len(next):], scratch) |
||||
pos += len(next) |
||||
s.refresh(p, line, pos) |
||||
} |
||||
case ctrlL: // clear screen
|
||||
s.eraseScreen() |
||||
s.refresh(p, line, pos) |
||||
case ctrlC: // reset
|
||||
fmt.Println("^C") |
||||
if s.ctrlCAborts { |
||||
return "", ErrPromptAborted |
||||
} |
||||
line = line[:0] |
||||
pos = 0 |
||||
fmt.Print(prompt) |
||||
s.restartPrompt() |
||||
case ctrlH, bs: // Backspace
|
||||
if pos <= 0 { |
||||
fmt.Print(beep) |
||||
} else { |
||||
n := len(getSuffixGlyphs(line[:pos], 1)) |
||||
line = append(line[:pos-n], line[pos:]...) |
||||
pos -= n |
||||
s.refresh(p, line, pos) |
||||
} |
||||
case ctrlU: // Erase line before cursor
|
||||
if killAction > 0 { |
||||
s.addToKillRing(line[:pos], 2) // Add in prepend mode
|
||||
} else { |
||||
s.addToKillRing(line[:pos], 0) // Add in normal mode
|
||||
} |
||||
|
||||
killAction = 2 // Mark that there was some killing
|
||||
line = line[pos:] |
||||
pos = 0 |
||||
s.refresh(p, line, pos) |
||||
case ctrlW: // Erase word
|
||||
if pos == 0 { |
||||
fmt.Print(beep) |
||||
break |
||||
} |
||||
// Remove whitespace to the left
|
||||
var buf []rune // Store the deleted chars in a buffer
|
||||
for { |
||||
if pos == 0 || !unicode.IsSpace(line[pos-1]) { |
||||
break |
||||
} |
||||
buf = append(buf, line[pos-1]) |
||||
line = append(line[:pos-1], line[pos:]...) |
||||
pos-- |
||||
} |
||||
// Remove non-whitespace to the left
|
||||
for { |
||||
if pos == 0 || unicode.IsSpace(line[pos-1]) { |
||||
break |
||||
} |
||||
buf = append(buf, line[pos-1]) |
||||
line = append(line[:pos-1], line[pos:]...) |
||||
pos-- |
||||
} |
||||
// Invert the buffer and save the result on the killRing
|
||||
var newBuf []rune |
||||
for i := len(buf) - 1; i >= 0; i-- { |
||||
newBuf = append(newBuf, buf[i]) |
||||
} |
||||
if killAction > 0 { |
||||
s.addToKillRing(newBuf, 2) // Add in prepend mode
|
||||
} else { |
||||
s.addToKillRing(newBuf, 0) // Add in normal mode
|
||||
} |
||||
killAction = 2 // Mark that there was some killing
|
||||
|
||||
s.refresh(p, line, pos) |
||||
case ctrlY: // Paste from Yank buffer
|
||||
line, pos, next, err = s.yank(p, line, pos) |
||||
goto haveNext |
||||
case ctrlR: // Reverse Search
|
||||
line, pos, next, err = s.reverseISearch(line, pos) |
||||
s.refresh(p, line, pos) |
||||
goto haveNext |
||||
case tab: // Tab completion
|
||||
line, pos, next, err = s.tabComplete(p, line, pos) |
||||
goto haveNext |
||||
// Catch keys that do nothing, but you don't want them to beep
|
||||
case esc: |
||||
// DO NOTHING
|
||||
// Unused keys
|
||||
case ctrlG, ctrlO, ctrlQ, ctrlS, ctrlV, ctrlX, ctrlZ: |
||||
fallthrough |
||||
// Catch unhandled control codes (anything <= 31)
|
||||
case 0, 28, 29, 30, 31: |
||||
fmt.Print(beep) |
||||
default: |
||||
if pos == len(line) && len(p)+len(line) < s.columns-1 { |
||||
line = append(line, v) |
||||
fmt.Printf("%c", v) |
||||
pos++ |
||||
} else { |
||||
line = append(line[:pos], append([]rune{v}, line[pos:]...)...) |
||||
pos++ |
||||
s.refresh(p, line, pos) |
||||
} |
||||
} |
||||
case action: |
||||
switch v { |
||||
case del: |
||||
if pos >= len(line) { |
||||
fmt.Print(beep) |
||||
} else { |
||||
n := len(getPrefixGlyphs(line[pos:], 1)) |
||||
line = append(line[:pos], line[pos+n:]...) |
||||
} |
||||
case left: |
||||
if pos > 0 { |
||||
pos -= len(getSuffixGlyphs(line[:pos], 1)) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case wordLeft: |
||||
if pos > 0 { |
||||
for { |
||||
pos-- |
||||
if pos == 0 || unicode.IsSpace(line[pos-1]) { |
||||
break |
||||
} |
||||
} |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case right: |
||||
if pos < len(line) { |
||||
pos += len(getPrefixGlyphs(line[pos:], 1)) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case wordRight: |
||||
if pos < len(line) { |
||||
for { |
||||
pos++ |
||||
if pos == len(line) || unicode.IsSpace(line[pos]) { |
||||
break |
||||
} |
||||
} |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case up: |
||||
historyAction = true |
||||
if historyPos > 0 { |
||||
if historyPos == len(prefixHistory) { |
||||
historyEnd = string(line) |
||||
} |
||||
historyPos-- |
||||
line = []rune(prefixHistory[historyPos]) |
||||
pos = len(line) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case down: |
||||
historyAction = true |
||||
if historyPos < len(prefixHistory) { |
||||
historyPos++ |
||||
if historyPos == len(prefixHistory) { |
||||
line = []rune(historyEnd) |
||||
} else { |
||||
line = []rune(prefixHistory[historyPos]) |
||||
} |
||||
pos = len(line) |
||||
} else { |
||||
fmt.Print(beep) |
||||
} |
||||
case home: // Start of line
|
||||
pos = 0 |
||||
case end: // End of line
|
||||
pos = len(line) |
||||
} |
||||
s.refresh(p, line, pos) |
||||
} |
||||
if !historyAction { |
||||
prefixHistory = s.getHistoryByPrefix(string(line)) |
||||
historyPos = len(prefixHistory) |
||||
} |
||||
if killAction > 0 { |
||||
killAction-- |
||||
} |
||||
} |
||||
return string(line), nil |
||||
} |
||||
|
||||
// PasswordPrompt displays p, and then waits for user input. The input typed by
|
||||
// the user is not displayed in the terminal.
|
||||
func (s *State) PasswordPrompt(prompt string) (string, error) { |
||||
if s.inputRedirected { |
||||
return s.promptUnsupported(prompt) |
||||
} |
||||
if s.outputRedirected { |
||||
return "", ErrNotTerminalOutput |
||||
} |
||||
if !s.terminalSupported { |
||||
return "", errors.New("liner: function not supported in this terminal") |
||||
} |
||||
|
||||
s.startPrompt() |
||||
defer s.stopPrompt() |
||||
s.getColumns() |
||||
|
||||
fmt.Print(prompt) |
||||
p := []rune(prompt) |
||||
var line []rune |
||||
pos := 0 |
||||
|
||||
mainLoop: |
||||
for { |
||||
next, err := s.readNext() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
switch v := next.(type) { |
||||
case rune: |
||||
switch v { |
||||
case cr, lf: |
||||
fmt.Println() |
||||
break mainLoop |
||||
case ctrlD: // del
|
||||
if pos == 0 && len(line) == 0 { |
||||
// exit
|
||||
return "", io.EOF |
||||
} |
||||
|
||||
// ctrlD is a potential EOF, so the rune reader shuts down.
|
||||
// Therefore, if it isn't actually an EOF, we must re-startPrompt.
|
||||
s.restartPrompt() |
||||
case ctrlL: // clear screen
|
||||
s.eraseScreen() |
||||
s.refresh(p, []rune{}, 0) |
||||
case ctrlH, bs: // Backspace
|
||||
if pos <= 0 { |
||||
fmt.Print(beep) |
||||
} else { |
||||
n := len(getSuffixGlyphs(line[:pos], 1)) |
||||
line = append(line[:pos-n], line[pos:]...) |
||||
pos -= n |
||||
} |
||||
case ctrlC: |
||||
fmt.Println("^C") |
||||
if s.ctrlCAborts { |
||||
return "", ErrPromptAborted |
||||
} |
||||
line = line[:0] |
||||
pos = 0 |
||||
fmt.Print(prompt) |
||||
s.restartPrompt() |
||||
// Unused keys
|
||||
case esc, tab, ctrlA, ctrlB, ctrlE, ctrlF, ctrlG, ctrlK, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlR, ctrlS, |
||||
ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: |
||||
fallthrough |
||||
// Catch unhandled control codes (anything <= 31)
|
||||
case 0, 28, 29, 30, 31: |
||||
fmt.Print(beep) |
||||
default: |
||||
line = append(line[:pos], append([]rune{v}, line[pos:]...)...) |
||||
pos++ |
||||
} |
||||
} |
||||
} |
||||
return string(line), nil |
||||
} |
@ -0,0 +1,90 @@ |
||||
package liner |
||||
|
||||
import ( |
||||
"bytes" |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
func TestAppend(t *testing.T) { |
||||
var s State |
||||
s.AppendHistory("foo") |
||||
s.AppendHistory("bar") |
||||
|
||||
var out bytes.Buffer |
||||
num, err := s.WriteHistory(&out) |
||||
if err != nil { |
||||
t.Fatal("Unexpected error writing history", err) |
||||
} |
||||
if num != 2 { |
||||
t.Fatalf("Expected 2 history entries, got %d", num) |
||||
} |
||||
|
||||
s.AppendHistory("baz") |
||||
num, err = s.WriteHistory(&out) |
||||
if err != nil { |
||||
t.Fatal("Unexpected error writing history", err) |
||||
} |
||||
if num != 3 { |
||||
t.Fatalf("Expected 3 history entries, got %d", num) |
||||
} |
||||
|
||||
s.AppendHistory("baz") |
||||
num, err = s.WriteHistory(&out) |
||||
if err != nil { |
||||
t.Fatal("Unexpected error writing history", err) |
||||
} |
||||
if num != 3 { |
||||
t.Fatalf("Expected 3 history entries after duplicate append, got %d", num) |
||||
} |
||||
|
||||
s.AppendHistory("baz") |
||||
|
||||
} |
||||
|
||||
func TestHistory(t *testing.T) { |
||||
input := `foo |
||||
bar |
||||
baz |
||||
quux |
||||
dingle` |
||||
|
||||
var s State |
||||
num, err := s.ReadHistory(strings.NewReader(input)) |
||||
if err != nil { |
||||
t.Fatal("Unexpected error reading history", err) |
||||
} |
||||
if num != 5 { |
||||
t.Fatal("Wrong number of history entries read") |
||||
} |
||||
|
||||
var out bytes.Buffer |
||||
num, err = s.WriteHistory(&out) |
||||
if err != nil { |
||||
t.Fatal("Unexpected error writing history", err) |
||||
} |
||||
if num != 5 { |
||||
t.Fatal("Wrong number of history entries written") |
||||
} |
||||
if strings.TrimSpace(out.String()) != input { |
||||
t.Fatal("Round-trip failure") |
||||
} |
||||
|
||||
// Test reading with a trailing newline present
|
||||
var s2 State |
||||
num, err = s2.ReadHistory(&out) |
||||
if err != nil { |
||||
t.Fatal("Unexpected error reading history the 2nd time", err) |
||||
} |
||||
if num != 5 { |
||||
t.Fatal("Wrong number of history entries read the 2nd time") |
||||
} |
||||
|
||||
num, err = s.ReadHistory(strings.NewReader(input + "\n\xff")) |
||||
if err == nil { |
||||
t.Fatal("Unexpected success reading corrupted history", err) |
||||
} |
||||
if num != 5 { |
||||
t.Fatal("Wrong number of history entries read the 3rd time") |
||||
} |
||||
} |
@ -0,0 +1,63 @@ |
||||
// +build linux darwin openbsd freebsd netbsd
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"strings" |
||||
"syscall" |
||||
"unsafe" |
||||
) |
||||
|
||||
func (s *State) cursorPos(x int) { |
||||
if s.useCHA { |
||||
// 'G' is "Cursor Character Absolute (CHA)"
|
||||
fmt.Printf("\x1b[%dG", x+1) |
||||
} else { |
||||
// 'C' is "Cursor Forward (CUF)"
|
||||
fmt.Print("\r") |
||||
if x > 0 { |
||||
fmt.Printf("\x1b[%dC", x) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *State) eraseLine() { |
||||
fmt.Print("\x1b[0K") |
||||
} |
||||
|
||||
func (s *State) eraseScreen() { |
||||
fmt.Print("\x1b[H\x1b[2J") |
||||
} |
||||
|
||||
type winSize struct { |
||||
row, col uint16 |
||||
xpixel, ypixel uint16 |
||||
} |
||||
|
||||
func (s *State) getColumns() { |
||||
var ws winSize |
||||
ok, _, _ := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdout), |
||||
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws))) |
||||
if ok < 0 { |
||||
s.columns = 80 |
||||
} |
||||
s.columns = int(ws.col) |
||||
} |
||||
|
||||
func (s *State) checkOutput() { |
||||
// xterm is known to support CHA
|
||||
if strings.Contains(strings.ToLower(os.Getenv("TERM")), "xterm") { |
||||
s.useCHA = true |
||||
return |
||||
} |
||||
|
||||
// The test for functional ANSI CHA is unreliable (eg the Windows
|
||||
// telnet command does not support reading the cursor position with
|
||||
// an ANSI DSR request, despite setting TERM=ansi)
|
||||
|
||||
// Assume CHA isn't supported (which should be safe, although it
|
||||
// does result in occasional visible cursor jitter)
|
||||
s.useCHA = false |
||||
} |
@ -0,0 +1,54 @@ |
||||
package liner |
||||
|
||||
import ( |
||||
"unsafe" |
||||
) |
||||
|
||||
type coord struct { |
||||
x, y int16 |
||||
} |
||||
type smallRect struct { |
||||
left, top, right, bottom int16 |
||||
} |
||||
|
||||
type consoleScreenBufferInfo struct { |
||||
dwSize coord |
||||
dwCursorPosition coord |
||||
wAttributes int16 |
||||
srWindow smallRect |
||||
dwMaximumWindowSize coord |
||||
} |
||||
|
||||
func (s *State) cursorPos(x int) { |
||||
var sbi consoleScreenBufferInfo |
||||
procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) |
||||
procSetConsoleCursorPosition.Call(uintptr(s.hOut), |
||||
uintptr(int(x)&0xFFFF|int(sbi.dwCursorPosition.y)<<16)) |
||||
} |
||||
|
||||
func (s *State) eraseLine() { |
||||
var sbi consoleScreenBufferInfo |
||||
procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) |
||||
var numWritten uint32 |
||||
procFillConsoleOutputCharacter.Call(uintptr(s.hOut), uintptr(' '), |
||||
uintptr(sbi.dwSize.x-sbi.dwCursorPosition.x), |
||||
uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|int(sbi.dwCursorPosition.y)<<16), |
||||
uintptr(unsafe.Pointer(&numWritten))) |
||||
} |
||||
|
||||
func (s *State) eraseScreen() { |
||||
var sbi consoleScreenBufferInfo |
||||
procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) |
||||
var numWritten uint32 |
||||
procFillConsoleOutputCharacter.Call(uintptr(s.hOut), uintptr(' '), |
||||
uintptr(sbi.dwSize.x)*uintptr(sbi.dwSize.y), |
||||
0, |
||||
uintptr(unsafe.Pointer(&numWritten))) |
||||
procSetConsoleCursorPosition.Call(uintptr(s.hOut), 0) |
||||
} |
||||
|
||||
func (s *State) getColumns() { |
||||
var sbi consoleScreenBufferInfo |
||||
procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) |
||||
s.columns = int(sbi.dwSize.x) |
||||
} |
@ -0,0 +1,37 @@ |
||||
// +build windows linux darwin openbsd freebsd netbsd
|
||||
|
||||
package liner |
||||
|
||||
import "testing" |
||||
|
||||
type testItem struct { |
||||
list []string |
||||
prefix string |
||||
} |
||||
|
||||
func TestPrefix(t *testing.T) { |
||||
list := []testItem{ |
||||
{[]string{"food", "foot"}, "foo"}, |
||||
{[]string{"foo", "foot"}, "foo"}, |
||||
{[]string{"food", "foo"}, "foo"}, |
||||
{[]string{"food", "foe", "foot"}, "fo"}, |
||||
{[]string{"food", "foot", "barbeque"}, ""}, |
||||
{[]string{"cafeteria", "café"}, "caf"}, |
||||
{[]string{"cafe", "café"}, "caf"}, |
||||
{[]string{"cafè", "café"}, "caf"}, |
||||
{[]string{"cafés", "café"}, "café"}, |
||||
{[]string{"áéíóú", "áéíóú"}, "áéíóú"}, |
||||
{[]string{"éclairs", "éclairs"}, "éclairs"}, |
||||
{[]string{"éclairs are the best", "éclairs are great", "éclairs"}, "éclairs"}, |
||||
{[]string{"éclair", "éclairs"}, "éclair"}, |
||||
{[]string{"éclairs", "éclair"}, "éclair"}, |
||||
{[]string{"éclair", "élan"}, "é"}, |
||||
} |
||||
|
||||
for _, test := range list { |
||||
lcp := longestCommonPrefix(test.list) |
||||
if lcp != test.prefix { |
||||
t.Errorf("%s != %s for %+v", lcp, test.prefix, test.list) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
// +build race
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"os" |
||||
"sync" |
||||
"testing" |
||||
) |
||||
|
||||
func TestWriteHistory(t *testing.T) { |
||||
oldout := os.Stdout |
||||
defer func() { os.Stdout = oldout }() |
||||
oldin := os.Stdout |
||||
defer func() { os.Stdin = oldin }() |
||||
|
||||
newinr, newinw, err := os.Pipe() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
os.Stdin = newinr |
||||
newoutr, newoutw, err := os.Pipe() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
defer newoutr.Close() |
||||
os.Stdout = newoutw |
||||
|
||||
var wait sync.WaitGroup |
||||
wait.Add(1) |
||||
s := NewLiner() |
||||
go func() { |
||||
s.AppendHistory("foo") |
||||
s.AppendHistory("bar") |
||||
s.Prompt("") |
||||
wait.Done() |
||||
}() |
||||
|
||||
s.WriteHistory(ioutil.Discard) |
||||
|
||||
newinw.Close() |
||||
wait.Wait() |
||||
} |
@ -0,0 +1,12 @@ |
||||
// +build go1.1,!windows
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"os" |
||||
"os/signal" |
||||
) |
||||
|
||||
func stopSignal(c chan<- os.Signal) { |
||||
signal.Stop(c) |
||||
} |
@ -0,0 +1,11 @@ |
||||
// +build !go1.1,!windows
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"os" |
||||
) |
||||
|
||||
func stopSignal(c chan<- os.Signal) { |
||||
// signal.Stop does not exist before Go 1.1
|
||||
} |
@ -0,0 +1,37 @@ |
||||
// +build linux darwin freebsd openbsd netbsd
|
||||
|
||||
package liner |
||||
|
||||
import ( |
||||
"syscall" |
||||
"unsafe" |
||||
) |
||||
|
||||
func (mode *termios) ApplyMode() error { |
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), setTermios, uintptr(unsafe.Pointer(mode))) |
||||
|
||||
if errno != 0 { |
||||
return errno |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// TerminalMode returns the current terminal input mode as an InputModeSetter.
|
||||
//
|
||||
// This function is provided for convenience, and should
|
||||
// not be necessary for most users of liner.
|
||||
func TerminalMode() (ModeApplier, error) { |
||||
mode, errno := getMode(syscall.Stdin) |
||||
|
||||
if errno != 0 { |
||||
return nil, errno |
||||
} |
||||
return mode, nil |
||||
} |
||||
|
||||
func getMode(handle int) (*termios, syscall.Errno) { |
||||
var mode termios |
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(handle), getTermios, uintptr(unsafe.Pointer(&mode))) |
||||
|
||||
return &mode, errno |
||||
} |
@ -0,0 +1,47 @@ |
||||
package liner |
||||
|
||||
import "unicode" |
||||
|
||||
// These character classes are mostly zero width (when combined).
|
||||
// A few might not be, depending on the user's font. Fixing this
|
||||
// is non-trivial, given that some terminals don't support
|
||||
// ANSI DSR/CPR
|
||||
var zeroWidth = []*unicode.RangeTable{ |
||||
unicode.Mn, |
||||
unicode.Me, |
||||
unicode.Cc, |
||||
unicode.Cf, |
||||
} |
||||
|
||||
func countGlyphs(s []rune) int { |
||||
n := 0 |
||||
for _, r := range s { |
||||
if !unicode.IsOneOf(zeroWidth, r) { |
||||
n++ |
||||
} |
||||
} |
||||
return n |
||||
} |
||||
|
||||
func getPrefixGlyphs(s []rune, num int) []rune { |
||||
p := 0 |
||||
for n := 0; n < num && p < len(s); p++ { |
||||
if !unicode.IsOneOf(zeroWidth, s[p]) { |
||||
n++ |
||||
} |
||||
} |
||||
for p < len(s) && unicode.IsOneOf(zeroWidth, s[p]) { |
||||
p++ |
||||
} |
||||
return s[:p] |
||||
} |
||||
|
||||
func getSuffixGlyphs(s []rune, num int) []rune { |
||||
p := len(s) |
||||
for n := 0; n < num && p > 0; p-- { |
||||
if !unicode.IsOneOf(zeroWidth, s[p-1]) { |
||||
n++ |
||||
} |
||||
} |
||||
return s[p:] |
||||
} |
@ -0,0 +1,87 @@ |
||||
package liner |
||||
|
||||
import ( |
||||
"strconv" |
||||
"testing" |
||||
) |
||||
|
||||
func accent(in []rune) []rune { |
||||
var out []rune |
||||
for _, r := range in { |
||||
out = append(out, r) |
||||
out = append(out, '\u0301') |
||||
} |
||||
return out |
||||
} |
||||
|
||||
var testString = []rune("query") |
||||
|
||||
func TestCountGlyphs(t *testing.T) { |
||||
count := countGlyphs(testString) |
||||
if count != len(testString) { |
||||
t.Errorf("ASCII count incorrect. %d != %d", count, len(testString)) |
||||
} |
||||
count = countGlyphs(accent(testString)) |
||||
if count != len(testString) { |
||||
t.Errorf("Accent count incorrect. %d != %d", count, len(testString)) |
||||
} |
||||
} |
||||
|
||||
func compare(a, b []rune, name string, t *testing.T) { |
||||
if len(a) != len(b) { |
||||
t.Errorf(`"%s" != "%s" in %s"`, string(a), string(b), name) |
||||
return |
||||
} |
||||
for i := range a { |
||||
if a[i] != b[i] { |
||||
t.Errorf(`"%s" != "%s" in %s"`, string(a), string(b), name) |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestPrefixGlyphs(t *testing.T) { |
||||
for i := 0; i <= len(testString); i++ { |
||||
iter := strconv.Itoa(i) |
||||
out := getPrefixGlyphs(testString, i) |
||||
compare(out, testString[:i], "ascii prefix "+iter, t) |
||||
out = getPrefixGlyphs(accent(testString), i) |
||||
compare(out, accent(testString[:i]), "accent prefix "+iter, t) |
||||
} |
||||
out := getPrefixGlyphs(testString, 999) |
||||
compare(out, testString, "ascii prefix overflow", t) |
||||
out = getPrefixGlyphs(accent(testString), 999) |
||||
compare(out, accent(testString), "accent prefix overflow", t) |
||||
|
||||
out = getPrefixGlyphs(testString, -3) |
||||
if len(out) != 0 { |
||||
t.Error("ascii prefix negative") |
||||
} |
||||
out = getPrefixGlyphs(accent(testString), -3) |
||||
if len(out) != 0 { |
||||
t.Error("accent prefix negative") |
||||
} |
||||
} |
||||
|
||||
func TestSuffixGlyphs(t *testing.T) { |
||||
for i := 0; i <= len(testString); i++ { |
||||
iter := strconv.Itoa(i) |
||||
out := getSuffixGlyphs(testString, i) |
||||
compare(out, testString[len(testString)-i:], "ascii suffix "+iter, t) |
||||
out = getSuffixGlyphs(accent(testString), i) |
||||
compare(out, accent(testString[len(testString)-i:]), "accent suffix "+iter, t) |
||||
} |
||||
out := getSuffixGlyphs(testString, 999) |
||||
compare(out, testString, "ascii suffix overflow", t) |
||||
out = getSuffixGlyphs(accent(testString), 999) |
||||
compare(out, accent(testString), "accent suffix overflow", t) |
||||
|
||||
out = getSuffixGlyphs(testString, -3) |
||||
if len(out) != 0 { |
||||
t.Error("ascii suffix negative") |
||||
} |
||||
out = getSuffixGlyphs(accent(testString), -3) |
||||
if len(out) != 0 { |
||||
t.Error("accent suffix negative") |
||||
} |
||||
} |
Loading…
Reference in new issue