1// +build linux darwin openbsd freebsd netbsd 2 3package liner 4 5import ( 6 "bufio" 7 "errors" 8 "os" 9 "os/signal" 10 "strconv" 11 "strings" 12 "syscall" 13 "time" 14) 15 16type nexter struct { 17 r rune 18 err error 19} 20 21// State represents an open terminal 22type State struct { 23 commonState 24 origMode termios 25 defaultMode termios 26 next <-chan nexter 27 winch chan os.Signal 28 pending []rune 29 useCHA bool 30} 31 32// NewLiner initializes a new *State, and sets the terminal into raw mode. To 33// restore the terminal to its previous state, call State.Close(). 34func NewLiner() *State { 35 var s State 36 s.r = bufio.NewReader(os.Stdin) 37 38 s.terminalSupported = TerminalSupported() 39 if m, err := TerminalMode(); err == nil { 40 s.origMode = *m.(*termios) 41 } else { 42 s.inputRedirected = true 43 } 44 if _, err := getMode(syscall.Stdout); err != 0 { 45 s.outputRedirected = true 46 } 47 if s.inputRedirected && s.outputRedirected { 48 s.terminalSupported = false 49 } 50 if s.terminalSupported && !s.inputRedirected && !s.outputRedirected { 51 mode := s.origMode 52 mode.Iflag &^= icrnl | inpck | istrip | ixon 53 mode.Cflag |= cs8 54 mode.Lflag &^= syscall.ECHO | icanon | iexten 55 mode.ApplyMode() 56 57 winch := make(chan os.Signal, 1) 58 signal.Notify(winch, syscall.SIGWINCH) 59 s.winch = winch 60 61 s.checkOutput() 62 } 63 64 if !s.outputRedirected { 65 s.outputRedirected = !s.getColumns() 66 } 67 68 return &s 69} 70 71var errTimedOut = errors.New("timeout") 72 73func (s *State) startPrompt() { 74 if s.terminalSupported { 75 if m, err := TerminalMode(); err == nil { 76 s.defaultMode = *m.(*termios) 77 mode := s.defaultMode 78 mode.Lflag &^= isig 79 mode.ApplyMode() 80 } 81 } 82 s.restartPrompt() 83} 84 85func (s *State) inputWaiting() bool { 86 return len(s.next) > 0 87} 88 89func (s *State) restartPrompt() { 90 next := make(chan nexter, 200) 91 go func() { 92 for { 93 var n nexter 94 n.r, _, n.err = s.r.ReadRune() 95 next <- n 96 // Shut down nexter loop when an end condition has been reached 97 if n.err != nil || n.r == '\n' || n.r == '\r' || n.r == ctrlC || n.r == ctrlD { 98 close(next) 99 return 100 } 101 } 102 }() 103 s.next = next 104} 105 106func (s *State) stopPrompt() { 107 if s.terminalSupported { 108 s.defaultMode.ApplyMode() 109 } 110} 111 112func (s *State) nextPending(timeout <-chan time.Time) (rune, error) { 113 select { 114 case thing, ok := <-s.next: 115 if !ok { 116 return 0, ErrInternal 117 } 118 if thing.err != nil { 119 return 0, thing.err 120 } 121 s.pending = append(s.pending, thing.r) 122 return thing.r, nil 123 case <-timeout: 124 rv := s.pending[0] 125 s.pending = s.pending[1:] 126 return rv, errTimedOut 127 } 128} 129 130func (s *State) readNext() (interface{}, error) { 131 if len(s.pending) > 0 { 132 rv := s.pending[0] 133 s.pending = s.pending[1:] 134 return rv, nil 135 } 136 var r rune 137 select { 138 case thing, ok := <-s.next: 139 if !ok { 140 return 0, ErrInternal 141 } 142 if thing.err != nil { 143 return nil, thing.err 144 } 145 r = thing.r 146 case <-s.winch: 147 s.getColumns() 148 return winch, nil 149 } 150 if r != esc { 151 return r, nil 152 } 153 s.pending = append(s.pending, r) 154 155 // Wait at most 50 ms for the rest of the escape sequence 156 // If nothing else arrives, it was an actual press of the esc key 157 timeout := time.After(50 * time.Millisecond) 158 flag, err := s.nextPending(timeout) 159 if err != nil { 160 if err == errTimedOut { 161 return flag, nil 162 } 163 return unknown, err 164 } 165 166 switch flag { 167 case '[': 168 code, err := s.nextPending(timeout) 169 if err != nil { 170 if err == errTimedOut { 171 return code, nil 172 } 173 return unknown, err 174 } 175 switch code { 176 case 'A': 177 s.pending = s.pending[:0] // escape code complete 178 return up, nil 179 case 'B': 180 s.pending = s.pending[:0] // escape code complete 181 return down, nil 182 case 'C': 183 s.pending = s.pending[:0] // escape code complete 184 return right, nil 185 case 'D': 186 s.pending = s.pending[:0] // escape code complete 187 return left, nil 188 case 'F': 189 s.pending = s.pending[:0] // escape code complete 190 return end, nil 191 case 'H': 192 s.pending = s.pending[:0] // escape code complete 193 return home, nil 194 case 'Z': 195 s.pending = s.pending[:0] // escape code complete 196 return shiftTab, nil 197 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 198 num := []rune{code} 199 for { 200 code, err := s.nextPending(timeout) 201 if err != nil { 202 if err == errTimedOut { 203 return code, nil 204 } 205 return nil, err 206 } 207 switch code { 208 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 209 num = append(num, code) 210 case ';': 211 // Modifier code to follow 212 // This only supports Ctrl-left and Ctrl-right for now 213 x, _ := strconv.ParseInt(string(num), 10, 32) 214 if x != 1 { 215 // Can't be left or right 216 rv := s.pending[0] 217 s.pending = s.pending[1:] 218 return rv, nil 219 } 220 num = num[:0] 221 for { 222 code, err = s.nextPending(timeout) 223 if err != nil { 224 if err == errTimedOut { 225 rv := s.pending[0] 226 s.pending = s.pending[1:] 227 return rv, nil 228 } 229 return nil, err 230 } 231 switch code { 232 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 233 num = append(num, code) 234 case 'C', 'D': 235 // right, left 236 mod, _ := strconv.ParseInt(string(num), 10, 32) 237 if mod != 5 { 238 // Not bare Ctrl 239 rv := s.pending[0] 240 s.pending = s.pending[1:] 241 return rv, nil 242 } 243 s.pending = s.pending[:0] // escape code complete 244 if code == 'C' { 245 return wordRight, nil 246 } 247 return wordLeft, nil 248 default: 249 // Not left or right 250 rv := s.pending[0] 251 s.pending = s.pending[1:] 252 return rv, nil 253 } 254 } 255 case '~': 256 s.pending = s.pending[:0] // escape code complete 257 x, _ := strconv.ParseInt(string(num), 10, 32) 258 switch x { 259 case 2: 260 return insert, nil 261 case 3: 262 return del, nil 263 case 5: 264 return pageUp, nil 265 case 6: 266 return pageDown, nil 267 case 1, 7: 268 return home, nil 269 case 4, 8: 270 return end, nil 271 case 15: 272 return f5, nil 273 case 17: 274 return f6, nil 275 case 18: 276 return f7, nil 277 case 19: 278 return f8, nil 279 case 20: 280 return f9, nil 281 case 21: 282 return f10, nil 283 case 23: 284 return f11, nil 285 case 24: 286 return f12, nil 287 default: 288 return unknown, nil 289 } 290 default: 291 // unrecognized escape code 292 rv := s.pending[0] 293 s.pending = s.pending[1:] 294 return rv, nil 295 } 296 } 297 } 298 299 case 'O': 300 code, err := s.nextPending(timeout) 301 if err != nil { 302 if err == errTimedOut { 303 return code, nil 304 } 305 return nil, err 306 } 307 s.pending = s.pending[:0] // escape code complete 308 switch code { 309 case 'c': 310 return wordRight, nil 311 case 'd': 312 return wordLeft, nil 313 case 'H': 314 return home, nil 315 case 'F': 316 return end, nil 317 case 'P': 318 return f1, nil 319 case 'Q': 320 return f2, nil 321 case 'R': 322 return f3, nil 323 case 'S': 324 return f4, nil 325 default: 326 return unknown, nil 327 } 328 case 'b': 329 s.pending = s.pending[:0] // escape code complete 330 return altB, nil 331 case 'd': 332 s.pending = s.pending[:0] // escape code complete 333 return altD, nil 334 case bs: 335 s.pending = s.pending[:0] // escape code complete 336 return altBs, nil 337 case 'f': 338 s.pending = s.pending[:0] // escape code complete 339 return altF, nil 340 case 'y': 341 s.pending = s.pending[:0] // escape code complete 342 return altY, nil 343 default: 344 rv := s.pending[0] 345 s.pending = s.pending[1:] 346 return rv, nil 347 } 348 349 // not reached 350 return r, nil 351} 352 353// Close returns the terminal to its previous mode 354func (s *State) Close() error { 355 signal.Stop(s.winch) 356 if !s.inputRedirected { 357 s.origMode.ApplyMode() 358 } 359 return nil 360} 361 362// TerminalSupported returns true if the current terminal supports 363// line editing features, and false if liner will use the 'dumb' 364// fallback for input. 365// Note that TerminalSupported does not check all factors that may 366// cause liner to not fully support the terminal (such as stdin redirection) 367func TerminalSupported() bool { 368 bad := map[string]bool{"": true, "dumb": true, "cons25": true} 369 return !bad[strings.ToLower(os.Getenv("TERM"))] 370} 371