Godeps: add github.com/peterh/liner

pull/433/head
Felix Lange 10 years ago
parent 38f6d60e6e
commit 2393de5d6b
  1. 4
      Godeps/Godeps.json
  2. 21
      Godeps/_workspace/src/github.com/peterh/liner/COPYING
  3. 95
      Godeps/_workspace/src/github.com/peterh/liner/README.md
  4. 39
      Godeps/_workspace/src/github.com/peterh/liner/bsdinput.go
  5. 219
      Godeps/_workspace/src/github.com/peterh/liner/common.go
  6. 57
      Godeps/_workspace/src/github.com/peterh/liner/fallbackinput.go
  7. 359
      Godeps/_workspace/src/github.com/peterh/liner/input.go
  8. 39
      Godeps/_workspace/src/github.com/peterh/liner/input_darwin.go
  9. 26
      Godeps/_workspace/src/github.com/peterh/liner/input_linux.go
  10. 61
      Godeps/_workspace/src/github.com/peterh/liner/input_test.go
  11. 313
      Godeps/_workspace/src/github.com/peterh/liner/input_windows.go
  12. 864
      Godeps/_workspace/src/github.com/peterh/liner/line.go
  13. 90
      Godeps/_workspace/src/github.com/peterh/liner/line_test.go
  14. 63
      Godeps/_workspace/src/github.com/peterh/liner/output.go
  15. 54
      Godeps/_workspace/src/github.com/peterh/liner/output_windows.go
  16. 37
      Godeps/_workspace/src/github.com/peterh/liner/prefix_test.go
  17. 44
      Godeps/_workspace/src/github.com/peterh/liner/race_test.go
  18. 12
      Godeps/_workspace/src/github.com/peterh/liner/signal.go
  19. 11
      Godeps/_workspace/src/github.com/peterh/liner/signal_legacy.go
  20. 37
      Godeps/_workspace/src/github.com/peterh/liner/unixmode.go
  21. 47
      Godeps/_workspace/src/github.com/peterh/liner/width.go
  22. 87
      Godeps/_workspace/src/github.com/peterh/liner/width_test.go

4
Godeps/Godeps.json generated vendored

@ -54,6 +54,10 @@
"ImportPath": "github.com/obscuren/qml",
"Rev": "c288002b52e905973b131089a8a7c761d4a2c36a"
},
{
"ImportPath": "github.com/peterh/liner",
"Rev": "29f6a646557d83e2b6e9ba05c45fbea9c006dbe8"
},
{
"ImportPath": "github.com/rakyll/globalconf",
"Rev": "415abc325023f1a00cd2d9fa512e0e71745791a2"

@ -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…
Cancel
Save