mirror of https://github.com/tidwall/tile38.git
367 lines
7.9 KiB
Go
367 lines
7.9 KiB
Go
// +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.inputRedirected = true
|
|
}
|
|
if _, err := getMode(syscall.Stdout); err != 0 {
|
|
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.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
|
|
}
|
|
}
|
|
|
|
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 'b':
|
|
s.pending = s.pending[:0] // escape code complete
|
|
return altB, 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 {
|
|
stopSignal(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"))]
|
|
}
|