Godeps: add github.com/peterh/liner
This commit is contained in:
parent
38f6d60e6e
commit
2393de5d6b
|
@ -54,6 +54,10 @@
|
||||||
"ImportPath": "github.com/obscuren/qml",
|
"ImportPath": "github.com/obscuren/qml",
|
||||||
"Rev": "c288002b52e905973b131089a8a7c761d4a2c36a"
|
"Rev": "c288002b52e905973b131089a8a7c761d4a2c36a"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/peterh/liner",
|
||||||
|
"Rev": "29f6a646557d83e2b6e9ba05c45fbea9c006dbe8"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/rakyll/globalconf",
|
"ImportPath": "github.com/rakyll/globalconf",
|
||||||
"Rev": "415abc325023f1a00cd2d9fa512e0e71745791a2"
|
"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…
Reference in New Issue