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 != 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.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"))] } 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