| // +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 |
| } |
| // 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 '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"))] |
| } |