whatcanGOwrong
This commit is contained in:
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
#### Liner is a scratch-your-own-itch project
|
||||
|
||||
While I try my best to fix any bugs encountered in liner, I do not have
|
||||
sufficient time to implement feature requests on your behalf.
|
||||
|
||||
If you are opening a feature request, you are implicitly volunteering to
|
||||
implement that feature. Obvious feature requests should be made via a pull
|
||||
request. Complex feature requests will be interpreted as a
|
||||
request-for-comments, and will be closed once comments are given.
|
||||
|
||||
#### Liner must remain backwards compatible
|
||||
|
||||
The API of Liner must not change in an incompatible way. When making
|
||||
changes to liner, please use the [Go 1 Compatibility Promise](https://golang.org/doc/go1compat)
|
||||
as a guideline.
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
If you have a feature request, please see the Contribution Guidelines before
|
||||
proceeding.
|
||||
|
||||
If you have a bug report, please supply the following information:
|
||||
|
||||
- Operating System (eg. Windows, Linux, Mac)
|
||||
- Terminal Emulator (eg. xterm, gnome-terminal, konsole, ConEmu, Terminal.app, Command Prompt)
|
||||
- Bug behaviour
|
||||
- Expected behaviour
|
||||
- Complete sample that reproduces the bug
|
||||
@@ -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,114 @@
|
||||
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 intended for use by cross-platform applications. Therefore, the
|
||||
decision was made to write it in pure Go, avoiding cgo, for ease of cross
|
||||
compilation. Furthermore, features only supported on some platforms have
|
||||
been intentionally omitted. For example, Ctrl-Z is "suspend" on Unix, but
|
||||
"EOF" on Windows. In the interest of making an application behave the same
|
||||
way on every supported platform, Ctrl-Z is ignored by Liner.
|
||||
|
||||
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, Alt-B | Move cursor to previous word
|
||||
Ctrl-Right, Alt-F | 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, Alt-BackSpace | Delete word leading up to cursor
|
||||
Alt-D | Delete word following 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
|
||||
|
||||
Note that "Previous" and "Next match from history" will retain the part of
|
||||
the line that the user has already typed, similar to zsh's
|
||||
"up-line-or-beginning-search" (which is the default on some systems) or
|
||||
bash's "history-search-backward" (which is my preferred behaviour, but does
|
||||
not appear to be the default `Up` keybinding on any system).
|
||||
|
||||
Getting started
|
||||
-----------------
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/peterh/liner"
|
||||
)
|
||||
|
||||
var (
|
||||
history_fn = filepath.Join(os.TempDir(), ".liner_example_history")
|
||||
names = []string{"john", "james", "mary", "nancy"}
|
||||
)
|
||||
|
||||
func main() {
|
||||
line := liner.NewLiner()
|
||||
defer line.Close()
|
||||
|
||||
line.SetCtrlCAborts(true)
|
||||
|
||||
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("Got: ", name)
|
||||
line.AppendHistory(name)
|
||||
} else if err == liner.ErrPromptAborted {
|
||||
log.Print("Aborted")
|
||||
} else {
|
||||
log.Print("Error reading line: ", err)
|
||||
}
|
||||
|
||||
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,42 @@
|
||||
//go:build openbsd || freebsd || netbsd
|
||||
// +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
|
||||
}
|
||||
|
||||
const cursorColumn = false
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
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"
|
||||
"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
|
||||
ctrlZStop bool
|
||||
r *bufio.Reader
|
||||
tabStyle TabStyle
|
||||
multiLineMode bool
|
||||
cursorRows int
|
||||
maxRows int
|
||||
shouldRestart ShouldRestart
|
||||
noBeep bool
|
||||
needRefresh bool
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
// ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the
|
||||
// prompt contains any unprintable runes (including substrings that could
|
||||
// be colour codes on some platforms).
|
||||
var ErrInvalidPrompt = errors.New("invalid prompt")
|
||||
|
||||
// ErrInternal is returned when liner experiences an error that it cannot
|
||||
// handle. For example, if the number of colums becomes zero during an
|
||||
// active call to Prompt
|
||||
var ErrInternal = errors.New("liner: internal error")
|
||||
|
||||
// KillRingMax is the max number of 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:]
|
||||
}
|
||||
}
|
||||
|
||||
// ClearHistory clears the scrollback history.
|
||||
func (s *State) ClearHistory() {
|
||||
s.historyMutex.Lock()
|
||||
defer s.historyMutex.Unlock()
|
||||
s.history = nil
|
||||
}
|
||||
|
||||
// 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 intelligent 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(string([]rune(line)[:pos])), string([]rune(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
|
||||
}
|
||||
|
||||
// SetCtrlZStop sets whether Prompt on a supported terminal will send
|
||||
// SIGTSTP when Ctrl-Z is pressed. The default is false
|
||||
func (s *State) SetCtrlZStop(stop bool) {
|
||||
s.ctrlZStop = stop
|
||||
}
|
||||
|
||||
// SetMultiLineMode sets whether line is auto-wrapped. The default is false (single line).
|
||||
func (s *State) SetMultiLineMode(mlmode bool) {
|
||||
s.multiLineMode = mlmode
|
||||
}
|
||||
|
||||
// ShouldRestart is passed the error generated by readNext and returns true if
|
||||
// the the read should be restarted or false if the error should be returned.
|
||||
type ShouldRestart func(err error) bool
|
||||
|
||||
// SetShouldRestart sets the restart function that Liner will call to determine
|
||||
// whether to retry the call to, or return the error returned by, readNext.
|
||||
func (s *State) SetShouldRestart(f ShouldRestart) {
|
||||
s.shouldRestart = f
|
||||
}
|
||||
|
||||
// SetBeep sets whether liner should beep the terminal at various times (output
|
||||
// ASCII BEL, 0x07). Default is true (will beep).
|
||||
func (s *State) SetBeep(beep bool) {
|
||||
s.noBeep = !beep
|
||||
}
|
||||
|
||||
func (s *State) promptUnsupported(p string) (string, error) {
|
||||
if !s.inputRedirected || !s.terminalSupported {
|
||||
fmt.Print(p)
|
||||
}
|
||||
linebuf, _, err := s.r.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(linebuf), nil
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
//go:build !windows && !linux && !darwin && !openbsd && !freebsd && !netbsd && !solaris
|
||||
// +build !windows,!linux,!darwin,!openbsd,!freebsd,!netbsd,!solaris
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const cursorColumn = true
|
||||
@@ -0,0 +1,6 @@
|
||||
module github.com/go-delve/liner
|
||||
|
||||
require (
|
||||
github.com/mattn/go-runewidth v0.0.3
|
||||
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
|
||||
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -0,0 +1,373 @@
|
||||
//go:build linux || darwin || openbsd || freebsd || netbsd || solaris
|
||||
// +build linux darwin openbsd freebsd netbsd solaris
|
||||
|
||||
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().
|
||||
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.inputRedirected = true
|
||||
}
|
||||
if _, err := getMode(syscall.Stdout); err != nil {
|
||||
s.outputRedirected = true
|
||||
}
|
||||
if s.inputRedirected && s.outputRedirected {
|
||||
s.terminalSupported = false
|
||||
}
|
||||
if s.terminalSupported && !s.inputRedirected && !s.outputRedirected {
|
||||
mode := s.origMode
|
||||
mode.Iflag &^= icrnl | inpck | istrip | ixon
|
||||
mode.Cflag |= cs8
|
||||
mode.Lflag &^= syscall.ECHO | icanon | iexten
|
||||
mode.Cc[syscall.VMIN] = 1
|
||||
mode.Cc[syscall.VTIME] = 0
|
||||
mode.ApplyMode()
|
||||
|
||||
winch := make(chan os.Signal, 1)
|
||||
signal.Notify(winch, syscall.SIGWINCH)
|
||||
s.winch = winch
|
||||
|
||||
s.checkOutput()
|
||||
}
|
||||
|
||||
if !s.outputRedirected {
|
||||
s.outputRedirected = !s.getColumns()
|
||||
}
|
||||
|
||||
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) inputWaiting() bool {
|
||||
return len(s.next) > 0
|
||||
}
|
||||
|
||||
func (s *State) restartPrompt() {
|
||||
next := make(chan nexter, 200)
|
||||
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, ErrInternal
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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, ErrInternal
|
||||
}
|
||||
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 1, 7:
|
||||
return home, nil
|
||||
case 4, 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 'b':
|
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return altB, nil
|
||||
case 'd':
|
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return altD, nil
|
||||
case bs:
|
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return altBs, nil
|
||||
case 'f':
|
||||
s.pending = s.pending[:0] // escape code complete
|
||||
return altF, 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 {
|
||||
signal.Stop(s.winch)
|
||||
if !s.inputRedirected {
|
||||
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.
|
||||
// Note that TerminalSupported does not check all factors that may
|
||||
// cause liner to not fully support the terminal (such as stdin redirection)
|
||||
func TerminalSupported() bool {
|
||||
bad := map[string]bool{"": true, "dumb": true, "cons25": true}
|
||||
return !bad[strings.ToLower(os.Getenv("TERM"))]
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
//go:build darwin
|
||||
// +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
|
||||
}
|
||||
|
||||
// Terminal.app needs a column for the cursor when the input line is at the
|
||||
// bottom of the window.
|
||||
const cursorColumn = true
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
//go:build linux
|
||||
// +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
|
||||
}
|
||||
|
||||
const cursorColumn = false
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package liner
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
getTermios = unix.TCGETS
|
||||
setTermios = unix.TCSETS
|
||||
)
|
||||
|
||||
const (
|
||||
icrnl = unix.ICRNL
|
||||
inpck = unix.INPCK
|
||||
istrip = unix.ISTRIP
|
||||
ixon = unix.IXON
|
||||
opost = unix.OPOST
|
||||
cs8 = unix.CS8
|
||||
isig = unix.ISIG
|
||||
icanon = unix.ICANON
|
||||
iexten = unix.IEXTEN
|
||||
)
|
||||
|
||||
type termios unix.Termios
|
||||
|
||||
const cursorColumn = false
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
//go:build !windows
|
||||
// +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')
|
||||
}
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
package liner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
procGetStdHandle = kernel32.NewProc("GetStdHandle")
|
||||
procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW")
|
||||
procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents")
|
||||
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 uint16
|
||||
ControlKeyState uint32
|
||||
}
|
||||
|
||||
// These names are from the Win32 api, so they use underscores (contrary to
|
||||
// what golint suggests)
|
||||
const (
|
||||
vk_back = 0x08
|
||||
vk_tab = 0x09
|
||||
vk_menu = 0x12 // ALT key
|
||||
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
|
||||
bKey = 0x42
|
||||
dKey = 0x44
|
||||
fKey = 0x46
|
||||
yKey = 0x59
|
||||
)
|
||||
|
||||
const (
|
||||
shiftPressed = 0x0010
|
||||
leftAltPressed = 0x0002
|
||||
leftCtrlPressed = 0x0008
|
||||
rightAltPressed = 0x0001
|
||||
rightCtrlPressed = 0x0004
|
||||
|
||||
modKeys = shiftPressed | leftAltPressed | rightAltPressed | leftCtrlPressed | rightCtrlPressed
|
||||
)
|
||||
|
||||
// inputWaiting only returns true if the next call to readNext will return immediately.
|
||||
func (s *State) inputWaiting() bool {
|
||||
var num uint32
|
||||
ok, _, _ := procGetNumberOfConsoleInputEvents.Call(uintptr(s.handle), uintptr(unsafe.Pointer(&num)))
|
||||
if ok == 0 {
|
||||
// call failed, so we cannot guarantee a non-blocking readNext
|
||||
return false
|
||||
}
|
||||
|
||||
// during a "paste" input events are always an odd number, and
|
||||
// the last one results in a blocking readNext, so return false
|
||||
// when num is 1 or 0.
|
||||
return num > 1
|
||||
}
|
||||
|
||||
func (s *State) readNext() (interface{}, error) {
|
||||
if s.repeat > 0 {
|
||||
s.repeat--
|
||||
return s.key, nil
|
||||
}
|
||||
|
||||
var input input_record
|
||||
var rv uint32
|
||||
|
||||
var surrogate uint16
|
||||
|
||||
for {
|
||||
ok, _, err := procReadConsoleInput.Call(uintptr(s.handle),
|
||||
uintptr(unsafe.Pointer(&input)), 1, uintptr(unsafe.Pointer(&rv)))
|
||||
|
||||
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 {
|
||||
if ke.VirtualKeyCode == vk_menu && ke.Char > 0 {
|
||||
// paste of unicode (eg. via ALT-numpad)
|
||||
if surrogate > 0 {
|
||||
return utf16.DecodeRune(rune(surrogate), rune(ke.Char)), nil
|
||||
} else if utf16.IsSurrogate(rune(ke.Char)) {
|
||||
surrogate = ke.Char
|
||||
continue
|
||||
} else {
|
||||
return rune(ke.Char), nil
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ke.VirtualKeyCode == vk_tab && ke.ControlKeyState&modKeys == shiftPressed {
|
||||
s.key = shiftTab
|
||||
} else if ke.VirtualKeyCode == vk_back && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
||||
ke.ControlKeyState&modKeys == rightAltPressed) {
|
||||
s.key = altBs
|
||||
} else if ke.VirtualKeyCode == bKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
||||
ke.ControlKeyState&modKeys == rightAltPressed) {
|
||||
s.key = altB
|
||||
} else if ke.VirtualKeyCode == dKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
||||
ke.ControlKeyState&modKeys == rightAltPressed) {
|
||||
s.key = altD
|
||||
} else if ke.VirtualKeyCode == fKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
||||
ke.ControlKeyState&modKeys == rightAltPressed) {
|
||||
s.key = altF
|
||||
} else if ke.VirtualKeyCode == yKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
||||
ke.ControlKeyState&modKeys == rightAltPressed) {
|
||||
s.key = altY
|
||||
} else if ke.Char > 0 {
|
||||
if surrogate > 0 {
|
||||
s.key = utf16.DecodeRune(rune(surrogate), rune(ke.Char))
|
||||
} else if utf16.IsSurrogate(rune(ke.Char)) {
|
||||
surrogate = ke.Char
|
||||
continue
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
const cursorColumn = true
|
||||
File diff suppressed because it is too large
Load Diff
+146
@@ -0,0 +1,146 @@
|
||||
package liner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"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")
|
||||
}
|
||||
|
||||
// clear the history and re-write
|
||||
s.ClearHistory()
|
||||
num, err = s.WriteHistory(&out)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error writing history", err)
|
||||
}
|
||||
if num != 0 {
|
||||
t.Fatal("Wrong number of history entries written, expected none")
|
||||
}
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumns(t *testing.T) {
|
||||
list := []string{"foo", "food", "This entry is quite a bit longer than the typical entry"}
|
||||
|
||||
output := []struct {
|
||||
width, columns, rows, maxWidth int
|
||||
}{
|
||||
{80, 1, 3, len(list[2]) + 1},
|
||||
{120, 2, 2, len(list[2]) + 1},
|
||||
{800, 14, 1, 0},
|
||||
{8, 1, 3, 7},
|
||||
}
|
||||
|
||||
for i, o := range output {
|
||||
col, row, max := calculateColumns(o.width, list)
|
||||
if col != o.columns {
|
||||
t.Fatalf("Wrong number of columns, %d != %d, in TestColumns %d\n", col, o.columns, i)
|
||||
}
|
||||
if row != o.rows {
|
||||
t.Fatalf("Wrong number of rows, %d != %d, in TestColumns %d\n", row, o.rows, i)
|
||||
}
|
||||
if max != o.maxWidth {
|
||||
t.Fatalf("Wrong column width, %d != %d, in TestColumns %d\n", max, o.maxWidth, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This example demonstrates a way to retrieve the current
|
||||
// history buffer without using a file.
|
||||
func ExampleState_WriteHistory() {
|
||||
var s State
|
||||
s.AppendHistory("foo")
|
||||
s.AppendHistory("bar")
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := s.WriteHistory(buf)
|
||||
if err == nil {
|
||||
history := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
||||
for i, line := range history {
|
||||
fmt.Println("History entry", i, ":", line)
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// History entry 0 : foo
|
||||
// History entry 1 : bar
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//go:build linux || darwin || openbsd || freebsd || netbsd || solaris
|
||||
// +build linux darwin openbsd freebsd netbsd solaris
|
||||
|
||||
package liner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func (s *State) moveUp(lines int) {
|
||||
fmt.Printf("\x1b[%dA", lines)
|
||||
}
|
||||
|
||||
func (s *State) moveDown(lines int) {
|
||||
fmt.Printf("\x1b[%dB", lines)
|
||||
}
|
||||
|
||||
func (s *State) emitNewLine() {
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
type winSize struct {
|
||||
row, col uint16
|
||||
xpixel, ypixel uint16
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package liner
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (s *State) getColumns() bool {
|
||||
ws, err := unix.IoctlGetWinsize(unix.Stdout, unix.TIOCGWINSZ)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
s.columns = int(ws.Col)
|
||||
return true
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build linux || darwin || openbsd || freebsd || netbsd
|
||||
// +build linux darwin openbsd freebsd netbsd
|
||||
|
||||
package liner
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (s *State) getColumns() bool {
|
||||
var ws winSize
|
||||
ok, _, _ := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdout),
|
||||
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws)))
|
||||
if int(ok) < 0 {
|
||||
return false
|
||||
}
|
||||
s.columns = int(ws.col)
|
||||
return true
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
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) moveUp(lines int) {
|
||||
var sbi consoleScreenBufferInfo
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi)))
|
||||
procSetConsoleCursorPosition.Call(uintptr(s.hOut),
|
||||
uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|(int(sbi.dwCursorPosition.y)-lines)<<16))
|
||||
}
|
||||
|
||||
func (s *State) moveDown(lines int) {
|
||||
var sbi consoleScreenBufferInfo
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi)))
|
||||
procSetConsoleCursorPosition.Call(uintptr(s.hOut),
|
||||
uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|(int(sbi.dwCursorPosition.y)+lines)<<16))
|
||||
}
|
||||
|
||||
func (s *State) emitNewLine() {
|
||||
// windows doesn't need to omit a new line
|
||||
}
|
||||
|
||||
func (s *State) getColumns() {
|
||||
var sbi consoleScreenBufferInfo
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi)))
|
||||
s.columns = int(sbi.dwSize.x)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
//go:build windows || linux || darwin || openbsd || freebsd || netbsd || solaris
|
||||
// +build windows linux darwin openbsd freebsd netbsd solaris
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
//go:build race
|
||||
// +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()
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// +build linux darwin openbsd freebsd netbsd
|
||||
|
||||
package liner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func handleCtrlZ() {
|
||||
pid := os.Getpid()
|
||||
pgrp, err := syscall.Getpgid(pid)
|
||||
if err == nil {
|
||||
syscall.Kill(-pgrp, syscall.SIGTSTP)
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package liner
|
||||
|
||||
func handleCtrlZ() {}
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build linux || darwin || freebsd || openbsd || netbsd
|
||||
// +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) {
|
||||
return getMode(syscall.Stdin)
|
||||
}
|
||||
|
||||
func getMode(handle int) (*termios, error) {
|
||||
var mode termios
|
||||
var err error
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(handle), getTermios, uintptr(unsafe.Pointer(&mode)))
|
||||
if errno != 0 {
|
||||
err = errno
|
||||
}
|
||||
|
||||
return &mode, err
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package liner
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (mode *termios) ApplyMode() error {
|
||||
return unix.IoctlSetTermios(unix.Stdin, setTermios, (*unix.Termios)(mode))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return getMode(unix.Stdin)
|
||||
}
|
||||
|
||||
func getMode(handle int) (*termios, error) {
|
||||
tos, err := unix.IoctlGetTermios(handle, getTermios)
|
||||
return (*termios)(tos), err
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package liner
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// countGlyphs considers zero-width characters to be zero glyphs wide,
|
||||
// and members of Chinese, Japanese, and Korean scripts to be 2 glyphs wide.
|
||||
func countGlyphs(s []rune) int {
|
||||
n := 0
|
||||
for _, r := range s {
|
||||
// speed up the common case
|
||||
if r < 127 {
|
||||
n++
|
||||
continue
|
||||
}
|
||||
|
||||
n += runewidth.RuneWidth(r)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func countMultiLineGlyphs(s []rune, columns int, start int) int {
|
||||
n := start
|
||||
for _, r := range s {
|
||||
if r < 127 {
|
||||
n++
|
||||
continue
|
||||
}
|
||||
switch runewidth.RuneWidth(r) {
|
||||
case 0:
|
||||
case 1:
|
||||
n++
|
||||
case 2:
|
||||
n += 2
|
||||
// no room for a 2-glyphs-wide char in the ending
|
||||
// so skip a column and display it at the beginning
|
||||
if n%columns == 1 {
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func getPrefixGlyphs(s []rune, num int) []rune {
|
||||
p := 0
|
||||
for n := 0; n < num && p < len(s); p++ {
|
||||
// speed up the common case
|
||||
if s[p] < 127 {
|
||||
n++
|
||||
continue
|
||||
}
|
||||
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-- {
|
||||
// speed up the common case
|
||||
if s[p-1] < 127 {
|
||||
n++
|
||||
continue
|
||||
}
|
||||
if !unicode.IsOneOf(zeroWidth, s[p-1]) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return s[p:]
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
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
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
s []rune
|
||||
glyphs int
|
||||
}
|
||||
|
||||
var testCases = []testCase{
|
||||
{[]rune("query"), 5},
|
||||
{[]rune("私"), 2},
|
||||
{[]rune("hello『世界』"), 13},
|
||||
}
|
||||
|
||||
func TestCountGlyphs(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
count := countGlyphs(testCase.s)
|
||||
if count != testCase.glyphs {
|
||||
t.Errorf("ASCII count incorrect. %d != %d", count, testCase.glyphs)
|
||||
}
|
||||
count = countGlyphs(accent(testCase.s))
|
||||
if count != testCase.glyphs {
|
||||
t.Errorf("Accent count incorrect. %d != %d", count, testCase.glyphs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 _, testCase := range testCases {
|
||||
for i := 0; i <= len(testCase.s); i++ {
|
||||
iter := strconv.Itoa(i)
|
||||
out := getPrefixGlyphs(testCase.s, i)
|
||||
compare(out, testCase.s[:i], "ascii prefix "+iter, t)
|
||||
out = getPrefixGlyphs(accent(testCase.s), i)
|
||||
compare(out, accent(testCase.s[:i]), "accent prefix "+iter, t)
|
||||
}
|
||||
out := getPrefixGlyphs(testCase.s, 999)
|
||||
compare(out, testCase.s, "ascii prefix overflow", t)
|
||||
out = getPrefixGlyphs(accent(testCase.s), 999)
|
||||
compare(out, accent(testCase.s), "accent prefix overflow", t)
|
||||
|
||||
out = getPrefixGlyphs(testCase.s, -3)
|
||||
if len(out) != 0 {
|
||||
t.Error("ascii prefix negative")
|
||||
}
|
||||
out = getPrefixGlyphs(accent(testCase.s), -3)
|
||||
if len(out) != 0 {
|
||||
t.Error("accent prefix negative")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixGlyphs(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
for i := 0; i <= len(testCase.s); i++ {
|
||||
iter := strconv.Itoa(i)
|
||||
out := getSuffixGlyphs(testCase.s, i)
|
||||
compare(out, testCase.s[len(testCase.s)-i:], "ascii suffix "+iter, t)
|
||||
out = getSuffixGlyphs(accent(testCase.s), i)
|
||||
compare(out, accent(testCase.s[len(testCase.s)-i:]), "accent suffix "+iter, t)
|
||||
}
|
||||
out := getSuffixGlyphs(testCase.s, 999)
|
||||
compare(out, testCase.s, "ascii suffix overflow", t)
|
||||
out = getSuffixGlyphs(accent(testCase.s), 999)
|
||||
compare(out, accent(testCase.s), "accent suffix overflow", t)
|
||||
|
||||
out = getSuffixGlyphs(testCase.s, -3)
|
||||
if len(out) != 0 {
|
||||
t.Error("ascii suffix negative")
|
||||
}
|
||||
out = getSuffixGlyphs(accent(testCase.s), -3)
|
||||
if len(out) != 0 {
|
||||
t.Error("accent suffix negative")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user